diff --git a/bun.config.ts b/bun.config.ts index 3d959da..811eb37 100644 --- a/bun.config.ts +++ b/bun.config.ts @@ -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: 'ws', entry: 'src/ws.ts', dts: true, external }); + +await buildWithBun({ naming: 'commander', entry: 'src/commander.ts', dts: true, external }); diff --git a/package.json b/package.json index c6d36e5..5f2f01a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@kevisual/router", - "version": "0.0.86", + "version": "0.0.87", "description": "", "type": "module", "main": "./dist/router.js", @@ -35,6 +35,7 @@ "@types/send": "^1.2.1", "@types/ws": "^8.18.1", "@types/xml2js": "^0.4.14", + "commander": "^14.0.3", "eventemitter3": "^5.0.4", "fast-glob": "^3.3.3", "hono": "^4.12.5", @@ -59,6 +60,7 @@ "exports": { ".": "./dist/router.js", "./browser": "./dist/router-browser.js", + "./commander": "./dist/commander.js", "./simple": "./dist/router-simple.js", "./opencode": "./dist/opencode.js", "./skill": "./dist/app.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b40c9f4..d9c79a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: '@types/xml2js': specifier: ^0.4.14 version: 0.4.14 + commander: + specifier: ^14.0.3 + version: 14.0.3 eventemitter3: specifier: ^5.0.4 version: 5.0.4 @@ -405,6 +408,10 @@ packages: bun-types@1.3.10: 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: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -938,6 +945,8 @@ snapshots: dependencies: '@types/node': 25.3.5 + commander@14.0.3: {} + commondir@1.0.1: {} cookie@1.1.1: {} diff --git a/src/commander.ts b/src/commander.ts new file mode 100644 index 0000000..3fb672d --- /dev/null +++ b/src/commander.ts @@ -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); +} +export const parseArgs = (args: string) => { + try { + return JSON.parse(args); + } catch { + // 尝试解析 a=b b=c 格式 + const result: Record = {}; + 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 ', '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); + } +} \ No newline at end of file