incur 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/SKILL.md +664 -0
- package/dist/Cli.d.ts +255 -0
- package/dist/Cli.d.ts.map +1 -0
- package/dist/Cli.js +900 -0
- package/dist/Cli.js.map +1 -0
- package/dist/Errors.d.ts +92 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +75 -0
- package/dist/Errors.js.map +1 -0
- package/dist/Formatter.d.ts +5 -0
- package/dist/Formatter.d.ts.map +1 -0
- package/dist/Formatter.js +91 -0
- package/dist/Formatter.js.map +1 -0
- package/dist/Help.d.ts +53 -0
- package/dist/Help.d.ts.map +1 -0
- package/dist/Help.js +231 -0
- package/dist/Help.js.map +1 -0
- package/dist/Mcp.d.ts +13 -0
- package/dist/Mcp.d.ts.map +1 -0
- package/dist/Mcp.js +140 -0
- package/dist/Mcp.js.map +1 -0
- package/dist/Parser.d.ts +24 -0
- package/dist/Parser.d.ts.map +1 -0
- package/dist/Parser.js +215 -0
- package/dist/Parser.js.map +1 -0
- package/dist/Register.d.ts +19 -0
- package/dist/Register.d.ts.map +1 -0
- package/dist/Register.js +2 -0
- package/dist/Register.js.map +1 -0
- package/dist/Schema.d.ts +4 -0
- package/dist/Schema.d.ts.map +1 -0
- package/dist/Schema.js +8 -0
- package/dist/Schema.js.map +1 -0
- package/dist/Skill.d.ts +29 -0
- package/dist/Skill.d.ts.map +1 -0
- package/dist/Skill.js +196 -0
- package/dist/Skill.js.map +1 -0
- package/dist/Skillgen.d.ts +3 -0
- package/dist/Skillgen.d.ts.map +1 -0
- package/dist/Skillgen.js +67 -0
- package/dist/Skillgen.js.map +1 -0
- package/dist/SyncMcp.d.ts +23 -0
- package/dist/SyncMcp.d.ts.map +1 -0
- package/dist/SyncMcp.js +100 -0
- package/dist/SyncMcp.js.map +1 -0
- package/dist/SyncSkills.d.ts +38 -0
- package/dist/SyncSkills.d.ts.map +1 -0
- package/dist/SyncSkills.js +163 -0
- package/dist/SyncSkills.js.map +1 -0
- package/dist/Typegen.d.ts +6 -0
- package/dist/Typegen.d.ts.map +1 -0
- package/dist/Typegen.js +92 -0
- package/dist/Typegen.js.map +1 -0
- package/dist/bin.d.ts +14 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +30 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/pm.d.ts +3 -0
- package/dist/internal/pm.d.ts.map +1 -0
- package/dist/internal/pm.js +11 -0
- package/dist/internal/pm.js.map +1 -0
- package/dist/internal/types.d.ts +11 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +2 -0
- package/dist/internal/types.js.map +1 -0
- package/dist/internal/utils.d.ts +8 -0
- package/dist/internal/utils.d.ts.map +1 -0
- package/dist/internal/utils.js +51 -0
- package/dist/internal/utils.js.map +1 -0
- package/examples/npm/cli.ts +180 -0
- package/examples/npm/node_modules/.bin/incur.src +21 -0
- package/examples/npm/node_modules/.bin/tsx +21 -0
- package/examples/npm/package.json +14 -0
- package/examples/npm/tsconfig.json +9 -0
- package/examples/presto/cli.ts +246 -0
- package/examples/presto/node_modules/.bin/incur.src +21 -0
- package/examples/presto/node_modules/.bin/tsx +21 -0
- package/examples/presto/package.json +14 -0
- package/examples/presto/tsconfig.json +9 -0
- package/package.json +53 -2
- package/src/Cli.test-d.ts +135 -0
- package/src/Cli.test.ts +1373 -0
- package/src/Cli.ts +1470 -0
- package/src/Errors.test.ts +96 -0
- package/src/Errors.ts +139 -0
- package/src/Formatter.test.ts +245 -0
- package/src/Formatter.ts +106 -0
- package/src/Help.test.ts +124 -0
- package/src/Help.ts +302 -0
- package/src/Mcp.test.ts +254 -0
- package/src/Mcp.ts +195 -0
- package/src/Parser.test-d.ts +45 -0
- package/src/Parser.test.ts +118 -0
- package/src/Parser.ts +247 -0
- package/src/Register.ts +18 -0
- package/src/Schema.test.ts +125 -0
- package/src/Schema.ts +8 -0
- package/src/Skill.test.ts +293 -0
- package/src/Skill.ts +253 -0
- package/src/Skillgen.ts +66 -0
- package/src/SyncMcp.test.ts +75 -0
- package/src/SyncMcp.ts +132 -0
- package/src/SyncSkills.test.ts +92 -0
- package/src/SyncSkills.ts +205 -0
- package/src/Typegen.test.ts +150 -0
- package/src/Typegen.ts +107 -0
- package/src/bin.ts +33 -0
- package/src/e2e.test.ts +1710 -0
- package/src/index.ts +14 -0
- package/src/internal/pm.test.ts +38 -0
- package/src/internal/pm.ts +8 -0
- package/src/internal/types.ts +22 -0
- package/src/internal/utils.ts +50 -0
- package/src/tsconfig.json +8 -0
package/src/Cli.ts
ADDED
|
@@ -0,0 +1,1470 @@
|
|
|
1
|
+
import type { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import type { FieldError } from './Errors.js'
|
|
4
|
+
import { IncurError, ValidationError } from './Errors.js'
|
|
5
|
+
import * as Formatter from './Formatter.js'
|
|
6
|
+
import * as Help from './Help.js'
|
|
7
|
+
import { detectRunner } from './internal/pm.js'
|
|
8
|
+
import type { OneOf } from './internal/types.js'
|
|
9
|
+
import * as Mcp from './Mcp.js'
|
|
10
|
+
import * as Parser from './Parser.js'
|
|
11
|
+
import type { Register } from './Register.js'
|
|
12
|
+
import * as Schema from './Schema.js'
|
|
13
|
+
import * as Skill from './Skill.js'
|
|
14
|
+
import * as SyncMcp from './SyncMcp.js'
|
|
15
|
+
import * as SyncSkills from './SyncSkills.js'
|
|
16
|
+
|
|
17
|
+
/** A CLI application instance. Also used as a command group when mounted on a parent CLI. */
|
|
18
|
+
export type Cli<commands extends CommandsMap = {}> = {
|
|
19
|
+
/** Registers a root command or mounts a sub-CLI as a command group. */
|
|
20
|
+
command: {
|
|
21
|
+
/** Registers a command. Returns the CLI instance for chaining. */
|
|
22
|
+
<
|
|
23
|
+
const name extends string,
|
|
24
|
+
const args extends z.ZodObject<any> | undefined = undefined,
|
|
25
|
+
const env extends z.ZodObject<any> | undefined = undefined,
|
|
26
|
+
const options extends z.ZodObject<any> | undefined = undefined,
|
|
27
|
+
const output extends z.ZodType | undefined = undefined,
|
|
28
|
+
>(
|
|
29
|
+
name: name,
|
|
30
|
+
definition: CommandDefinition<args, env, options, output>,
|
|
31
|
+
): Cli<commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<options> } }>
|
|
32
|
+
/** Mounts a sub-CLI as a command group. */
|
|
33
|
+
<const name extends string, const sub extends CommandsMap>(
|
|
34
|
+
cli: Cli<sub> & { name: name },
|
|
35
|
+
): Cli<commands & { [key in keyof sub & string as `${name} ${key}`]: sub[key] }>
|
|
36
|
+
/** Mounts a root CLI as a single command. */
|
|
37
|
+
<
|
|
38
|
+
const name extends string,
|
|
39
|
+
const args extends z.ZodObject<any> | undefined,
|
|
40
|
+
const opts extends z.ZodObject<any> | undefined,
|
|
41
|
+
>(
|
|
42
|
+
cli: Root<args, opts> & { name: name },
|
|
43
|
+
): Cli<commands & { [key in name]: { args: InferOutput<args>; options: InferOutput<opts> } }>
|
|
44
|
+
}
|
|
45
|
+
/** A short description of the CLI. */
|
|
46
|
+
description?: string | undefined
|
|
47
|
+
/** The name of the CLI application. */
|
|
48
|
+
name: string
|
|
49
|
+
/** Parses argv, runs the matched command, and writes the output envelope to stdout. */
|
|
50
|
+
serve(argv?: string[], options?: serve.Options): Promise<void>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Root CLI — a single command with no subcommands. Carries phantom generics for mounting inference. */
|
|
54
|
+
export type Root<
|
|
55
|
+
_args extends z.ZodObject<any> | undefined = undefined,
|
|
56
|
+
_options extends z.ZodObject<any> | undefined = undefined,
|
|
57
|
+
> = Omit<Cli, 'command'>
|
|
58
|
+
|
|
59
|
+
/** Extracts the commands map from the registered type. */
|
|
60
|
+
export type Commands = Register extends { commands: infer commands extends CommandsMap }
|
|
61
|
+
? commands
|
|
62
|
+
: {}
|
|
63
|
+
|
|
64
|
+
/** Call to action. */
|
|
65
|
+
export type Cta<commands extends CommandsMap = Commands> =
|
|
66
|
+
| ([keyof commands] extends [never] ? string : (keyof commands & string) | (string & {}))
|
|
67
|
+
| ([keyof commands] extends [never]
|
|
68
|
+
? {
|
|
69
|
+
/** Positional arguments appended as bare values. */
|
|
70
|
+
args?: Record<string, unknown> | undefined
|
|
71
|
+
/** The command name to run. */
|
|
72
|
+
command: string
|
|
73
|
+
/** A short description of what the command does. */
|
|
74
|
+
description?: string | undefined
|
|
75
|
+
/** Named options formatted as `--key value` flags. */
|
|
76
|
+
options?: Record<string, unknown> | undefined
|
|
77
|
+
}
|
|
78
|
+
:
|
|
79
|
+
| {
|
|
80
|
+
[name in keyof commands & string]: {
|
|
81
|
+
/** Positional arguments appended as bare values. */
|
|
82
|
+
args?:
|
|
83
|
+
| { [key in keyof commands[name]['args']]?: commands[name]['args'][key] | true }
|
|
84
|
+
| undefined
|
|
85
|
+
/** The command name to run. */
|
|
86
|
+
command: name
|
|
87
|
+
/** A short description of what the command does. */
|
|
88
|
+
description?: string | undefined
|
|
89
|
+
/** Named options formatted as `--key value` flags. */
|
|
90
|
+
options?:
|
|
91
|
+
| {
|
|
92
|
+
[key in keyof commands[name]['options']]?:
|
|
93
|
+
| commands[name]['options'][key]
|
|
94
|
+
| true
|
|
95
|
+
}
|
|
96
|
+
| undefined
|
|
97
|
+
}
|
|
98
|
+
}[keyof commands & string]
|
|
99
|
+
| {
|
|
100
|
+
/** The command name to run. */
|
|
101
|
+
command: string & {}
|
|
102
|
+
/** A short description of what the command does. */
|
|
103
|
+
description?: string | undefined
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
/** Creates a leaf CLI with a root handler and no subcommands. */
|
|
107
|
+
export function create<
|
|
108
|
+
const args extends z.ZodObject<any> | undefined = undefined,
|
|
109
|
+
const env extends z.ZodObject<any> | undefined = undefined,
|
|
110
|
+
const opts extends z.ZodObject<any> | undefined = undefined,
|
|
111
|
+
const output extends z.ZodType | undefined = undefined,
|
|
112
|
+
>(
|
|
113
|
+
name: string,
|
|
114
|
+
definition: create.Options<args, env, opts, output> & { run: Function },
|
|
115
|
+
): Root<args, opts>
|
|
116
|
+
/** Creates a router CLI that registers subcommands. */
|
|
117
|
+
export function create<
|
|
118
|
+
const args extends z.ZodObject<any> | undefined = undefined,
|
|
119
|
+
const env extends z.ZodObject<any> | undefined = undefined,
|
|
120
|
+
const opts extends z.ZodObject<any> | undefined = undefined,
|
|
121
|
+
const output extends z.ZodType | undefined = undefined,
|
|
122
|
+
>(name: string, definition?: create.Options<args, env, opts, output>): Cli
|
|
123
|
+
/** Creates a leaf CLI from a single options object (e.g. package.json). */
|
|
124
|
+
export function create<
|
|
125
|
+
const args extends z.ZodObject<any> | undefined = undefined,
|
|
126
|
+
const env extends z.ZodObject<any> | undefined = undefined,
|
|
127
|
+
const opts extends z.ZodObject<any> | undefined = undefined,
|
|
128
|
+
const output extends z.ZodType | undefined = undefined,
|
|
129
|
+
>(
|
|
130
|
+
definition: create.Options<args, env, opts, output> & { name: string; run: Function },
|
|
131
|
+
): Root<args, opts>
|
|
132
|
+
/** Creates a router CLI from a single options object (e.g. package.json). */
|
|
133
|
+
export function create<
|
|
134
|
+
const args extends z.ZodObject<any> | undefined = undefined,
|
|
135
|
+
const env extends z.ZodObject<any> | undefined = undefined,
|
|
136
|
+
const opts extends z.ZodObject<any> | undefined = undefined,
|
|
137
|
+
const output extends z.ZodType | undefined = undefined,
|
|
138
|
+
>(definition: create.Options<args, env, opts, output> & { name: string }): Cli
|
|
139
|
+
export function create(
|
|
140
|
+
nameOrDefinition: string | (any & { name: string }),
|
|
141
|
+
definition?: any,
|
|
142
|
+
): Cli | Root {
|
|
143
|
+
const name = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name
|
|
144
|
+
const def = typeof nameOrDefinition === 'string' ? (definition ?? {}) : nameOrDefinition
|
|
145
|
+
if ('run' in def) {
|
|
146
|
+
const rootDef = def as CommandDefinition<any, any, any>
|
|
147
|
+
const leafCommands = new Map<string, CommandEntry>()
|
|
148
|
+
leafCommands.set(name, rootDef)
|
|
149
|
+
|
|
150
|
+
const leaf: Root = {
|
|
151
|
+
name,
|
|
152
|
+
description: def.description,
|
|
153
|
+
async serve(argv = process.argv.slice(2), options: serve.Options = {}) {
|
|
154
|
+
return serveImpl(name, leafCommands, [name, ...argv], {
|
|
155
|
+
...options,
|
|
156
|
+
description: def.description,
|
|
157
|
+
format: def.format,
|
|
158
|
+
mcp: def.mcp,
|
|
159
|
+
sync: def.sync,
|
|
160
|
+
version: def.version,
|
|
161
|
+
})
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
toRootDefinition.set(leaf, rootDef)
|
|
165
|
+
toCommands.set(leaf as unknown as Cli, leafCommands)
|
|
166
|
+
return leaf
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const commands = new Map<string, CommandEntry>()
|
|
170
|
+
|
|
171
|
+
const cli: Cli = {
|
|
172
|
+
name,
|
|
173
|
+
description: def.description,
|
|
174
|
+
|
|
175
|
+
command(nameOrCli: any, def?: any): any {
|
|
176
|
+
if (typeof nameOrCli === 'string') {
|
|
177
|
+
commands.set(nameOrCli, def)
|
|
178
|
+
return cli
|
|
179
|
+
}
|
|
180
|
+
const rootDef = toRootDefinition.get(nameOrCli)
|
|
181
|
+
if (rootDef) {
|
|
182
|
+
commands.set(nameOrCli.name, rootDef)
|
|
183
|
+
return cli
|
|
184
|
+
}
|
|
185
|
+
const sub = nameOrCli as Cli
|
|
186
|
+
const subCommands = toCommands.get(sub)!
|
|
187
|
+
commands.set(sub.name, { _group: true, description: sub.description, commands: subCommands })
|
|
188
|
+
return cli
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
async serve(argv = process.argv.slice(2), serveOptions: serve.Options = {}) {
|
|
192
|
+
return serveImpl(name, commands, argv, {
|
|
193
|
+
...serveOptions,
|
|
194
|
+
description: def.description,
|
|
195
|
+
format: def.format,
|
|
196
|
+
mcp: def.mcp,
|
|
197
|
+
sync: def.sync,
|
|
198
|
+
version: def.version,
|
|
199
|
+
})
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
toCommands.set(cli, commands)
|
|
204
|
+
return cli
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export declare namespace create {
|
|
208
|
+
/** Options for creating a CLI. Provide `run` for a leaf CLI, omit it for a router. */
|
|
209
|
+
type Options<
|
|
210
|
+
args extends z.ZodObject<any> | undefined = undefined,
|
|
211
|
+
env extends z.ZodObject<any> | undefined = undefined,
|
|
212
|
+
options extends z.ZodObject<any> | undefined = undefined,
|
|
213
|
+
output extends z.ZodType | undefined = undefined,
|
|
214
|
+
> = {
|
|
215
|
+
/** Map of option names to single-char aliases. */
|
|
216
|
+
alias?: options extends z.ZodObject<any>
|
|
217
|
+
? Partial<Record<keyof z.output<options>, string>>
|
|
218
|
+
: Record<string, string> | undefined
|
|
219
|
+
/** Zod schema for positional arguments. */
|
|
220
|
+
args?: args | undefined
|
|
221
|
+
/** A short description of what the CLI does. */
|
|
222
|
+
description?: string | undefined
|
|
223
|
+
/** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
|
|
224
|
+
env?: env | undefined
|
|
225
|
+
/** Usage examples for this command. */
|
|
226
|
+
examples?: Example<args, options>[] | undefined
|
|
227
|
+
/** Default output format. Overridden by `--format` or `--json`. */
|
|
228
|
+
format?: Formatter.Format | undefined
|
|
229
|
+
/** Zod schema for named options/flags. */
|
|
230
|
+
options?: options | undefined
|
|
231
|
+
/** Zod schema for the return value. */
|
|
232
|
+
output?: output | undefined
|
|
233
|
+
/** Alternative usage patterns shown in help output. */
|
|
234
|
+
usage?: Usage<args, options>[] | undefined
|
|
235
|
+
/** The root command handler. When provided, creates a leaf CLI with no subcommands. */
|
|
236
|
+
run?:
|
|
237
|
+
| ((context: {
|
|
238
|
+
args: InferOutput<args>
|
|
239
|
+
/** Parsed environment variables. */
|
|
240
|
+
env: InferOutput<env>
|
|
241
|
+
/** Return an error result with optional CTAs. */
|
|
242
|
+
error: (options: {
|
|
243
|
+
code: string
|
|
244
|
+
cta?: CtaBlock | undefined
|
|
245
|
+
message: string
|
|
246
|
+
retryable?: boolean | undefined
|
|
247
|
+
}) => never
|
|
248
|
+
/** Return a success result with optional metadata (e.g. CTAs). */
|
|
249
|
+
ok: (data: InferReturn<output>, meta?: { cta?: CtaBlock | undefined }) => never
|
|
250
|
+
options: InferOutput<options>
|
|
251
|
+
}) =>
|
|
252
|
+
| InferReturn<output>
|
|
253
|
+
| Promise<InferReturn<output>>
|
|
254
|
+
| AsyncGenerator<InferReturn<output>, unknown, unknown>)
|
|
255
|
+
| undefined
|
|
256
|
+
/** Options for the built-in `mcp add` command. */
|
|
257
|
+
mcp?:
|
|
258
|
+
| {
|
|
259
|
+
/** Target specific agents by default (e.g. `['claude-code', 'cursor']`). */
|
|
260
|
+
agents?: string[] | undefined
|
|
261
|
+
/** Override the command agents will run to start the MCP server. Auto-detected if omitted. */
|
|
262
|
+
command?: string | undefined
|
|
263
|
+
}
|
|
264
|
+
| undefined
|
|
265
|
+
/** Options for the built-in `skills add` command. */
|
|
266
|
+
sync?:
|
|
267
|
+
| {
|
|
268
|
+
/** Working directory for resolving `include` globs. Pass `import.meta.dirname` when running from a bin entry. Defaults to `process.cwd()`. */
|
|
269
|
+
cwd?: string | undefined
|
|
270
|
+
/** Default grouping depth for skill files. Overridden by `--depth`. Defaults to `1`. */
|
|
271
|
+
depth?: number | undefined
|
|
272
|
+
/** Glob patterns for directories containing SKILL.md files to include (e.g. `"skills/*"`, `"my-skill"`). */
|
|
273
|
+
include?: string[] | undefined
|
|
274
|
+
/** Example prompts shown after sync to help users get started. */
|
|
275
|
+
suggestions?: string[] | undefined
|
|
276
|
+
}
|
|
277
|
+
| undefined
|
|
278
|
+
/** The CLI version string. */
|
|
279
|
+
version?: string | undefined
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export declare namespace serve {
|
|
284
|
+
/** Options for `serve()`, primarily used for testing. */
|
|
285
|
+
type Options = {
|
|
286
|
+
/** Override environment variable source. Defaults to `process.env`. */
|
|
287
|
+
env?: Record<string, string | undefined> | undefined
|
|
288
|
+
/** Override exit handler. Defaults to `process.exit`. */
|
|
289
|
+
exit?: ((code: number) => void) | undefined
|
|
290
|
+
/** Override stdout writer. Defaults to `process.stdout.write`. */
|
|
291
|
+
stdout?: ((s: string) => void) | undefined
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @internal Shared serve implementation for both router and leaf CLIs. */
|
|
296
|
+
// biome-ignore lint/correctness/noUnusedVariables: _
|
|
297
|
+
async function serveImpl(
|
|
298
|
+
name: string,
|
|
299
|
+
commands: Map<string, CommandEntry>,
|
|
300
|
+
argv: string[],
|
|
301
|
+
options: serveImpl.Options = {},
|
|
302
|
+
) {
|
|
303
|
+
const stdout = options.stdout ?? ((s: string) => process.stdout.write(s))
|
|
304
|
+
const exit = options.exit ?? ((code: number) => process.exit(code))
|
|
305
|
+
|
|
306
|
+
const {
|
|
307
|
+
verbose,
|
|
308
|
+
format: formatFlag,
|
|
309
|
+
formatExplicit,
|
|
310
|
+
llms,
|
|
311
|
+
mcp: mcpFlag,
|
|
312
|
+
help,
|
|
313
|
+
version,
|
|
314
|
+
rest: filtered,
|
|
315
|
+
} = extractBuiltinFlags(argv)
|
|
316
|
+
|
|
317
|
+
// --mcp: start as MCP stdio server
|
|
318
|
+
if (mcpFlag) {
|
|
319
|
+
await Mcp.serve(name, options.version ?? '0.0.0', commands)
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Human mode: default unless --verbose or explicit --format/--json override
|
|
324
|
+
const human = !formatExplicit && !verbose
|
|
325
|
+
|
|
326
|
+
function writeln(s: string) {
|
|
327
|
+
stdout(s.endsWith('\n') ? s : `${s}\n`)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Skills staleness check (skip for built-in commands)
|
|
331
|
+
if (!llms && !help && !version) {
|
|
332
|
+
const isSkillsAdd =
|
|
333
|
+
filtered[0] === 'skills' || (filtered[0] === name && filtered[1] === 'skills')
|
|
334
|
+
const isMcpAdd = filtered[0] === 'mcp' || (filtered[0] === name && filtered[1] === 'mcp')
|
|
335
|
+
if (!isSkillsAdd && !isMcpAdd) {
|
|
336
|
+
const stored = SyncSkills.readHash(name)
|
|
337
|
+
if (stored) {
|
|
338
|
+
const groups = new Map<string, string>()
|
|
339
|
+
const entries = collectSkillCommands(commands, [], groups)
|
|
340
|
+
if (Skill.hash(entries) !== stored) {
|
|
341
|
+
const runner = detectRunner()
|
|
342
|
+
const spec = SyncMcp.detectPackageSpecifier(name)
|
|
343
|
+
process.stderr.write(
|
|
344
|
+
`⚠ Skills are out of date. Run '${runner} ${spec} skills add' to update.\n\n`,
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (llms) {
|
|
352
|
+
// Scope to a subtree if command tokens are provided
|
|
353
|
+
let scopedCommands = commands
|
|
354
|
+
const prefix: string[] = []
|
|
355
|
+
for (const token of filtered) {
|
|
356
|
+
const entry = scopedCommands.get(token)
|
|
357
|
+
if (!entry) break
|
|
358
|
+
if (isGroup(entry)) {
|
|
359
|
+
scopedCommands = entry.commands
|
|
360
|
+
prefix.push(token)
|
|
361
|
+
} else {
|
|
362
|
+
// Leaf command — scope to just this command
|
|
363
|
+
scopedCommands = new Map([[token, entry]])
|
|
364
|
+
break
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!formatExplicit || formatFlag === 'md') {
|
|
369
|
+
const groups = new Map<string, string>()
|
|
370
|
+
const cmds = collectSkillCommands(scopedCommands, prefix, groups)
|
|
371
|
+
const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name
|
|
372
|
+
writeln(Skill.generate(scopedName, cmds, groups))
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
writeln(Formatter.format(buildManifest(scopedCommands, prefix), formatFlag))
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
|
|
380
|
+
const skillsIdx =
|
|
381
|
+
filtered[0] === 'skills' ? 0 : filtered[0] === name && filtered[1] === 'skills' ? 1 : -1
|
|
382
|
+
if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills' && filtered[skillsIdx + 1] === 'add') {
|
|
383
|
+
if (help) {
|
|
384
|
+
writeln(
|
|
385
|
+
[
|
|
386
|
+
`${name} skills add — Sync skill files to your agent`,
|
|
387
|
+
'',
|
|
388
|
+
`Usage: ${name} skills add [options]`,
|
|
389
|
+
'',
|
|
390
|
+
'Options:',
|
|
391
|
+
' --depth <number> Grouping depth for skill files (default: 1)',
|
|
392
|
+
' --no-global Install to project instead of globally',
|
|
393
|
+
].join('\n'),
|
|
394
|
+
)
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
const rest = filtered.slice(skillsIdx + 2)
|
|
398
|
+
const depthArg = rest.indexOf('--depth')
|
|
399
|
+
const depth = depthArg !== -1 ? Number(rest[depthArg + 1]) : (options.sync?.depth ?? 1)
|
|
400
|
+
const global = rest.includes('--no-global') ? false : undefined
|
|
401
|
+
try {
|
|
402
|
+
if (human) stdout('Syncing...')
|
|
403
|
+
const result = await SyncSkills.sync(name, commands, {
|
|
404
|
+
cwd: options.sync?.cwd,
|
|
405
|
+
depth,
|
|
406
|
+
description: options.description,
|
|
407
|
+
global,
|
|
408
|
+
include: options.sync?.include,
|
|
409
|
+
})
|
|
410
|
+
if (human) {
|
|
411
|
+
stdout('\r\x1b[K')
|
|
412
|
+
const lines: string[] = []
|
|
413
|
+
const skillLabel = (s: (typeof result.skills)[number]) =>
|
|
414
|
+
s.external || s.name === name ? s.name : `${name}-${s.name}`
|
|
415
|
+
const maxLen = Math.max(...result.skills.map((s) => skillLabel(s).length))
|
|
416
|
+
for (const s of result.skills) {
|
|
417
|
+
const label = skillLabel(s)
|
|
418
|
+
const padding = s.description
|
|
419
|
+
? `${' '.repeat(maxLen - label.length)} ${s.description}`
|
|
420
|
+
: ''
|
|
421
|
+
lines.push(` ✓ ${label}${padding}`)
|
|
422
|
+
}
|
|
423
|
+
lines.push('')
|
|
424
|
+
lines.push(`${result.skills.length} skill${result.skills.length === 1 ? '' : 's'} synced`)
|
|
425
|
+
const suggestions = options.sync?.suggestions
|
|
426
|
+
if (suggestions && suggestions.length > 0) {
|
|
427
|
+
lines.push('')
|
|
428
|
+
lines.push(`Your agent can now use ${name}. Try asking:`)
|
|
429
|
+
for (const s of suggestions) lines.push(` "${s}"`)
|
|
430
|
+
}
|
|
431
|
+
lines.push('')
|
|
432
|
+
lines.push(`Run \`${name} --help\` to see the full command reference.`)
|
|
433
|
+
writeln(lines.join('\n'))
|
|
434
|
+
} else
|
|
435
|
+
writeln(Formatter.format({ skills: result.paths }, formatExplicit ? formatFlag : 'toon'))
|
|
436
|
+
} catch (err) {
|
|
437
|
+
writeln(
|
|
438
|
+
Formatter.format(
|
|
439
|
+
{ code: 'SYNC_SKILLS_FAILED', message: err instanceof Error ? err.message : String(err) },
|
|
440
|
+
formatExplicit ? formatFlag : 'toon',
|
|
441
|
+
),
|
|
442
|
+
)
|
|
443
|
+
exit(1)
|
|
444
|
+
}
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// mcp add: register CLI as MCP server via `npx add-mcp`
|
|
449
|
+
const mcpIdx = filtered[0] === 'mcp' ? 0 : filtered[0] === name && filtered[1] === 'mcp' ? 1 : -1
|
|
450
|
+
if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp' && filtered[mcpIdx + 1] === 'add') {
|
|
451
|
+
if (help) {
|
|
452
|
+
writeln(
|
|
453
|
+
[
|
|
454
|
+
`${name} mcp add — Register as an MCP server for your agent`,
|
|
455
|
+
'',
|
|
456
|
+
`Usage: ${name} mcp add [options]`,
|
|
457
|
+
'',
|
|
458
|
+
'Options:',
|
|
459
|
+
' -c, --command <cmd> Override the command agents will run (e.g. "pnpm my-cli --mcp")',
|
|
460
|
+
' --no-global Install to project instead of globally',
|
|
461
|
+
' --agent <agent> Target a specific agent (e.g. claude-code, cursor)',
|
|
462
|
+
].join('\n'),
|
|
463
|
+
)
|
|
464
|
+
return
|
|
465
|
+
}
|
|
466
|
+
const rest = filtered.slice(mcpIdx + 2)
|
|
467
|
+
const global = rest.includes('--no-global') ? false : true
|
|
468
|
+
|
|
469
|
+
// Parse --command / -c and --agent flags from argv
|
|
470
|
+
let command = options.mcp?.command
|
|
471
|
+
const agents: string[] = [...(options.mcp?.agents ?? [])]
|
|
472
|
+
for (let i = 0; i < rest.length; i++) {
|
|
473
|
+
if ((rest[i] === '--command' || rest[i] === '-c') && rest[i + 1]) command = rest[++i]!
|
|
474
|
+
else if (rest[i] === '--agent' && rest[i + 1]) agents.push(rest[++i]!)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
try {
|
|
478
|
+
if (human) stdout('Registering MCP server...')
|
|
479
|
+
const result = await SyncMcp.register(name, {
|
|
480
|
+
command,
|
|
481
|
+
global,
|
|
482
|
+
agents,
|
|
483
|
+
})
|
|
484
|
+
if (human) {
|
|
485
|
+
stdout('\r\x1b[K')
|
|
486
|
+
const lines: string[] = []
|
|
487
|
+
lines.push(`✓ Registered ${name} as MCP server`)
|
|
488
|
+
if (result.agents.length > 0) lines.push(` Agents: ${result.agents.join(', ')}`)
|
|
489
|
+
lines.push('')
|
|
490
|
+
lines.push(`Agents can now use ${name} tools.`)
|
|
491
|
+
const suggestions = options.sync?.suggestions
|
|
492
|
+
if (suggestions && suggestions.length > 0) {
|
|
493
|
+
lines.push('')
|
|
494
|
+
lines.push('Try asking:')
|
|
495
|
+
for (const s of suggestions) lines.push(` "${s}"`)
|
|
496
|
+
}
|
|
497
|
+
writeln(lines.join('\n'))
|
|
498
|
+
} else
|
|
499
|
+
writeln(
|
|
500
|
+
Formatter.format(
|
|
501
|
+
{ name, command: result.command, agents: result.agents },
|
|
502
|
+
formatExplicit ? formatFlag : 'toon',
|
|
503
|
+
),
|
|
504
|
+
)
|
|
505
|
+
} catch (err) {
|
|
506
|
+
writeln(
|
|
507
|
+
Formatter.format(
|
|
508
|
+
{ code: 'MCP_ADD_FAILED', message: err instanceof Error ? err.message : String(err) },
|
|
509
|
+
formatExplicit ? formatFlag : 'toon',
|
|
510
|
+
),
|
|
511
|
+
)
|
|
512
|
+
exit(1)
|
|
513
|
+
}
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// --help takes precedence over --version
|
|
518
|
+
if (version && !help && options.version) {
|
|
519
|
+
writeln(options.version)
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (filtered.length === 0) {
|
|
524
|
+
writeln(
|
|
525
|
+
Help.formatRoot(name, {
|
|
526
|
+
description: options.description,
|
|
527
|
+
version: options.version,
|
|
528
|
+
commands: collectHelpCommands(commands),
|
|
529
|
+
root: true,
|
|
530
|
+
}),
|
|
531
|
+
)
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const resolved = resolveCommand(commands, filtered)
|
|
536
|
+
|
|
537
|
+
// --help after a command → show help for that command
|
|
538
|
+
if (help) {
|
|
539
|
+
if ('help' in resolved || 'error' in resolved) {
|
|
540
|
+
// group or unknown → show root help for that path
|
|
541
|
+
const helpName = 'help' in resolved ? `${name} ${resolved.path}` : name
|
|
542
|
+
const helpDesc = 'help' in resolved ? resolved.description : options.description
|
|
543
|
+
const helpCmds = 'help' in resolved ? resolved.commands : commands
|
|
544
|
+
const isRoot = helpName === name
|
|
545
|
+
writeln(
|
|
546
|
+
Help.formatRoot(helpName, {
|
|
547
|
+
description: helpDesc,
|
|
548
|
+
version: isRoot ? options.version : undefined,
|
|
549
|
+
commands: collectHelpCommands(helpCmds),
|
|
550
|
+
root: isRoot,
|
|
551
|
+
}),
|
|
552
|
+
)
|
|
553
|
+
} else {
|
|
554
|
+
const isRootCmd = resolved.path === name
|
|
555
|
+
const commandName = isRootCmd ? name : `${name} ${resolved.path}`
|
|
556
|
+
writeln(
|
|
557
|
+
Help.formatCommand(commandName, {
|
|
558
|
+
alias: resolved.command.alias as Record<string, string> | undefined,
|
|
559
|
+
description: resolved.command.description,
|
|
560
|
+
version: isRootCmd ? options.version : undefined,
|
|
561
|
+
args: resolved.command.args,
|
|
562
|
+
env: resolved.command.env,
|
|
563
|
+
hint: resolved.command.hint,
|
|
564
|
+
options: resolved.command.options,
|
|
565
|
+
examples: formatExamples(resolved.command.examples),
|
|
566
|
+
usage: resolved.command.usage,
|
|
567
|
+
root: isRootCmd,
|
|
568
|
+
}),
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if ('help' in resolved) {
|
|
575
|
+
writeln(
|
|
576
|
+
Help.formatRoot(`${name} ${resolved.path}`, {
|
|
577
|
+
description: resolved.description,
|
|
578
|
+
commands: collectHelpCommands(resolved.commands),
|
|
579
|
+
}),
|
|
580
|
+
)
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const start = performance.now()
|
|
585
|
+
|
|
586
|
+
// Resolve effective format: explicit --format/--json → command default → CLI default → toon
|
|
587
|
+
const resolvedFormat = 'command' in resolved && resolved.command.format
|
|
588
|
+
const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon'
|
|
589
|
+
|
|
590
|
+
function write(output: Output) {
|
|
591
|
+
const cta = output.meta.cta
|
|
592
|
+
if (human) {
|
|
593
|
+
if (output.ok) writeln(Formatter.format(output.data, format))
|
|
594
|
+
else writeln(formatHumanError(output.error))
|
|
595
|
+
if (cta) writeln(formatHumanCta(cta))
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
if (verbose) return writeln(Formatter.format(output, format))
|
|
599
|
+
const base = output.ok ? output.data : output.error
|
|
600
|
+
if (!cta) return writeln(Formatter.format(base, format))
|
|
601
|
+
const payload =
|
|
602
|
+
typeof base === 'object' && base !== null ? { ...base, cta } : { data: base, cta }
|
|
603
|
+
writeln(Formatter.format(payload, format))
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if ('error' in resolved) {
|
|
607
|
+
const helpCmd = resolved.path ? `${name} ${resolved.path} --help` : `${name} --help`
|
|
608
|
+
const message = `'${resolved.error}' is not a command. See '${helpCmd}' for a list of available commands.`
|
|
609
|
+
if (human) {
|
|
610
|
+
writeln(formatHumanError({ code: 'COMMAND_NOT_FOUND', message }))
|
|
611
|
+
exit(1)
|
|
612
|
+
return
|
|
613
|
+
}
|
|
614
|
+
write({
|
|
615
|
+
ok: false,
|
|
616
|
+
error: { code: 'COMMAND_NOT_FOUND', message },
|
|
617
|
+
meta: {
|
|
618
|
+
command: resolved.error,
|
|
619
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
620
|
+
},
|
|
621
|
+
})
|
|
622
|
+
exit(1)
|
|
623
|
+
return
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const { command, path, rest } = resolved
|
|
627
|
+
|
|
628
|
+
try {
|
|
629
|
+
const { args, options: parsedOptions } = Parser.parse(rest, {
|
|
630
|
+
alias: command.alias as Record<string, string> | undefined,
|
|
631
|
+
args: command.args,
|
|
632
|
+
options: command.options,
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
const envSource = options.env ?? process.env
|
|
636
|
+
const env = command.env ? Parser.parseEnv(command.env, envSource) : {}
|
|
637
|
+
|
|
638
|
+
const okFn = (data: unknown, meta: { cta?: CtaBlock | undefined } = {}): never => {
|
|
639
|
+
return { [sentinel]: 'ok', data, cta: meta.cta } as never
|
|
640
|
+
}
|
|
641
|
+
const errorFn = (opts: {
|
|
642
|
+
code: string
|
|
643
|
+
message: string
|
|
644
|
+
retryable?: boolean | undefined
|
|
645
|
+
cta?: CtaBlock | undefined
|
|
646
|
+
}): never => {
|
|
647
|
+
return { [sentinel]: 'error', ...opts } as never
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const result = command.run({
|
|
651
|
+
args,
|
|
652
|
+
env,
|
|
653
|
+
options: parsedOptions,
|
|
654
|
+
ok: okFn,
|
|
655
|
+
error: errorFn,
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
// Streaming path — async generator
|
|
659
|
+
if (isAsyncGenerator(result)) {
|
|
660
|
+
await handleStreaming(result, {
|
|
661
|
+
name,
|
|
662
|
+
path,
|
|
663
|
+
start,
|
|
664
|
+
format,
|
|
665
|
+
formatExplicit,
|
|
666
|
+
human,
|
|
667
|
+
verbose,
|
|
668
|
+
write,
|
|
669
|
+
writeln,
|
|
670
|
+
exit,
|
|
671
|
+
})
|
|
672
|
+
return
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const awaited = await result
|
|
676
|
+
|
|
677
|
+
if (isSentinel(awaited)) {
|
|
678
|
+
const cta = formatCtaBlock(name, awaited.cta)
|
|
679
|
+
if (awaited[sentinel] === 'ok') {
|
|
680
|
+
write({
|
|
681
|
+
ok: true,
|
|
682
|
+
data: awaited.data,
|
|
683
|
+
meta: {
|
|
684
|
+
command: path,
|
|
685
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
686
|
+
...(cta ? { cta } : undefined),
|
|
687
|
+
},
|
|
688
|
+
})
|
|
689
|
+
} else {
|
|
690
|
+
write({
|
|
691
|
+
ok: false,
|
|
692
|
+
error: {
|
|
693
|
+
code: awaited.code,
|
|
694
|
+
message: awaited.message,
|
|
695
|
+
...(awaited.retryable !== undefined ? { retryable: awaited.retryable } : undefined),
|
|
696
|
+
},
|
|
697
|
+
meta: {
|
|
698
|
+
command: path,
|
|
699
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
700
|
+
...(cta ? { cta } : undefined),
|
|
701
|
+
},
|
|
702
|
+
})
|
|
703
|
+
exit(1)
|
|
704
|
+
}
|
|
705
|
+
} else {
|
|
706
|
+
write({
|
|
707
|
+
ok: true,
|
|
708
|
+
data: awaited,
|
|
709
|
+
meta: {
|
|
710
|
+
command: path,
|
|
711
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
712
|
+
},
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
} catch (error) {
|
|
716
|
+
const errorOutput: Output = {
|
|
717
|
+
ok: false,
|
|
718
|
+
error: {
|
|
719
|
+
code:
|
|
720
|
+
error instanceof IncurError
|
|
721
|
+
? error.code
|
|
722
|
+
: error instanceof ValidationError
|
|
723
|
+
? 'VALIDATION_ERROR'
|
|
724
|
+
: 'UNKNOWN',
|
|
725
|
+
message: error instanceof Error ? error.message : String(error),
|
|
726
|
+
...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
|
|
727
|
+
...(error instanceof ValidationError ? { fieldErrors: error.fieldErrors } : undefined),
|
|
728
|
+
},
|
|
729
|
+
meta: {
|
|
730
|
+
command: path,
|
|
731
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
732
|
+
},
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (human && error instanceof ValidationError) {
|
|
736
|
+
writeln(formatHumanValidationError(name, path, command, error))
|
|
737
|
+
exit(1)
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
write(errorOutput)
|
|
742
|
+
exit(1)
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** @internal Formats a validation error for TTY with usage hint. */
|
|
747
|
+
function formatHumanValidationError(
|
|
748
|
+
cli: string,
|
|
749
|
+
path: string,
|
|
750
|
+
command: CommandDefinition<any, any, any>,
|
|
751
|
+
error: ValidationError,
|
|
752
|
+
): string {
|
|
753
|
+
const lines: string[] = []
|
|
754
|
+
for (const fe of error.fieldErrors) lines.push(`Error: missing required argument <${fe.path}>`)
|
|
755
|
+
lines.push('See below for usage.')
|
|
756
|
+
lines.push('')
|
|
757
|
+
lines.push(
|
|
758
|
+
Help.formatCommand(path === cli ? cli : `${cli} ${path}`, {
|
|
759
|
+
alias: command.alias as Record<string, string> | undefined,
|
|
760
|
+
description: command.description,
|
|
761
|
+
args: command.args,
|
|
762
|
+
env: command.env,
|
|
763
|
+
hint: command.hint,
|
|
764
|
+
options: command.options,
|
|
765
|
+
examples: formatExamples(command.examples),
|
|
766
|
+
usage: command.usage,
|
|
767
|
+
}),
|
|
768
|
+
)
|
|
769
|
+
return lines.join('\n')
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** @internal Resolves a command from the tree by walking tokens until a leaf is found. */
|
|
773
|
+
function resolveCommand(
|
|
774
|
+
commands: Map<string, CommandEntry>,
|
|
775
|
+
tokens: string[],
|
|
776
|
+
):
|
|
777
|
+
| { command: CommandDefinition<any, any, any>; path: string; rest: string[] }
|
|
778
|
+
| {
|
|
779
|
+
help: true
|
|
780
|
+
path: string
|
|
781
|
+
description?: string | undefined
|
|
782
|
+
commands: Map<string, CommandEntry>
|
|
783
|
+
}
|
|
784
|
+
| { error: string; path: string } {
|
|
785
|
+
const [first, ...rest] = tokens
|
|
786
|
+
|
|
787
|
+
if (!first || !commands.has(first)) return { error: first ?? '(none)', path: '' }
|
|
788
|
+
|
|
789
|
+
let entry = commands.get(first)!
|
|
790
|
+
const path = [first]
|
|
791
|
+
let remaining = rest
|
|
792
|
+
|
|
793
|
+
while (isGroup(entry)) {
|
|
794
|
+
const next = remaining[0]
|
|
795
|
+
if (!next)
|
|
796
|
+
return {
|
|
797
|
+
help: true,
|
|
798
|
+
path: path.join(' '),
|
|
799
|
+
description: entry.description,
|
|
800
|
+
commands: entry.commands,
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const child = entry.commands.get(next)
|
|
804
|
+
if (!child) {
|
|
805
|
+
return { error: next, path: path.join(' ') }
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
path.push(next)
|
|
809
|
+
remaining = remaining.slice(1)
|
|
810
|
+
entry = child
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return { command: entry, path: path.join(' '), rest: remaining }
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** @internal Options for serveImpl, extending public serve.Options with internal metadata. */
|
|
817
|
+
declare namespace serveImpl {
|
|
818
|
+
type Options = serve.Options & {
|
|
819
|
+
description?: string | undefined
|
|
820
|
+
/** CLI-level default output format. */
|
|
821
|
+
format?: Formatter.Format | undefined
|
|
822
|
+
mcp?:
|
|
823
|
+
| {
|
|
824
|
+
agents?: string[] | undefined
|
|
825
|
+
command?: string | undefined
|
|
826
|
+
}
|
|
827
|
+
| undefined
|
|
828
|
+
sync?:
|
|
829
|
+
| {
|
|
830
|
+
cwd?: string | undefined
|
|
831
|
+
depth?: number | undefined
|
|
832
|
+
include?: string[] | undefined
|
|
833
|
+
suggestions?: string[] | undefined
|
|
834
|
+
}
|
|
835
|
+
| undefined
|
|
836
|
+
version?: string | undefined
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** @internal Extracts built-in flags (--verbose, --format, --json, --llms, --help, --version) from argv. */
|
|
841
|
+
function extractBuiltinFlags(argv: string[]) {
|
|
842
|
+
let verbose = false
|
|
843
|
+
let llms = false
|
|
844
|
+
let mcp = false
|
|
845
|
+
let help = false
|
|
846
|
+
let version = false
|
|
847
|
+
let format: Formatter.Format = 'toon'
|
|
848
|
+
let formatExplicit = false
|
|
849
|
+
const rest: string[] = []
|
|
850
|
+
|
|
851
|
+
for (let i = 0; i < argv.length; i++) {
|
|
852
|
+
const token = argv[i]!
|
|
853
|
+
if (token === '--verbose') verbose = true
|
|
854
|
+
else if (token === '--llms') llms = true
|
|
855
|
+
else if (token === '--mcp') mcp = true
|
|
856
|
+
else if (token === '--help' || token === '-h') help = true
|
|
857
|
+
else if (token === '--version') version = true
|
|
858
|
+
else if (token === '--json') {
|
|
859
|
+
format = 'json'
|
|
860
|
+
formatExplicit = true
|
|
861
|
+
} else if (token === '--format' && argv[i + 1]) {
|
|
862
|
+
format = argv[i + 1] as Formatter.Format
|
|
863
|
+
formatExplicit = true
|
|
864
|
+
i++
|
|
865
|
+
} else rest.push(token)
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return { verbose, format, formatExplicit, llms, mcp, help, version, rest }
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** @internal Collects immediate child commands/groups for help output. */
|
|
872
|
+
function collectHelpCommands(
|
|
873
|
+
commands: Map<string, CommandEntry>,
|
|
874
|
+
): { name: string; description?: string | undefined }[] {
|
|
875
|
+
const result: { name: string; description?: string | undefined }[] = []
|
|
876
|
+
for (const [name, entry] of commands) {
|
|
877
|
+
if (isGroup(entry)) result.push({ name, description: entry.description })
|
|
878
|
+
else result.push({ name, description: entry.description })
|
|
879
|
+
}
|
|
880
|
+
return result.sort((a, b) => a.name.localeCompare(b.name))
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Shape of the commands map accumulated through `.command()` chains. */
|
|
884
|
+
export type CommandsMap = Record<
|
|
885
|
+
string,
|
|
886
|
+
{ args: Record<string, unknown>; options: Record<string, unknown> }
|
|
887
|
+
>
|
|
888
|
+
|
|
889
|
+
/** @internal Entry stored in a command map — either a leaf definition or a group. */
|
|
890
|
+
type CommandEntry = CommandDefinition<any, any, any> | InternalGroup
|
|
891
|
+
|
|
892
|
+
/** @internal A command group's internal storage. */
|
|
893
|
+
type InternalGroup = {
|
|
894
|
+
_group: true
|
|
895
|
+
description?: string | undefined
|
|
896
|
+
commands: Map<string, CommandEntry>
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** @internal Type guard for command groups. */
|
|
900
|
+
function isGroup(entry: CommandEntry): entry is InternalGroup {
|
|
901
|
+
return '_group' in entry
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** @internal Maps CLI instances to their command maps. */
|
|
905
|
+
export const toCommands = new WeakMap<Cli, Map<string, CommandEntry>>()
|
|
906
|
+
|
|
907
|
+
/** @internal Maps root CLI instances to their command definitions. */
|
|
908
|
+
const toRootDefinition = new WeakMap<Root, CommandDefinition<any, any, any>>()
|
|
909
|
+
|
|
910
|
+
/** @internal Sentinel symbol for `ok()` and `error()` return values. */
|
|
911
|
+
const sentinel = Symbol.for('incur.sentinel')
|
|
912
|
+
|
|
913
|
+
/** @internal A tagged ok result returned by the `ok` context helper. */
|
|
914
|
+
type OkResult = {
|
|
915
|
+
[sentinel]: 'ok'
|
|
916
|
+
data: unknown
|
|
917
|
+
cta?: CtaBlock | undefined
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** @internal A tagged error result returned by the `error` context helper. */
|
|
921
|
+
type ErrorResult = {
|
|
922
|
+
[sentinel]: 'error'
|
|
923
|
+
code: string
|
|
924
|
+
message: string
|
|
925
|
+
retryable?: boolean | undefined
|
|
926
|
+
cta?: CtaBlock | undefined
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/** @internal A CTA block with a description and list of suggested commands. */
|
|
930
|
+
type CtaBlock<commands extends CommandsMap = Commands> = {
|
|
931
|
+
/** Commands to suggest. */
|
|
932
|
+
commands: Cta<commands>[]
|
|
933
|
+
/** Human-readable label. Defaults to `"Suggested commands:"`. */
|
|
934
|
+
description?: string | undefined
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/** @internal Formats an error for human-readable TTY output. */
|
|
938
|
+
function formatHumanError(error: {
|
|
939
|
+
code: string
|
|
940
|
+
message: string
|
|
941
|
+
fieldErrors?: FieldError[] | undefined
|
|
942
|
+
}): string {
|
|
943
|
+
const prefix =
|
|
944
|
+
error.code === 'UNKNOWN' || error.code === 'COMMAND_NOT_FOUND'
|
|
945
|
+
? 'Error'
|
|
946
|
+
: `Error (${error.code})`
|
|
947
|
+
let out = `${prefix}: ${error.message}`
|
|
948
|
+
if (error.fieldErrors) for (const fe of error.fieldErrors) out += `\n ${fe.path}: ${fe.message}`
|
|
949
|
+
return out
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/** @internal Formats a CTA block for human-readable TTY output. */
|
|
953
|
+
function formatHumanCta(cta: FormattedCtaBlock): string {
|
|
954
|
+
const lines: string[] = ['', cta.description]
|
|
955
|
+
for (const c of cta.commands) {
|
|
956
|
+
const desc = c.description ? ` ${c.description}` : ''
|
|
957
|
+
lines.push(` ${c.command}${desc}`)
|
|
958
|
+
}
|
|
959
|
+
return lines.join('\n')
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/** @internal Type guard for sentinel results. */
|
|
963
|
+
function isSentinel(value: unknown): value is OkResult | ErrorResult {
|
|
964
|
+
return typeof value === 'object' && value !== null && sentinel in value
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/** @internal Type guard for async generators returned by streaming `run` handlers. */
|
|
968
|
+
function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unknown, unknown> {
|
|
969
|
+
return (
|
|
970
|
+
typeof value === 'object' &&
|
|
971
|
+
value !== null &&
|
|
972
|
+
Symbol.asyncIterator in value &&
|
|
973
|
+
typeof (value as any).next === 'function'
|
|
974
|
+
)
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** @internal Handles streaming output from an async generator `run` handler. */
|
|
978
|
+
async function handleStreaming(
|
|
979
|
+
generator: AsyncGenerator<unknown, unknown, unknown>,
|
|
980
|
+
ctx: {
|
|
981
|
+
name: string
|
|
982
|
+
path: string
|
|
983
|
+
start: number
|
|
984
|
+
format: Formatter.Format
|
|
985
|
+
formatExplicit: boolean
|
|
986
|
+
human: boolean
|
|
987
|
+
verbose: boolean
|
|
988
|
+
write: (output: Output) => void
|
|
989
|
+
writeln: (s: string) => void
|
|
990
|
+
exit: (code: number) => void
|
|
991
|
+
},
|
|
992
|
+
) {
|
|
993
|
+
// Incremental: human, no explicit format (default toon), or explicit jsonl
|
|
994
|
+
// Buffered: explicit json/yaml/toon/md
|
|
995
|
+
const useJsonl = !ctx.human && ctx.formatExplicit && ctx.format === 'jsonl'
|
|
996
|
+
const incremental = ctx.human || useJsonl || !ctx.formatExplicit
|
|
997
|
+
|
|
998
|
+
if (incremental) {
|
|
999
|
+
// Incremental output: write each chunk as it arrives
|
|
1000
|
+
try {
|
|
1001
|
+
let returnValue: unknown
|
|
1002
|
+
while (true) {
|
|
1003
|
+
const { value, done } = await generator.next()
|
|
1004
|
+
if (done) {
|
|
1005
|
+
returnValue = value
|
|
1006
|
+
break
|
|
1007
|
+
}
|
|
1008
|
+
if (isSentinel(value)) {
|
|
1009
|
+
const tagged = value as any
|
|
1010
|
+
if (tagged[sentinel] === 'error') {
|
|
1011
|
+
if (useJsonl)
|
|
1012
|
+
ctx.writeln(
|
|
1013
|
+
JSON.stringify({
|
|
1014
|
+
type: 'error',
|
|
1015
|
+
ok: false,
|
|
1016
|
+
error: {
|
|
1017
|
+
code: tagged.code,
|
|
1018
|
+
message: tagged.message,
|
|
1019
|
+
...(tagged.retryable !== undefined
|
|
1020
|
+
? { retryable: tagged.retryable }
|
|
1021
|
+
: undefined),
|
|
1022
|
+
},
|
|
1023
|
+
}),
|
|
1024
|
+
)
|
|
1025
|
+
else ctx.writeln(formatHumanError({ code: tagged.code, message: tagged.message }))
|
|
1026
|
+
ctx.exit(1)
|
|
1027
|
+
return
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
if (useJsonl) ctx.writeln(JSON.stringify({ type: 'chunk', data: value }))
|
|
1031
|
+
else ctx.writeln(Formatter.format(value, 'toon'))
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// Handle return value — error() or ok() sentinel
|
|
1035
|
+
if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
|
|
1036
|
+
const err = returnValue as ErrorResult
|
|
1037
|
+
if (useJsonl)
|
|
1038
|
+
ctx.writeln(
|
|
1039
|
+
JSON.stringify({
|
|
1040
|
+
type: 'error',
|
|
1041
|
+
ok: false,
|
|
1042
|
+
error: {
|
|
1043
|
+
code: err.code,
|
|
1044
|
+
message: err.message,
|
|
1045
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
1046
|
+
},
|
|
1047
|
+
}),
|
|
1048
|
+
)
|
|
1049
|
+
else ctx.writeln(formatHumanError({ code: err.code, message: err.message }))
|
|
1050
|
+
ctx.exit(1)
|
|
1051
|
+
return
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const cta =
|
|
1055
|
+
isSentinel(returnValue) && returnValue[sentinel] === 'ok'
|
|
1056
|
+
? formatCtaBlock(ctx.name, (returnValue as OkResult).cta)
|
|
1057
|
+
: undefined
|
|
1058
|
+
|
|
1059
|
+
if (useJsonl)
|
|
1060
|
+
ctx.writeln(
|
|
1061
|
+
JSON.stringify({
|
|
1062
|
+
type: 'done',
|
|
1063
|
+
ok: true,
|
|
1064
|
+
meta: {
|
|
1065
|
+
command: ctx.path,
|
|
1066
|
+
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
1067
|
+
...(cta ? { cta } : undefined),
|
|
1068
|
+
},
|
|
1069
|
+
}),
|
|
1070
|
+
)
|
|
1071
|
+
else if (cta) ctx.writeln(formatHumanCta(cta))
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
if (useJsonl)
|
|
1074
|
+
ctx.writeln(
|
|
1075
|
+
JSON.stringify({
|
|
1076
|
+
type: 'error',
|
|
1077
|
+
ok: false,
|
|
1078
|
+
error: {
|
|
1079
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
1080
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1081
|
+
},
|
|
1082
|
+
}),
|
|
1083
|
+
)
|
|
1084
|
+
else
|
|
1085
|
+
ctx.writeln(
|
|
1086
|
+
formatHumanError({
|
|
1087
|
+
code: 'UNKNOWN',
|
|
1088
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1089
|
+
}),
|
|
1090
|
+
)
|
|
1091
|
+
ctx.exit(1)
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
// Buffered output: collect all chunks, write as single value
|
|
1095
|
+
const chunks: unknown[] = []
|
|
1096
|
+
try {
|
|
1097
|
+
let returnValue: unknown
|
|
1098
|
+
while (true) {
|
|
1099
|
+
const { value, done } = await generator.next()
|
|
1100
|
+
if (done) {
|
|
1101
|
+
returnValue = value
|
|
1102
|
+
break
|
|
1103
|
+
}
|
|
1104
|
+
if (isSentinel(value)) {
|
|
1105
|
+
const tagged = value as any
|
|
1106
|
+
if (tagged[sentinel] === 'error') {
|
|
1107
|
+
ctx.write({
|
|
1108
|
+
ok: false,
|
|
1109
|
+
error: {
|
|
1110
|
+
code: tagged.code,
|
|
1111
|
+
message: tagged.message,
|
|
1112
|
+
...(tagged.retryable !== undefined ? { retryable: tagged.retryable } : undefined),
|
|
1113
|
+
},
|
|
1114
|
+
meta: {
|
|
1115
|
+
command: ctx.path,
|
|
1116
|
+
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
1117
|
+
},
|
|
1118
|
+
})
|
|
1119
|
+
ctx.exit(1)
|
|
1120
|
+
return
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
chunks.push(value)
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
|
|
1127
|
+
const err = returnValue as ErrorResult
|
|
1128
|
+
ctx.write({
|
|
1129
|
+
ok: false,
|
|
1130
|
+
error: {
|
|
1131
|
+
code: err.code,
|
|
1132
|
+
message: err.message,
|
|
1133
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
1134
|
+
},
|
|
1135
|
+
meta: {
|
|
1136
|
+
command: ctx.path,
|
|
1137
|
+
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
1138
|
+
},
|
|
1139
|
+
})
|
|
1140
|
+
ctx.exit(1)
|
|
1141
|
+
return
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const cta =
|
|
1145
|
+
isSentinel(returnValue) && returnValue[sentinel] === 'ok'
|
|
1146
|
+
? formatCtaBlock(ctx.name, (returnValue as OkResult).cta)
|
|
1147
|
+
: undefined
|
|
1148
|
+
|
|
1149
|
+
ctx.write({
|
|
1150
|
+
ok: true,
|
|
1151
|
+
data: chunks,
|
|
1152
|
+
meta: {
|
|
1153
|
+
command: ctx.path,
|
|
1154
|
+
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
1155
|
+
...(cta ? { cta } : undefined),
|
|
1156
|
+
},
|
|
1157
|
+
})
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
ctx.write({
|
|
1160
|
+
ok: false,
|
|
1161
|
+
error: {
|
|
1162
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
1163
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1164
|
+
},
|
|
1165
|
+
meta: {
|
|
1166
|
+
command: ctx.path,
|
|
1167
|
+
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
1168
|
+
},
|
|
1169
|
+
})
|
|
1170
|
+
ctx.exit(1)
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/** @internal Formats a CTA block into the output envelope shape. */
|
|
1176
|
+
function formatCtaBlock(name: string, block: CtaBlock | undefined): FormattedCtaBlock | undefined {
|
|
1177
|
+
if (!block || block.commands.length === 0) return undefined
|
|
1178
|
+
return {
|
|
1179
|
+
description: block.description ?? 'Suggested commands:',
|
|
1180
|
+
commands: block.commands.map((c) => formatCta(name, c)),
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/** @internal Formats a CTA by prefixing the CLI name. Handles string and object forms. */
|
|
1185
|
+
function formatCta(name: string, cta: Cta): FormattedCta {
|
|
1186
|
+
if (typeof cta === 'string') return { command: `${name} ${cta}` }
|
|
1187
|
+
const prefix = cta.command === name || cta.command.startsWith(`${name} `) ? '' : `${name} `
|
|
1188
|
+
let cmd = `${prefix}${cta.command}`
|
|
1189
|
+
if (cta.args)
|
|
1190
|
+
for (const [key, value] of Object.entries(cta.args))
|
|
1191
|
+
cmd += value === true ? ` <${key}>` : ` ${value}`
|
|
1192
|
+
if (cta.options)
|
|
1193
|
+
for (const [key, value] of Object.entries(cta.options))
|
|
1194
|
+
cmd += value === true ? ` --${key} <${key}>` : ` --${key} ${value}`
|
|
1195
|
+
return { command: cmd, ...(cta.description ? { description: cta.description } : undefined) }
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/** @internal Builds the `--llms` manifest from the command tree. */
|
|
1199
|
+
function buildManifest(commands: Map<string, CommandEntry>, prefix: string[] = []) {
|
|
1200
|
+
return {
|
|
1201
|
+
version: 'incur.v1',
|
|
1202
|
+
commands: collectCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/** @internal Recursively collects leaf commands with their full paths. */
|
|
1207
|
+
function collectCommands(
|
|
1208
|
+
commands: Map<string, CommandEntry>,
|
|
1209
|
+
prefix: string[],
|
|
1210
|
+
): {
|
|
1211
|
+
name: string
|
|
1212
|
+
description?: string | undefined
|
|
1213
|
+
schema?: Record<string, unknown> | undefined
|
|
1214
|
+
examples?: { command: string; description?: string | undefined }[] | undefined
|
|
1215
|
+
}[] {
|
|
1216
|
+
const result: ReturnType<typeof collectCommands> = []
|
|
1217
|
+
for (const [name, entry] of commands) {
|
|
1218
|
+
const path = [...prefix, name]
|
|
1219
|
+
if (isGroup(entry)) {
|
|
1220
|
+
result.push(...collectCommands(entry.commands, path))
|
|
1221
|
+
} else {
|
|
1222
|
+
const cmd: (typeof result)[number] = { name: path.join(' ') }
|
|
1223
|
+
if (entry.description) cmd.description = entry.description
|
|
1224
|
+
|
|
1225
|
+
const inputSchema = buildInputSchema(entry.args, entry.env, entry.options)
|
|
1226
|
+
const outputSchema = entry.output ? Schema.toJsonSchema(entry.output) : undefined
|
|
1227
|
+
if (inputSchema || outputSchema) {
|
|
1228
|
+
cmd.schema = {}
|
|
1229
|
+
if (inputSchema?.args) cmd.schema.args = inputSchema.args
|
|
1230
|
+
if (inputSchema?.env) cmd.schema.env = inputSchema.env
|
|
1231
|
+
if (inputSchema?.options) cmd.schema.options = inputSchema.options
|
|
1232
|
+
if (outputSchema) cmd.schema.output = outputSchema
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const examples = formatExamples(entry.examples)
|
|
1236
|
+
if (examples) {
|
|
1237
|
+
const cmdName = path.join(' ')
|
|
1238
|
+
cmd.examples = examples.map((e) => ({
|
|
1239
|
+
...e,
|
|
1240
|
+
command: e.command ? `${cmdName} ${e.command}` : cmdName,
|
|
1241
|
+
}))
|
|
1242
|
+
}
|
|
1243
|
+
result.push(cmd)
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return result
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/** @internal Recursively collects leaf commands as `Skill.CommandInfo` for `--llms --format md`. */
|
|
1250
|
+
function collectSkillCommands(
|
|
1251
|
+
commands: Map<string, CommandEntry>,
|
|
1252
|
+
prefix: string[],
|
|
1253
|
+
groups: Map<string, string>,
|
|
1254
|
+
): Skill.CommandInfo[] {
|
|
1255
|
+
const result: Skill.CommandInfo[] = []
|
|
1256
|
+
for (const [name, entry] of commands) {
|
|
1257
|
+
const path = [...prefix, name]
|
|
1258
|
+
if (isGroup(entry)) {
|
|
1259
|
+
if (entry.description) groups.set(path.join(' '), entry.description)
|
|
1260
|
+
result.push(...collectSkillCommands(entry.commands, path, groups))
|
|
1261
|
+
} else {
|
|
1262
|
+
const cmd: Skill.CommandInfo = { name: path.join(' ') }
|
|
1263
|
+
if (entry.description) cmd.description = entry.description
|
|
1264
|
+
if (entry.args) cmd.args = entry.args
|
|
1265
|
+
if (entry.env) cmd.env = entry.env
|
|
1266
|
+
if (entry.hint) cmd.hint = entry.hint
|
|
1267
|
+
if (entry.options) cmd.options = entry.options
|
|
1268
|
+
if (entry.output) cmd.output = entry.output
|
|
1269
|
+
const examples = formatExamples(entry.examples)
|
|
1270
|
+
if (examples) {
|
|
1271
|
+
const cmdName = path.join(' ')
|
|
1272
|
+
cmd.examples = examples.map((e) => ({
|
|
1273
|
+
...e,
|
|
1274
|
+
command: e.command ? `${cmdName} ${e.command}` : cmdName,
|
|
1275
|
+
}))
|
|
1276
|
+
}
|
|
1277
|
+
result.push(cmd)
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return result.sort((a, b) => a.name.localeCompare(b.name))
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/** @internal Formats examples into `{ command, description }` objects. `command` is the args/options suffix only. */
|
|
1284
|
+
export function formatExamples(
|
|
1285
|
+
examples: Example<any, any>[] | undefined,
|
|
1286
|
+
): { command: string; description?: string }[] | undefined {
|
|
1287
|
+
if (!examples || examples.length === 0) return undefined
|
|
1288
|
+
return examples.map((ex) => {
|
|
1289
|
+
const parts: string[] = []
|
|
1290
|
+
if (ex.args) for (const value of Object.values(ex.args)) parts.push(String(value))
|
|
1291
|
+
if (ex.options)
|
|
1292
|
+
for (const [key, value] of Object.entries(ex.options)) parts.push(`--${key} ${value}`)
|
|
1293
|
+
const result: { command: string; description?: string } = { command: parts.join(' ') }
|
|
1294
|
+
if (ex.description) result.description = ex.description
|
|
1295
|
+
return result
|
|
1296
|
+
})
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
/** @internal Builds separate args, env, and options JSON Schemas. */
|
|
1300
|
+
function buildInputSchema(
|
|
1301
|
+
args: z.ZodObject<any> | undefined,
|
|
1302
|
+
env: z.ZodObject<any> | undefined,
|
|
1303
|
+
options: z.ZodObject<any> | undefined,
|
|
1304
|
+
):
|
|
1305
|
+
| {
|
|
1306
|
+
args?: Record<string, unknown> | undefined
|
|
1307
|
+
env?: Record<string, unknown> | undefined
|
|
1308
|
+
options?: Record<string, unknown> | undefined
|
|
1309
|
+
}
|
|
1310
|
+
| undefined {
|
|
1311
|
+
if (!args && !env && !options) return undefined
|
|
1312
|
+
const result: {
|
|
1313
|
+
args?: Record<string, unknown> | undefined
|
|
1314
|
+
env?: Record<string, unknown> | undefined
|
|
1315
|
+
options?: Record<string, unknown> | undefined
|
|
1316
|
+
} = {}
|
|
1317
|
+
if (args) result.args = Schema.toJsonSchema(args)
|
|
1318
|
+
if (env) result.env = Schema.toJsonSchema(env)
|
|
1319
|
+
if (options) result.options = Schema.toJsonSchema(options)
|
|
1320
|
+
return result
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/** @internal A usage example for a command, typed against its args and options schemas. */
|
|
1324
|
+
type Example<
|
|
1325
|
+
args extends z.ZodObject<any> | undefined,
|
|
1326
|
+
options extends z.ZodObject<any> | undefined,
|
|
1327
|
+
> = {
|
|
1328
|
+
/** Positional arguments for this example. */
|
|
1329
|
+
args?: args extends z.ZodObject<any> ? Partial<z.output<args>> | undefined : undefined
|
|
1330
|
+
/** A short description of what this example demonstrates. */
|
|
1331
|
+
description?: string | undefined
|
|
1332
|
+
/** Named options for this example. */
|
|
1333
|
+
options?: options extends z.ZodObject<any> ? Partial<z.output<options>> | undefined : undefined
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/** @internal A usage pattern shown in help output. */
|
|
1337
|
+
type Usage<
|
|
1338
|
+
args extends z.ZodObject<any> | undefined,
|
|
1339
|
+
options extends z.ZodObject<any> | undefined,
|
|
1340
|
+
> = {
|
|
1341
|
+
/** Positional arguments to include. Use `true` to show as `<name>`. */
|
|
1342
|
+
args?: args extends z.ZodObject<any>
|
|
1343
|
+
? Partial<Record<keyof z.output<args>, true>> | undefined
|
|
1344
|
+
: undefined
|
|
1345
|
+
/** Named options to include. Use `true` to show as `--name <name>`. */
|
|
1346
|
+
options?: options extends z.ZodObject<any>
|
|
1347
|
+
? Partial<Record<keyof z.output<options>, true>> | undefined
|
|
1348
|
+
: undefined
|
|
1349
|
+
/** Text prepended before the command (e.g. `"cat file.txt |"`). */
|
|
1350
|
+
prefix?: string | undefined
|
|
1351
|
+
/** Text appended after the command (e.g. `"| head"`). */
|
|
1352
|
+
suffix?: string | undefined
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/** @internal Inferred output type of a Zod schema, or `{}` when the schema is not provided. */
|
|
1356
|
+
type InferOutput<schema extends z.ZodObject<any> | undefined> =
|
|
1357
|
+
schema extends z.ZodObject<any> ? z.output<schema> : {}
|
|
1358
|
+
|
|
1359
|
+
/** @internal Inferred return type for a command handler. */
|
|
1360
|
+
type InferReturn<output extends z.ZodType | undefined> = output extends z.ZodType
|
|
1361
|
+
? z.output<output>
|
|
1362
|
+
: unknown
|
|
1363
|
+
|
|
1364
|
+
/** @internal The output envelope written to stdout. */
|
|
1365
|
+
type Output = OneOf<
|
|
1366
|
+
| {
|
|
1367
|
+
/** The command's return data. */
|
|
1368
|
+
data: unknown
|
|
1369
|
+
/** Request metadata. */
|
|
1370
|
+
meta: Output.Meta
|
|
1371
|
+
/** Whether the command succeeded. */
|
|
1372
|
+
ok: true
|
|
1373
|
+
}
|
|
1374
|
+
| {
|
|
1375
|
+
/** Error details. */
|
|
1376
|
+
error: {
|
|
1377
|
+
/** Machine-readable error code. */
|
|
1378
|
+
code: string
|
|
1379
|
+
/** Per-field validation errors. */
|
|
1380
|
+
fieldErrors?: FieldError[] | undefined
|
|
1381
|
+
/** Human-readable error message. */
|
|
1382
|
+
message: string
|
|
1383
|
+
/** Whether the operation can be retried. */
|
|
1384
|
+
retryable?: boolean | undefined
|
|
1385
|
+
}
|
|
1386
|
+
/** Request metadata. */
|
|
1387
|
+
meta: Output.Meta
|
|
1388
|
+
/** Whether the command succeeded. */
|
|
1389
|
+
ok: false
|
|
1390
|
+
}
|
|
1391
|
+
>
|
|
1392
|
+
|
|
1393
|
+
/** @internal */
|
|
1394
|
+
declare namespace Output {
|
|
1395
|
+
/** Shared metadata included in every envelope. */
|
|
1396
|
+
type Meta = {
|
|
1397
|
+
/** The command that was invoked. */
|
|
1398
|
+
command: string
|
|
1399
|
+
/** Suggested next commands. */
|
|
1400
|
+
cta?: FormattedCtaBlock | undefined
|
|
1401
|
+
/** Wall-clock duration of the command. */
|
|
1402
|
+
duration: string
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/** @internal Defines a command's schema, handler, and metadata. */
|
|
1407
|
+
type CommandDefinition<
|
|
1408
|
+
args extends z.ZodObject<any> | undefined = undefined,
|
|
1409
|
+
env extends z.ZodObject<any> | undefined = undefined,
|
|
1410
|
+
options extends z.ZodObject<any> | undefined = undefined,
|
|
1411
|
+
output extends z.ZodType | undefined = undefined,
|
|
1412
|
+
> = {
|
|
1413
|
+
/** Map of option names to single-char aliases. */
|
|
1414
|
+
alias?: options extends z.ZodObject<any>
|
|
1415
|
+
? Partial<Record<keyof z.output<options>, string>>
|
|
1416
|
+
: Record<string, string> | undefined
|
|
1417
|
+
/** Zod schema for positional arguments. */
|
|
1418
|
+
args?: args | undefined
|
|
1419
|
+
/** A short description of what the command does. */
|
|
1420
|
+
description?: string | undefined
|
|
1421
|
+
/** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
|
|
1422
|
+
env?: env | undefined
|
|
1423
|
+
/** Usage examples for this command. */
|
|
1424
|
+
examples?: Example<args, options>[] | undefined
|
|
1425
|
+
/** Default output format. Overridden by `--format` or `--json`. */
|
|
1426
|
+
format?: Formatter.Format | undefined
|
|
1427
|
+
/** Plain text hint displayed after examples and before global options. */
|
|
1428
|
+
hint?: string | undefined
|
|
1429
|
+
/** Zod schema for named options/flags. */
|
|
1430
|
+
options?: options | undefined
|
|
1431
|
+
/** Zod schema for the command's return value. */
|
|
1432
|
+
output?: output | undefined
|
|
1433
|
+
/** Alternative usage patterns shown in help output. */
|
|
1434
|
+
usage?: Usage<args, options>[] | undefined
|
|
1435
|
+
/** The command handler. Return a value for single-return, or use `async *run` to stream chunks. */
|
|
1436
|
+
run(context: {
|
|
1437
|
+
args: InferOutput<args>
|
|
1438
|
+
/** Parsed environment variables. */
|
|
1439
|
+
env: InferOutput<env>
|
|
1440
|
+
/** Return an error result with optional CTAs. */
|
|
1441
|
+
error: (options: {
|
|
1442
|
+
code: string
|
|
1443
|
+
cta?: CtaBlock | undefined
|
|
1444
|
+
message: string
|
|
1445
|
+
retryable?: boolean | undefined
|
|
1446
|
+
}) => never
|
|
1447
|
+
/** Return a success result with optional metadata (e.g. CTAs). */
|
|
1448
|
+
ok: (data: InferReturn<output>, meta?: { cta?: CtaBlock | undefined }) => never
|
|
1449
|
+
options: InferOutput<options>
|
|
1450
|
+
}):
|
|
1451
|
+
| InferReturn<output>
|
|
1452
|
+
| Promise<InferReturn<output>>
|
|
1453
|
+
| AsyncGenerator<InferReturn<output>, unknown, unknown>
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/** @internal A formatted CTA block as it appears in the output envelope. */
|
|
1457
|
+
type FormattedCtaBlock = {
|
|
1458
|
+
/** Formatted command suggestions. */
|
|
1459
|
+
commands: FormattedCta[]
|
|
1460
|
+
/** Human-readable label for the CTA block. */
|
|
1461
|
+
description: string
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/** @internal A formatted CTA as it appears in the output envelope. */
|
|
1465
|
+
type FormattedCta = {
|
|
1466
|
+
/** The full command string with args and options folded in. */
|
|
1467
|
+
command: string
|
|
1468
|
+
/** A short description of what the command does. */
|
|
1469
|
+
description?: string | undefined
|
|
1470
|
+
}
|