chore: 添加 commander 模块及相关命令行工具功能

This commit is contained in:
2026-03-07 01:41:47 +08:00
parent a9cf1505ff
commit 7345940f18
4 changed files with 129 additions and 1 deletions

View File

@@ -14,3 +14,5 @@ await buildWithBun({ naming: 'router-simple', entry: 'src/router-simple.ts', dts
await buildWithBun({ naming: 'opencode', entry: 'src/opencode.ts', dts: true, external }); await buildWithBun({ naming: 'opencode', entry: 'src/opencode.ts', dts: true, external });
await buildWithBun({ naming: 'ws', entry: 'src/ws.ts', dts: true, external }); await buildWithBun({ naming: 'ws', entry: 'src/ws.ts', dts: true, external });
await buildWithBun({ naming: 'commander', entry: 'src/commander.ts', dts: true, external });

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://json.schemastore.org/package", "$schema": "https://json.schemastore.org/package",
"name": "@kevisual/router", "name": "@kevisual/router",
"version": "0.0.86", "version": "0.0.87",
"description": "", "description": "",
"type": "module", "type": "module",
"main": "./dist/router.js", "main": "./dist/router.js",
@@ -35,6 +35,7 @@
"@types/send": "^1.2.1", "@types/send": "^1.2.1",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14", "@types/xml2js": "^0.4.14",
"commander": "^14.0.3",
"eventemitter3": "^5.0.4", "eventemitter3": "^5.0.4",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"hono": "^4.12.5", "hono": "^4.12.5",
@@ -59,6 +60,7 @@
"exports": { "exports": {
".": "./dist/router.js", ".": "./dist/router.js",
"./browser": "./dist/router-browser.js", "./browser": "./dist/router-browser.js",
"./commander": "./dist/commander.js",
"./simple": "./dist/router-simple.js", "./simple": "./dist/router-simple.js",
"./opencode": "./dist/opencode.js", "./opencode": "./dist/opencode.js",
"./skill": "./dist/app.js", "./skill": "./dist/app.js",

9
pnpm-lock.yaml generated
View File

@@ -51,6 +51,9 @@ importers:
'@types/xml2js': '@types/xml2js':
specifier: ^0.4.14 specifier: ^0.4.14
version: 0.4.14 version: 0.4.14
commander:
specifier: ^14.0.3
version: 14.0.3
eventemitter3: eventemitter3:
specifier: ^5.0.4 specifier: ^5.0.4
version: 5.0.4 version: 5.0.4
@@ -405,6 +408,10 @@ packages:
bun-types@1.3.10: bun-types@1.3.10:
resolution: {integrity: sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg==} resolution: {integrity: sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg==}
commander@14.0.3:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'}
commondir@1.0.1: commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
@@ -938,6 +945,8 @@ snapshots:
dependencies: dependencies:
'@types/node': 25.3.5 '@types/node': 25.3.5
commander@14.0.3: {}
commondir@1.0.1: {} commondir@1.0.1: {}
cookie@1.1.1: {} cookie@1.1.1: {}

115
src/commander.ts Normal file
View File

@@ -0,0 +1,115 @@
import { program } from 'commander';
import { App } from './app.ts';
export const groupByPath = (routes: App['routes']) => {
return routes.reduce((acc, route) => {
const path = route.path || 'default';
if (!acc[path]) {
acc[path] = [];
}
acc[path].push(route);
return acc;
}, {} as Record<string, typeof routes>);
}
export const parseArgs = (args: string) => {
try {
return JSON.parse(args);
} catch {
// 尝试解析 a=b b=c 格式
const result: Record<string, string> = {};
const pairs = args.match(/(\S+?)=(\S+)/g);
if (pairs && pairs.length > 0) {
for (const pair of pairs) {
const idx = pair.indexOf('=');
const key = pair.slice(0, idx);
const value = pair.slice(idx + 1);
result[key] = value;
}
return result;
}
throw new Error('Invalid arguments: expected JSON or key=value pairs (e.g. a=b c=d)');
}
}
export const parseDescription = (route: App['routes'][number]) => {
let desc = '';
if (route.metadata?.skill) {
desc += `\n\t=====${route.metadata.skill}=====\n`;
}
let hasSummary = false;
if (route.metadata?.summary) {
desc += `\t${route.metadata.summary}`;
hasSummary = true;
}
if (route.metadata?.args) {
const argsLines = Object.entries(route.metadata.args).map(([key, schema]: [string, any]) => {
const defType: string = schema?._def?.type ?? schema?.type ?? '';
const isOptional = defType === 'optional';
const innerType: string = isOptional
? (schema?._def?.innerType?.type ?? schema?._def?.innerType?._def?.type ?? '')
: defType;
const description: string =
schema?.description ??
schema?._def?.description ??
'';
const optionalMark = isOptional ? '?' : '';
const descPart = description ? ` ${description}` : '';
return `\t - ${key}${optionalMark}: ${innerType}${descPart}`;
});
desc += '\n\targs:\n' + argsLines.join('\n');
}
if (route.description && !hasSummary) {
desc += `\t - ${route.description}`;
}
return desc;
}
export const createCommand = (opts: { app: App, program: typeof program }) => {
const { app, program } = opts;
const routes = app.routes;
const groupRoutes = groupByPath(routes);
for (const path in groupRoutes) {
const routeList = groupRoutes[path];
const keys = routeList.map(route => route.key).filter(Boolean);
const subProgram = program.command(path).description(`路由《${path}${keys.length > 0 ? ': ' + keys.join(', ') : ''}`);
routeList.forEach(route => {
if (!route.key) return;
const description = parseDescription(route);
subProgram.command(route.key)
.description(description || '')
.option('--args <args>', 'JSON字符串参数传递给命令执行')
.action(async (options) => {
const output = (data: any) => {
if (typeof data === 'object') {
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
} else {
process.stdout.write(String(data) + '\n');
}
}
try {
const args = options.args ? parseArgs(options.args) : {};
// 这里可以添加实际的命令执行逻辑,例如调用对应的路由处理函数
const res = await app.run({ path, key: route.key, payload: args }, { appId: app.appId });
if (res.code === 200) {
output(res.data);
} else {
output(`Error: ${res.message}`);
}
} catch (error) {
output(`Execution error: ${error instanceof Error ? error.message : String(error)}`);
}
});
});
}
}
program.parse(process.argv);
export const parse = (opts: { app: App, description?: string, parse?: boolean }) => {
const { app, description, parse } = opts;
program.description(description || 'Router 命令行工具');
createCommand({ app: app as App, program });
if (parse) {
program.parse(process.argv);
}
}