incur 0.4.0 → 0.4.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.
Files changed (115) hide show
  1. package/README.md +83 -22
  2. package/SKILL.md +6 -6
  3. package/dist/Cli.d.ts +46 -26
  4. package/dist/Cli.d.ts.map +1 -1
  5. package/dist/Cli.js +728 -441
  6. package/dist/Cli.js.map +1 -1
  7. package/dist/Completions.d.ts +4 -3
  8. package/dist/Completions.d.ts.map +1 -1
  9. package/dist/Completions.js +17 -10
  10. package/dist/Completions.js.map +1 -1
  11. package/dist/Fetch.d.ts.map +1 -1
  12. package/dist/Fetch.js +10 -9
  13. package/dist/Fetch.js.map +1 -1
  14. package/dist/Filter.js +0 -18
  15. package/dist/Filter.js.map +1 -1
  16. package/dist/Formatter.d.ts.map +1 -1
  17. package/dist/Formatter.js +6 -1
  18. package/dist/Formatter.js.map +1 -1
  19. package/dist/Help.d.ts +7 -1
  20. package/dist/Help.d.ts.map +1 -1
  21. package/dist/Help.js +44 -27
  22. package/dist/Help.js.map +1 -1
  23. package/dist/Mcp.d.ts +37 -5
  24. package/dist/Mcp.d.ts.map +1 -1
  25. package/dist/Mcp.js +71 -72
  26. package/dist/Mcp.js.map +1 -1
  27. package/dist/Openapi.d.ts.map +1 -1
  28. package/dist/Openapi.js +22 -14
  29. package/dist/Openapi.js.map +1 -1
  30. package/dist/Parser.d.ts +4 -0
  31. package/dist/Parser.d.ts.map +1 -1
  32. package/dist/Parser.js +70 -38
  33. package/dist/Parser.js.map +1 -1
  34. package/dist/Schema.d.ts +5 -1
  35. package/dist/Schema.d.ts.map +1 -1
  36. package/dist/Schema.js +13 -2
  37. package/dist/Schema.js.map +1 -1
  38. package/dist/Skill.d.ts +2 -1
  39. package/dist/Skill.d.ts.map +1 -1
  40. package/dist/Skill.js +33 -19
  41. package/dist/Skill.js.map +1 -1
  42. package/dist/Skillgen.js +1 -1
  43. package/dist/Skillgen.js.map +1 -1
  44. package/dist/SyncSkills.d.ts +48 -0
  45. package/dist/SyncSkills.d.ts.map +1 -1
  46. package/dist/SyncSkills.js +108 -10
  47. package/dist/SyncSkills.js.map +1 -1
  48. package/dist/Typegen.js +4 -2
  49. package/dist/Typegen.js.map +1 -1
  50. package/dist/bin.d.ts +2 -1
  51. package/dist/bin.d.ts.map +1 -1
  52. package/dist/bin.js +17 -2
  53. package/dist/bin.js.map +1 -1
  54. package/dist/internal/command.d.ts +170 -0
  55. package/dist/internal/command.d.ts.map +1 -0
  56. package/dist/internal/command.js +292 -0
  57. package/dist/internal/command.js.map +1 -0
  58. package/dist/internal/configSchema.d.ts +8 -0
  59. package/dist/internal/configSchema.d.ts.map +1 -0
  60. package/dist/internal/configSchema.js +57 -0
  61. package/dist/internal/configSchema.js.map +1 -0
  62. package/dist/internal/dereference.d.ts +12 -0
  63. package/dist/internal/dereference.d.ts.map +1 -0
  64. package/dist/internal/dereference.js +71 -0
  65. package/dist/internal/dereference.js.map +1 -0
  66. package/dist/internal/helpers.d.ts +9 -0
  67. package/dist/internal/helpers.d.ts.map +1 -0
  68. package/dist/internal/helpers.js +54 -0
  69. package/dist/internal/helpers.js.map +1 -0
  70. package/dist/middleware.d.ts +6 -8
  71. package/dist/middleware.d.ts.map +1 -1
  72. package/dist/middleware.js +1 -1
  73. package/dist/middleware.js.map +1 -1
  74. package/examples/npm/.npmrc.json +21 -0
  75. package/examples/npm/config.schema.json +134 -0
  76. package/package.json +6 -29
  77. package/src/Cli.test-d.ts +44 -33
  78. package/src/Cli.test.ts +1231 -101
  79. package/src/Cli.ts +877 -569
  80. package/src/Completions.test.ts +136 -12
  81. package/src/Completions.ts +18 -13
  82. package/src/Fetch.test.ts +21 -0
  83. package/src/Fetch.ts +8 -10
  84. package/src/Filter.ts +0 -17
  85. package/src/Formatter.test.ts +15 -2
  86. package/src/Formatter.ts +5 -1
  87. package/src/Help.test.ts +184 -20
  88. package/src/Help.ts +52 -28
  89. package/src/Mcp.test.ts +159 -0
  90. package/src/Mcp.ts +108 -86
  91. package/src/Openapi.test.ts +17 -5
  92. package/src/Openapi.ts +21 -15
  93. package/src/Parser.test-d.ts +22 -0
  94. package/src/Parser.test.ts +89 -0
  95. package/src/Parser.ts +87 -36
  96. package/src/Schema.test.ts +29 -0
  97. package/src/Schema.ts +12 -2
  98. package/src/Skill.test.ts +87 -6
  99. package/src/Skill.ts +38 -21
  100. package/src/Skillgen.ts +1 -1
  101. package/src/SyncMcp.test.ts +6 -8
  102. package/src/SyncSkills.test.ts +146 -3
  103. package/src/SyncSkills.ts +191 -10
  104. package/src/Typegen.test.ts +15 -0
  105. package/src/Typegen.ts +4 -2
  106. package/src/bin.ts +21 -2
  107. package/src/e2e.test.ts +188 -98
  108. package/src/internal/command.ts +449 -0
  109. package/src/internal/configSchema.test.ts +193 -0
  110. package/src/internal/configSchema.ts +66 -0
  111. package/src/internal/dereference.test.ts +695 -0
  112. package/src/internal/dereference.ts +75 -0
  113. package/src/internal/helpers.test.ts +75 -0
  114. package/src/internal/helpers.ts +59 -0
  115. package/src/middleware.ts +5 -12
package/dist/Cli.js CHANGED
@@ -1,10 +1,17 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
1
4
  import { estimateTokenCount, sliceByTokens } from 'tokenx';
5
+ import { z } from 'zod';
2
6
  import * as Completions from './Completions.js';
3
- import { IncurError, ValidationError } from './Errors.js';
7
+ import { IncurError, ParseError, ValidationError } from './Errors.js';
4
8
  import * as Fetch from './Fetch.js';
5
9
  import * as Filter from './Filter.js';
6
10
  import * as Formatter from './Formatter.js';
7
11
  import * as Help from './Help.js';
12
+ import { builtinCommands, findBuiltin, shells, } from './internal/command.js';
13
+ import * as Command from './internal/command.js';
14
+ import { isRecord, suggest } from './internal/helpers.js';
8
15
  import { detectRunner } from './internal/pm.js';
9
16
  import * as Mcp from './Mcp.js';
10
17
  import * as Openapi from './Openapi.js';
@@ -26,7 +33,6 @@ export function create(nameOrDefinition, definition) {
26
33
  name,
27
34
  description: def.description,
28
35
  env: def.env,
29
- options: def.options,
30
36
  vars: def.vars,
31
37
  command(nameOrCli, def) {
32
38
  if (typeof nameOrCli === 'string') {
@@ -53,11 +59,18 @@ export function create(nameOrDefinition, definition) {
53
59
  return cli;
54
60
  }
55
61
  commands.set(nameOrCli, def);
62
+ if (def.aliases)
63
+ for (const a of def.aliases)
64
+ commands.set(a, { _alias: true, target: nameOrCli });
56
65
  return cli;
57
66
  }
58
67
  const mountedRootDef = toRootDefinition.get(nameOrCli);
59
68
  if (mountedRootDef) {
60
69
  commands.set(nameOrCli.name, mountedRootDef);
70
+ const rootAliases = toRootAliases.get(nameOrCli);
71
+ if (rootAliases)
72
+ for (const a of rootAliases)
73
+ commands.set(a, { _alias: true, target: nameOrCli.name });
61
74
  return cli;
62
75
  }
63
76
  const sub = nameOrCli;
@@ -77,10 +90,13 @@ export function create(nameOrDefinition, definition) {
77
90
  if (pending.length > 0)
78
91
  await Promise.all(pending);
79
92
  return fetchImpl(name, commands, req, {
93
+ envSchema: def.env,
80
94
  mcpHandler,
81
95
  middlewares,
96
+ name,
82
97
  rootCommand: rootDef,
83
98
  vars: def.vars,
99
+ version: def.version,
84
100
  });
85
101
  },
86
102
  async serve(argv = process.argv.slice(2), serveOptions = {}) {
@@ -89,12 +105,12 @@ export function create(nameOrDefinition, definition) {
89
105
  return serveImpl(name, commands, argv, {
90
106
  ...serveOptions,
91
107
  aliases: def.aliases,
108
+ config: def.config,
92
109
  description: def.description,
93
110
  envSchema: def.env,
94
111
  format: def.format,
95
112
  mcp: def.mcp,
96
113
  middlewares,
97
- optionsSchema: def.options,
98
114
  outputPolicy: def.outputPolicy,
99
115
  rootCommand: rootDef,
100
116
  rootFetch,
@@ -110,6 +126,12 @@ export function create(nameOrDefinition, definition) {
110
126
  };
111
127
  if (rootDef)
112
128
  toRootDefinition.set(cli, rootDef);
129
+ if (rootDef && def.aliases)
130
+ toRootAliases.set(cli, def.aliases);
131
+ if (def.options)
132
+ toRootOptions.set(cli, def.options);
133
+ if (def.config !== undefined)
134
+ toConfigEnabled.set(cli, true);
113
135
  if (def.outputPolicy)
114
136
  toOutputPolicy.set(cli, def.outputPolicy);
115
137
  toMiddlewares.set(cli, middlewares);
@@ -121,10 +143,35 @@ export function create(nameOrDefinition, definition) {
121
143
  async function serveImpl(name, commands, argv, options = {}) {
122
144
  const stdout = options.stdout ?? ((s) => process.stdout.write(s));
123
145
  const exit = options.exit ?? ((code) => process.exit(code));
124
- const { verbose, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, rest: filtered, } = extractBuiltinFlags(argv);
146
+ const human = process.stdout.isTTY === true;
147
+ const configEnabled = options.config !== undefined;
148
+ const configFlag = options.config?.flag;
149
+ const displayName = resolveDisplayName(name, options.aliases);
150
+ function writeln(s) {
151
+ stdout(s.endsWith('\n') ? s : `${s}\n`);
152
+ }
153
+ let builtinFlags;
154
+ try {
155
+ builtinFlags = extractBuiltinFlags(argv, { configFlag });
156
+ }
157
+ catch (error) {
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ if (human)
160
+ writeln(formatHumanError({ code: 'UNKNOWN', message }));
161
+ else
162
+ writeln(Formatter.format({ code: 'UNKNOWN', message }, 'toon'));
163
+ exit(1);
164
+ return;
165
+ }
166
+ const { fullOutput, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, configPath, configDisabled, rest: filtered, } = builtinFlags;
125
167
  // --mcp: start as MCP stdio server
126
168
  if (mcpFlag) {
127
- await Mcp.serve(name, options.version ?? '0.0.0', commands);
169
+ await Mcp.serve(name, options.version ?? '0.0.0', commands, {
170
+ middlewares: options.middlewares,
171
+ env: options.envSchema,
172
+ vars: options.vars,
173
+ version: options.version,
174
+ });
128
175
  return;
129
176
  }
130
177
  // COMPLETE: dynamic shell completions (called by shell hook at tab-press)
@@ -141,30 +188,51 @@ async function serveImpl(name, commands, argv, options = {}) {
141
188
  else {
142
189
  const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1);
143
190
  const candidates = Completions.complete(commands, options.rootCommand, words, index);
191
+ // Add built-in commands (completions, mcp, skills) to completions
192
+ const current = words[index] ?? '';
193
+ const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'));
194
+ if (nonFlags.length <= 1) {
195
+ for (const b of builtinCommands) {
196
+ if (b.name.startsWith(current) && !candidates.some((c) => c.value === b.name))
197
+ candidates.push({
198
+ value: b.name,
199
+ description: b.description,
200
+ ...(b.subcommands ? { noSpace: true } : undefined),
201
+ });
202
+ }
203
+ }
204
+ else if (nonFlags.length === 2) {
205
+ const parent = nonFlags[nonFlags.length - 1];
206
+ const builtin = findBuiltin(parent);
207
+ if (builtin?.subcommands)
208
+ for (const sub of builtin.subcommands)
209
+ if (sub.name.startsWith(current))
210
+ candidates.push({ value: sub.name, description: sub.description });
211
+ }
144
212
  const out = Completions.format(completeShell, candidates);
145
213
  if (out)
146
214
  stdout(out);
147
215
  }
148
216
  return;
149
217
  }
150
- // Human mode: stdout is a TTY.
151
- const human = process.stdout.isTTY === true;
152
- function writeln(s) {
153
- stdout(s.endsWith('\n') ? s : `${s}\n`);
154
- }
155
218
  // Skills staleness check (skip for built-in commands)
219
+ let skillsCta;
156
220
  if (!llms && !llmsFull && !schema && !help && !version) {
157
- const isSkillsAdd = filtered[0] === 'skills' || (filtered[0] === name && filtered[1] === 'skills');
158
- const isMcpAdd = filtered[0] === 'mcp' || (filtered[0] === name && filtered[1] === 'mcp');
221
+ const isSkillsAdd = builtinIdx(filtered, name, 'skills') !== -1;
222
+ const isMcpAdd = builtinIdx(filtered, name, 'mcp') !== -1;
159
223
  if (!isSkillsAdd && !isMcpAdd) {
160
224
  const stored = SyncSkills.readHash(name);
161
- if (stored) {
225
+ if (stored && SyncSkills.hasInstalledSkills(name, { cwd: options.sync?.cwd })) {
162
226
  const groups = new Map();
163
- const entries = collectSkillCommands(commands, [], groups);
227
+ const entries = collectSkillCommands(commands, [], groups, options.rootCommand);
164
228
  if (Skill.hash(entries) !== stored) {
165
- const runner = detectRunner();
166
- const spec = SyncMcp.detectPackageSpecifier(name);
167
- process.stderr.write(`⚠ Skills are out of date. Run '${runner} ${spec} skills add' to update.\n\n`);
229
+ const command = process.env.npm_config_user_agent || process.env.npm_execpath
230
+ ? `${detectRunner()} ${SyncMcp.detectPackageSpecifier(name)} skills add`
231
+ : `${displayName} skills add`;
232
+ skillsCta = {
233
+ description: 'Skills are out of date:',
234
+ commands: [{ command, description: 'sync outdated skills' }],
235
+ };
168
236
  }
169
237
  }
170
238
  }
@@ -175,9 +243,10 @@ async function serveImpl(name, commands, argv, options = {}) {
175
243
  const prefix = [];
176
244
  let scopedDescription = options.description;
177
245
  for (const token of filtered) {
178
- const entry = scopedCommands.get(token);
179
- if (!entry)
246
+ const rawEntry = scopedCommands.get(token);
247
+ if (!rawEntry)
180
248
  break;
249
+ const entry = resolveAlias(scopedCommands, rawEntry);
181
250
  if (isGroup(entry)) {
182
251
  scopedCommands = entry.commands;
183
252
  scopedDescription = entry.description;
@@ -189,10 +258,11 @@ async function serveImpl(name, commands, argv, options = {}) {
189
258
  break;
190
259
  }
191
260
  }
261
+ const scopedRoot = prefix.length === 0 ? options.rootCommand : undefined;
192
262
  if (llmsFull) {
193
263
  if (!formatExplicit || formatFlag === 'md') {
194
264
  const groups = new Map();
195
- const cmds = collectSkillCommands(scopedCommands, prefix, groups);
265
+ const cmds = collectSkillCommands(scopedCommands, prefix, groups, scopedRoot);
196
266
  const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name;
197
267
  writeln(Skill.generate(scopedName, cmds, groups));
198
268
  return;
@@ -202,7 +272,7 @@ async function serveImpl(name, commands, argv, options = {}) {
202
272
  }
203
273
  if (!formatExplicit || formatFlag === 'md') {
204
274
  const groups = new Map();
205
- const cmds = collectSkillCommands(scopedCommands, prefix, groups);
275
+ const cmds = collectSkillCommands(scopedCommands, prefix, groups, scopedRoot);
206
276
  const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name;
207
277
  writeln(Skill.index(scopedName, cmds, scopedDescription));
208
278
  return;
@@ -211,51 +281,23 @@ async function serveImpl(name, commands, argv, options = {}) {
211
281
  return;
212
282
  }
213
283
  // completions <shell>: print shell hook script to stdout
214
- const completionsIdx = (() => {
215
- // e.g. `completions bash`
216
- if (filtered[0] === 'completions')
217
- return 0;
218
- // e.g. `my-cli completions bash`
219
- if (filtered[0] === name && filtered[1] === 'completions')
220
- return 1;
221
- // not a completions invocation
222
- return -1;
223
- })();
224
- if (completionsIdx !== -1 && filtered[completionsIdx] === 'completions') {
225
- if (help) {
226
- writeln([
227
- `${name} completions — Generate shell completion script`,
228
- '',
229
- `Usage: ${name} completions <shell>`,
230
- '',
231
- 'Shells:',
232
- ' bash',
233
- ' fish',
234
- ' nushell',
235
- ' zsh',
236
- '',
237
- 'Setup:',
238
- ...(() => {
239
- const rows = [
240
- ['bash', `eval "$(${name} completions bash)"`, '# add to ~/.bashrc'],
241
- ['zsh', `eval "$(${name} completions zsh)"`, '# add to ~/.zshrc'],
242
- ['fish', `${name} completions fish | source`, '# add to ~/.config/fish/config.fish'],
243
- ['nushell', `see \`${name} completions nushell\``, '# add to config.nu'],
244
- ];
245
- const shellW = Math.max(...rows.map((r) => r[0].length));
246
- const cmdW = Math.max(...rows.map((r) => r[1].length));
247
- return rows.map(([shell, cmd, comment]) => ` ${shell.padEnd(shellW)} ${cmd.padEnd(cmdW)} ${comment}`);
248
- })(),
249
- ].join('\n'));
284
+ const completionsIdx = builtinIdx(filtered, name, 'completions');
285
+ if (completionsIdx !== -1) {
286
+ const shell = filtered[completionsIdx + 1];
287
+ if (help || !shell) {
288
+ const b = findBuiltin('completions');
289
+ writeln(Help.formatCommand(`${name} completions`, {
290
+ args: b.args,
291
+ description: b.description,
292
+ hideGlobalOptions: true,
293
+ hint: b.hint?.(name),
294
+ }));
250
295
  return;
251
296
  }
252
- const shell = filtered[completionsIdx + 1];
253
- if (!shell || !['bash', 'fish', 'nushell', 'zsh'].includes(shell)) {
297
+ if (!shells.includes(shell)) {
254
298
  writeln(formatHumanError({
255
299
  code: 'INVALID_SHELL',
256
- message: shell
257
- ? `Unknown shell '${shell}'. Supported: bash, fish, nushell, zsh`
258
- : `Missing shell argument. Usage: ${name} completions <bash|fish|nushell|zsh>`,
300
+ message: `Unknown shell '${shell}'. Supported: ${shells.join(', ')}`,
259
301
  }));
260
302
  exit(1);
261
303
  return;
@@ -265,18 +307,84 @@ async function serveImpl(name, commands, argv, options = {}) {
265
307
  return;
266
308
  }
267
309
  // skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
268
- const skillsIdx = filtered[0] === 'skills' ? 0 : filtered[0] === name && filtered[1] === 'skills' ? 1 : -1;
269
- if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills' && filtered[skillsIdx + 1] === 'add') {
310
+ const skillsIdx = builtinIdx(filtered, name, 'skills');
311
+ if (skillsIdx !== -1) {
312
+ const skillsSub = filtered[skillsIdx + 1];
313
+ if (skillsSub && skillsSub !== 'add' && skillsSub !== 'list') {
314
+ const suggestion = suggest(skillsSub, ['add', 'list']);
315
+ const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
316
+ const message = `'${skillsSub}' is not a command for '${name} skills'.${didYouMean}`;
317
+ const ctaCommands = [];
318
+ if (suggestion) {
319
+ const corrected = argv.map((t) => (t === skillsSub ? suggestion : t));
320
+ ctaCommands.push({ command: `${name} ${corrected.join(' ')}` });
321
+ }
322
+ ctaCommands.push({
323
+ command: `${name} skills --help`,
324
+ description: 'see all available commands',
325
+ });
326
+ const cta = {
327
+ description: ctaCommands.length === 1 ? 'Suggested command:' : 'Suggested commands:',
328
+ commands: ctaCommands,
329
+ };
330
+ if (human) {
331
+ writeln(formatHumanError({ code: 'COMMAND_NOT_FOUND', message }));
332
+ writeln(formatHumanCta(cta));
333
+ }
334
+ else
335
+ writeln(Formatter.format({ code: 'COMMAND_NOT_FOUND', message, cta }, 'toon'));
336
+ exit(1);
337
+ return;
338
+ }
339
+ if (!skillsSub) {
340
+ const b = findBuiltin('skills');
341
+ writeln(formatBuiltinHelp(name, b));
342
+ return;
343
+ }
344
+ if (skillsSub === 'list') {
345
+ if (help) {
346
+ const b = findBuiltin('skills');
347
+ writeln(formatBuiltinSubcommandHelp(name, b, 'list'));
348
+ return;
349
+ }
350
+ try {
351
+ const result = await SyncSkills.list(name, commands, {
352
+ cwd: options.sync?.cwd,
353
+ depth: options.sync?.depth ?? 1,
354
+ description: options.description,
355
+ include: options.sync?.include,
356
+ rootCommand: options.rootCommand,
357
+ });
358
+ if (result.length === 0) {
359
+ writeln('No skills found.');
360
+ return;
361
+ }
362
+ const lines = [];
363
+ const maxLen = Math.max(...result.map((s) => s.name.length));
364
+ for (const s of result) {
365
+ const icon = s.installed ? '✓' : '✗';
366
+ const padding = s.description
367
+ ? `${' '.repeat(maxLen - s.name.length)} ${s.description}`
368
+ : '';
369
+ lines.push(` ${icon} ${s.name}${padding}`);
370
+ }
371
+ const installedCount = result.filter((s) => s.installed).length;
372
+ lines.push('');
373
+ lines.push(`${result.length} skill${result.length === 1 ? '' : 's'} (${installedCount} installed)`);
374
+ writeln(lines.join('\n'));
375
+ }
376
+ catch (err) {
377
+ writeln(Formatter.format({
378
+ code: 'LIST_SKILLS_FAILED',
379
+ message: err instanceof Error ? err.message : String(err),
380
+ }, formatExplicit ? formatFlag : 'toon'));
381
+ exit(1);
382
+ }
383
+ return;
384
+ }
270
385
  if (help) {
271
- writeln([
272
- `${name} skills add — Sync skill files to your agent`,
273
- '',
274
- `Usage: ${name} skills add [options]`,
275
- '',
276
- 'Options:',
277
- ' --depth <number> Grouping depth for skill files (default: 1)',
278
- ' --no-global Install to project instead of globally',
279
- ].join('\n'));
386
+ const b = findBuiltin('skills');
387
+ writeln(formatBuiltinSubcommandHelp(name, b, 'add'));
280
388
  return;
281
389
  }
282
390
  const rest = filtered.slice(skillsIdx + 2);
@@ -296,10 +404,11 @@ async function serveImpl(name, commands, argv, options = {}) {
296
404
  description: options.description,
297
405
  global,
298
406
  include: options.sync?.include,
407
+ rootCommand: options.rootCommand,
299
408
  });
300
409
  stdout('\r\x1b[K');
301
410
  const lines = [];
302
- const skillLabel = (s) => s.external || s.name === name ? s.name : `${name}-${s.name}`;
411
+ const skillLabel = (s) => s.name;
303
412
  const maxLen = Math.max(...result.skills.map((s) => skillLabel(s).length));
304
413
  for (const s of result.skills) {
305
414
  const label = skillLabel(s);
@@ -320,9 +429,9 @@ async function serveImpl(name, commands, argv, options = {}) {
320
429
  lines.push('');
321
430
  lines.push(`Run \`${name} --help\` to see the full command reference.`);
322
431
  writeln(lines.join('\n'));
323
- if (verbose || formatExplicit) {
432
+ if (fullOutput || formatExplicit) {
324
433
  const output = { skills: result.paths };
325
- if (verbose && result.agents.length > 0)
434
+ if (fullOutput && result.agents.length > 0)
326
435
  output.agents = result.agents;
327
436
  writeln(Formatter.format(output, formatExplicit ? formatFlag : 'toon'));
328
437
  }
@@ -334,19 +443,40 @@ async function serveImpl(name, commands, argv, options = {}) {
334
443
  return;
335
444
  }
336
445
  // mcp add: register CLI as MCP server via `npx add-mcp`
337
- const mcpIdx = filtered[0] === 'mcp' ? 0 : filtered[0] === name && filtered[1] === 'mcp' ? 1 : -1;
338
- if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp' && filtered[mcpIdx + 1] === 'add') {
446
+ const mcpIdx = builtinIdx(filtered, name, 'mcp');
447
+ if (mcpIdx !== -1) {
448
+ const mcpSub = filtered[mcpIdx + 1];
449
+ if (mcpSub && mcpSub !== 'add') {
450
+ const suggestion = suggest(mcpSub, ['add']);
451
+ const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
452
+ const message = `'${mcpSub}' is not a command for '${name} mcp'.${didYouMean}`;
453
+ const ctaCommands = [];
454
+ if (suggestion) {
455
+ const corrected = argv.map((t) => (t === mcpSub ? suggestion : t));
456
+ ctaCommands.push({ command: `${name} ${corrected.join(' ')}` });
457
+ }
458
+ ctaCommands.push({ command: `${name} mcp --help`, description: 'see all available commands' });
459
+ const cta = {
460
+ description: ctaCommands.length === 1 ? 'Suggested command:' : 'Suggested commands:',
461
+ commands: ctaCommands,
462
+ };
463
+ if (human) {
464
+ writeln(formatHumanError({ code: 'COMMAND_NOT_FOUND', message }));
465
+ writeln(formatHumanCta(cta));
466
+ }
467
+ else
468
+ writeln(Formatter.format({ code: 'COMMAND_NOT_FOUND', message, cta }, 'toon'));
469
+ exit(1);
470
+ return;
471
+ }
472
+ if (!mcpSub) {
473
+ const b = findBuiltin('mcp');
474
+ writeln(formatBuiltinHelp(name, b));
475
+ return;
476
+ }
339
477
  if (help) {
340
- writeln([
341
- `${name} mcp add — Register as an MCP server for your agent`,
342
- '',
343
- `Usage: ${name} mcp add [options]`,
344
- '',
345
- 'Options:',
346
- ' -c, --command <cmd> Override the command agents will run (e.g. "pnpm my-cli --mcp")',
347
- ' --no-global Install to project instead of globally',
348
- ' --agent <agent> Target a specific agent (e.g. claude-code, cursor)',
349
- ].join('\n'));
478
+ const b = findBuiltin('mcp');
479
+ writeln(formatBuiltinSubcommandHelp(name, b, 'add'));
350
480
  return;
351
481
  }
352
482
  const rest = filtered.slice(mcpIdx + 2);
@@ -382,7 +512,7 @@ async function serveImpl(name, commands, argv, options = {}) {
382
512
  lines.push(` "${s}"`);
383
513
  }
384
514
  writeln(lines.join('\n'));
385
- if (verbose || formatExplicit)
515
+ if (fullOutput || formatExplicit)
386
516
  writeln(Formatter.format({ name, command: result.command, agents: result.agents }, formatExplicit ? formatFlag : 'toon'));
387
517
  }
388
518
  catch (err) {
@@ -406,6 +536,7 @@ async function serveImpl(name, commands, argv, options = {}) {
406
536
  writeln(Help.formatCommand(name, {
407
537
  alias: cmd.alias,
408
538
  aliases: options.aliases,
539
+ configFlag,
409
540
  description: cmd.description ?? options.description,
410
541
  version: options.version,
411
542
  args: cmd.args,
@@ -426,6 +557,7 @@ async function serveImpl(name, commands, argv, options = {}) {
426
557
  else {
427
558
  writeln(Help.formatRoot(name, {
428
559
  aliases: options.aliases,
560
+ configFlag,
429
561
  description: options.description,
430
562
  version: options.version,
431
563
  commands: collectHelpCommands(commands),
@@ -468,6 +600,7 @@ async function serveImpl(name, commands, argv, options = {}) {
468
600
  writeln(Help.formatCommand(name, {
469
601
  alias: cmd.alias,
470
602
  aliases: options.aliases,
603
+ configFlag,
471
604
  description: cmd.description ?? options.description,
472
605
  version: options.version,
473
606
  args: cmd.args,
@@ -484,6 +617,7 @@ async function serveImpl(name, commands, argv, options = {}) {
484
617
  else {
485
618
  writeln(Help.formatRoot(helpName, {
486
619
  aliases: isRoot ? options.aliases : undefined,
620
+ configFlag,
487
621
  description: helpDesc,
488
622
  version: isRoot ? options.version : undefined,
489
623
  commands: collectHelpCommands(helpCmds),
@@ -500,7 +634,8 @@ async function serveImpl(name, commands, argv, options = {}) {
500
634
  : undefined;
501
635
  writeln(Help.formatCommand(commandName, {
502
636
  alias: cmd.alias,
503
- aliases: isRootCmd ? options.aliases : undefined,
637
+ aliases: isRootCmd ? options.aliases : cmd.aliases,
638
+ configFlag,
504
639
  description: cmd.description,
505
640
  version: isRootCmd ? options.version : undefined,
506
641
  args: cmd.args,
@@ -520,6 +655,7 @@ async function serveImpl(name, commands, argv, options = {}) {
520
655
  if (schema) {
521
656
  if ('help' in resolved) {
522
657
  writeln(Help.formatRoot(`${name} ${resolved.path}`, {
658
+ configFlag,
523
659
  description: resolved.description,
524
660
  commands: collectHelpCommands(resolved.commands),
525
661
  }));
@@ -527,7 +663,9 @@ async function serveImpl(name, commands, argv, options = {}) {
527
663
  }
528
664
  if ('error' in resolved) {
529
665
  const parent = resolved.path ? `${name} ${resolved.path}` : name;
530
- writeln(`Error: '${resolved.error}' is not a command for '${parent}'.`);
666
+ const suggestion = suggest(resolved.error, resolved.commands.keys());
667
+ const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
668
+ writeln(`Error: '${resolved.error}' is not a command for '${parent}'.${didYouMean}`);
531
669
  exit(1);
532
670
  return;
533
671
  }
@@ -552,24 +690,27 @@ async function serveImpl(name, commands, argv, options = {}) {
552
690
  }
553
691
  if ('help' in resolved) {
554
692
  writeln(Help.formatRoot(`${name} ${resolved.path}`, {
693
+ configFlag,
555
694
  description: resolved.description,
556
695
  commands: collectHelpCommands(resolved.commands),
557
696
  }));
558
697
  return;
559
698
  }
560
699
  const start = performance.now();
561
- // Parse root CLI-level options (available to middleware via c.options)
562
- const rootOptions = options.optionsSchema
563
- ? Parser.parse(filtered, {
564
- alias: options.rootCommand?.alias,
565
- options: options.optionsSchema,
566
- }).options
567
- : {};
568
700
  // Resolve effective format: explicit --format/--json → command default → CLI default → toon
569
701
  const resolvedFormat = 'command' in resolved && resolved.command.format;
570
702
  const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon';
571
- // Fall back to root fetch when no subcommand matches
572
- const effective = 'error' in resolved && options.rootFetch && !resolved.path
703
+ // Fall back to root fetch/command when no subcommand matches,
704
+ // but only if the token doesn't look like a typo of a known command.
705
+ const rootFallbackBlocked = 'error' in resolved &&
706
+ !resolved.path &&
707
+ (() => {
708
+ const candidates = [...resolved.commands.keys()];
709
+ for (const b of builtinCommands)
710
+ candidates.push(b.name);
711
+ return suggest(resolved.error, candidates) !== undefined;
712
+ })();
713
+ const effective = 'error' in resolved && options.rootFetch && !resolved.path && !rootFallbackBlocked
573
714
  ? {
574
715
  fetchGateway: {
575
716
  _fetch: true,
@@ -580,7 +721,7 @@ async function serveImpl(name, commands, argv, options = {}) {
580
721
  path: name,
581
722
  rest: filtered,
582
723
  }
583
- : 'error' in resolved && options.rootCommand && !resolved.path
724
+ : 'error' in resolved && options.rootCommand && !resolved.path && !rootFallbackBlocked
584
725
  ? { command: options.rootCommand, path: name, rest: filtered }
585
726
  : resolved;
586
727
  // Resolve outputPolicy: command/group → CLI-level → default ('all')
@@ -607,13 +748,28 @@ async function serveImpl(name, commands, argv, options = {}) {
607
748
  function write(output) {
608
749
  if (filterPaths && output.ok && output.data != null)
609
750
  output = { ...output, data: Filter.apply(output.data, filterPaths) };
751
+ if (skillsCta) {
752
+ const existing = output.meta.cta;
753
+ output = {
754
+ ...output,
755
+ meta: {
756
+ ...output.meta,
757
+ cta: existing
758
+ ? {
759
+ description: existing.description,
760
+ commands: [...existing.commands, ...skillsCta.commands],
761
+ }
762
+ : skillsCta,
763
+ },
764
+ };
765
+ }
610
766
  if (tokenCount) {
611
767
  const base = output.ok ? output.data : output.error;
612
768
  const formatted = base != null ? Formatter.format(base, format) : '';
613
769
  return writeln(String(estimateTokenCount(formatted)));
614
770
  }
615
771
  const cta = output.meta.cta;
616
- if (human && !verbose) {
772
+ if (human && !fullOutput) {
617
773
  if (output.ok && output.data != null && renderOutput) {
618
774
  const t = truncate(Formatter.format(output.data, format));
619
775
  writeln(t.text);
@@ -624,7 +780,7 @@ async function serveImpl(name, commands, argv, options = {}) {
624
780
  writeln(formatHumanCta(cta));
625
781
  return;
626
782
  }
627
- if (verbose) {
783
+ if (fullOutput) {
628
784
  if (tokenLimit != null || tokenOffset != null) {
629
785
  // Truncate data separately so meta (including nextOffset) is always visible
630
786
  const dataFormatted = output.ok && output.data != null
@@ -659,14 +815,29 @@ async function serveImpl(name, commands, argv, options = {}) {
659
815
  if ('error' in effective) {
660
816
  const helpCmd = effective.path ? `${name} ${effective.path} --help` : `${name} --help`;
661
817
  const parent = effective.path ? `${name} ${effective.path}` : name;
662
- const message = `'${effective.error}' is not a command for '${parent}'.`;
818
+ const candidates = 'commands' in effective ? [...effective.commands.keys()] : [];
819
+ if (!effective.path)
820
+ for (const b of builtinCommands)
821
+ candidates.push(b.name);
822
+ const suggestion = suggest(effective.error, candidates);
823
+ const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
824
+ const message = `'${effective.error}' is not a command for '${parent}'.${didYouMean}`;
825
+ const ctaCommands = [];
826
+ if (suggestion) {
827
+ const corrected = argv.map((t) => (t === effective.error ? suggestion : t));
828
+ ctaCommands.push({ command: `${name} ${corrected.join(' ')}` });
829
+ }
830
+ ctaCommands.push({ command: helpCmd, description: 'see all available commands' });
663
831
  const cta = {
664
- description: 'See available commands:',
665
- commands: [{ command: helpCmd }],
832
+ description: ctaCommands.length === 1 ? 'Suggested command:' : 'Suggested commands:',
833
+ commands: ctaCommands,
666
834
  };
667
- if (human && !verbose) {
835
+ if (human && !fullOutput) {
668
836
  writeln(formatHumanError({ code: 'COMMAND_NOT_FOUND', message }));
669
- writeln(formatHumanCta(cta));
837
+ const mergedCta = skillsCta
838
+ ? { ...cta, commands: [...cta.commands, ...skillsCta.commands] }
839
+ : cta;
840
+ writeln(formatHumanCta(mergedCta));
670
841
  exit(1);
671
842
  return;
672
843
  }
@@ -706,7 +877,7 @@ async function serveImpl(name, commands, argv, options = {}) {
706
877
  formatExplicit,
707
878
  human,
708
879
  renderOutput,
709
- verbose,
880
+ fullOutput,
710
881
  truncate,
711
882
  write,
712
883
  writeln,
@@ -754,12 +925,12 @@ async function serveImpl(name, commands, argv, options = {}) {
754
925
  const mwCtx = {
755
926
  agent: !human,
756
927
  command: path,
928
+ displayName,
757
929
  env: cliEnv,
758
930
  error: errorFn,
759
931
  format,
760
932
  formatExplicit,
761
933
  name,
762
- options: rootOptions,
763
934
  set(key, value) {
764
935
  varsMap[key] = value;
765
936
  },
@@ -770,7 +941,7 @@ async function serveImpl(name, commands, argv, options = {}) {
770
941
  if (!isSentinel(result) || result[sentinel] !== 'error')
771
942
  return;
772
943
  const err = result;
773
- const cta = formatCtaBlock(name, err.cta);
944
+ const cta = formatCtaBlock(displayName, err.cta);
774
945
  write({
775
946
  ok: false,
776
947
  error: {
@@ -817,182 +988,113 @@ async function serveImpl(name, commands, argv, options = {}) {
817
988
  : []),
818
989
  ...(command.middleware ?? []),
819
990
  ];
820
- // Initialize vars from schema defaults
821
- const varsMap = options.vars ? options.vars.parse({}) : {};
822
- const envSource = options.env ?? process.env;
823
- const runCommand = async () => {
824
- const { args, options: parsedOptions } = Parser.parse(rest, {
825
- alias: command.alias,
826
- args: command.args,
827
- options: command.options,
828
- });
829
- if (human)
830
- emitDeprecationWarnings(rest, command.options, command.alias);
831
- const env = command.env ? Parser.parseEnv(command.env, envSource) : {};
832
- const okFn = (data, meta = {}) => {
833
- return { [sentinel]: 'ok', data, cta: meta.cta };
834
- };
835
- const errorFn = (opts) => {
836
- return { [sentinel]: 'error', ...opts };
837
- };
838
- const result = command.run({
839
- agent: !human,
840
- args,
841
- env,
842
- error: errorFn,
843
- format,
844
- formatExplicit,
845
- name,
846
- ok: okFn,
847
- options: parsedOptions,
848
- var: varsMap,
849
- version: options.version,
850
- });
851
- // Streaming path — async generator
852
- if (isAsyncGenerator(result)) {
853
- await handleStreaming(result, {
854
- name,
855
- path,
856
- start,
857
- format,
858
- formatExplicit,
859
- human,
860
- renderOutput,
861
- verbose,
862
- truncate,
863
- write,
864
- writeln,
865
- exit,
991
+ if (human)
992
+ emitDeprecationWarnings(rest, command.options, command.alias);
993
+ let defaults;
994
+ if (configEnabled) {
995
+ try {
996
+ defaults = await loadCommandOptionDefaults(name, path, {
997
+ configDisabled,
998
+ configPath,
999
+ files: options.config?.files,
1000
+ loader: options.config?.loader,
866
1001
  });
867
- return;
868
- }
869
- const awaited = await result;
870
- if (isSentinel(awaited)) {
871
- const cta = formatCtaBlock(name, awaited.cta);
872
- if (awaited[sentinel] === 'ok') {
873
- write({
874
- ok: true,
875
- data: awaited.data,
876
- meta: {
877
- command: path,
878
- duration: `${Math.round(performance.now() - start)}ms`,
879
- ...(cta ? { cta } : undefined),
880
- },
881
- });
882
- }
883
- else {
884
- const err = awaited;
885
- write({
886
- ok: false,
887
- error: {
888
- code: err.code,
889
- message: err.message,
890
- ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
891
- },
892
- meta: {
893
- command: path,
894
- duration: `${Math.round(performance.now() - start)}ms`,
895
- ...(cta ? { cta } : undefined),
896
- },
897
- });
898
- exit(err.exitCode ?? 1);
899
- }
900
1002
  }
901
- else {
1003
+ catch (error) {
902
1004
  write({
903
- ok: true,
904
- data: awaited,
905
- meta: {
906
- command: path,
907
- duration: `${Math.round(performance.now() - start)}ms`,
1005
+ ok: false,
1006
+ error: {
1007
+ code: error instanceof IncurError ? error.code : 'UNKNOWN',
1008
+ message: error instanceof Error ? error.message : String(error),
908
1009
  },
1010
+ meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
909
1011
  });
1012
+ exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
1013
+ return;
910
1014
  }
911
- };
912
- try {
913
- const cliEnv = options.envSchema ? Parser.parseEnv(options.envSchema, envSource) : {};
914
- if (allMiddleware.length > 0) {
915
- const errorFn = (opts) => {
916
- return { [sentinel]: 'error', ...opts };
917
- };
918
- const mwCtx = {
919
- agent: !human,
1015
+ }
1016
+ const result = await Command.execute(command, {
1017
+ agent: !human,
1018
+ argv: rest,
1019
+ defaults,
1020
+ displayName,
1021
+ env: options.envSchema,
1022
+ envSource: options.env,
1023
+ format,
1024
+ formatExplicit,
1025
+ inputOptions: {},
1026
+ middlewares: allMiddleware,
1027
+ name,
1028
+ path,
1029
+ vars: options.vars,
1030
+ version: options.version,
1031
+ });
1032
+ const duration = `${Math.round(performance.now() - start)}ms`;
1033
+ // Streaming path — async generator
1034
+ if ('stream' in result) {
1035
+ await handleStreaming(result.stream, {
1036
+ name: displayName,
1037
+ path,
1038
+ start,
1039
+ format,
1040
+ formatExplicit,
1041
+ human,
1042
+ renderOutput,
1043
+ fullOutput,
1044
+ truncate,
1045
+ write,
1046
+ writeln,
1047
+ exit,
1048
+ });
1049
+ return;
1050
+ }
1051
+ if (result.ok) {
1052
+ const cta = formatCtaBlock(displayName, result.cta);
1053
+ write({
1054
+ ok: true,
1055
+ data: result.data,
1056
+ meta: {
920
1057
  command: path,
921
- env: cliEnv,
922
- error: errorFn,
923
- format,
924
- formatExplicit,
925
- name,
926
- options: rootOptions,
927
- set(key, value) {
928
- varsMap[key] = value;
929
- },
930
- var: varsMap,
931
- version: options.version,
932
- };
933
- const handleMwSentinel = (result) => {
934
- if (!isSentinel(result) || result[sentinel] !== 'error')
935
- return;
936
- const err = result;
937
- const cta = formatCtaBlock(name, err.cta);
938
- write({
939
- ok: false,
940
- error: {
941
- code: err.code,
942
- message: err.message,
943
- ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
944
- },
945
- meta: {
946
- command: path,
947
- duration: `${Math.round(performance.now() - start)}ms`,
948
- ...(cta ? { cta } : undefined),
949
- },
950
- });
951
- exit(err.exitCode ?? 1);
952
- };
953
- const composed = allMiddleware.reduceRight((next, mw) => async () => {
954
- handleMwSentinel(await mw(mwCtx, next));
955
- }, runCommand);
956
- await composed();
957
- }
958
- else {
959
- await runCommand();
960
- }
1058
+ duration,
1059
+ ...(cta ? { cta } : undefined),
1060
+ },
1061
+ });
961
1062
  }
962
- catch (error) {
963
- const errorOutput = {
1063
+ else {
1064
+ const cta = formatCtaBlock(displayName, result.cta);
1065
+ if (human && !formatExplicit && result.error.fieldErrors) {
1066
+ writeln(formatHumanValidationError(displayName, path, command, new ValidationError({
1067
+ message: result.error.message,
1068
+ fieldErrors: result.error.fieldErrors,
1069
+ }), options.env, configFlag));
1070
+ exit(1);
1071
+ return;
1072
+ }
1073
+ write({
964
1074
  ok: false,
965
1075
  error: {
966
- code: error instanceof IncurError
967
- ? error.code
968
- : error instanceof ValidationError
969
- ? 'VALIDATION_ERROR'
970
- : 'UNKNOWN',
971
- message: error instanceof Error ? error.message : String(error),
972
- ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
973
- ...(error instanceof ValidationError ? { fieldErrors: error.fieldErrors } : undefined),
1076
+ code: result.error.code,
1077
+ message: result.error.message,
1078
+ ...(result.error.retryable !== undefined
1079
+ ? { retryable: result.error.retryable }
1080
+ : undefined),
1081
+ ...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
974
1082
  },
975
1083
  meta: {
976
1084
  command: path,
977
- duration: `${Math.round(performance.now() - start)}ms`,
1085
+ duration,
1086
+ ...(cta ? { cta } : undefined),
978
1087
  },
979
- };
980
- if (human && !formatExplicit && error instanceof ValidationError) {
981
- writeln(formatHumanValidationError(name, path, command, error, options.env));
982
- exit(1);
983
- return;
984
- }
985
- write(errorOutput);
986
- exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
1088
+ });
1089
+ exit(result.exitCode ?? 1);
987
1090
  }
988
1091
  }
989
1092
  /** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
990
1093
  function createMcpHttpHandler(name, version) {
991
1094
  let transport;
992
- return async (req, commands) => {
1095
+ return async (req, commands, mcpOptions) => {
993
1096
  if (!transport) {
994
- const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
995
- const { WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js');
1097
+ const { McpServer, WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/server');
996
1098
  const server = new McpServer({ name, version });
997
1099
  for (const tool of Mcp.collectTools(commands, [])) {
998
1100
  const mergedShape = {
@@ -1002,10 +1104,16 @@ function createMcpHttpHandler(name, version) {
1002
1104
  const hasInput = Object.keys(mergedShape).length > 0;
1003
1105
  server.registerTool(tool.name, {
1004
1106
  ...(tool.description ? { description: tool.description } : undefined),
1005
- ...(hasInput ? { inputSchema: mergedShape } : undefined),
1107
+ ...(hasInput ? { inputSchema: z.object(mergedShape) } : undefined),
1006
1108
  }, async (...callArgs) => {
1007
1109
  const params = hasInput ? callArgs[0] : {};
1008
- return Mcp.callTool(tool, params);
1110
+ return Mcp.callTool(tool, params, {
1111
+ name,
1112
+ version,
1113
+ middlewares: mcpOptions?.middlewares,
1114
+ env: mcpOptions?.env,
1115
+ vars: mcpOptions?.vars,
1116
+ });
1009
1117
  });
1010
1118
  }
1011
1119
  transport = new WebStandardStreamableHTTPServerTransport({
@@ -1024,14 +1132,18 @@ async function fetchImpl(name, commands, req, options = {}) {
1024
1132
  const segments = url.pathname.split('/').filter(Boolean);
1025
1133
  // MCP over HTTP: route /mcp to the MCP transport
1026
1134
  if (segments[0] === 'mcp' && segments.length === 1 && options.mcpHandler)
1027
- return options.mcpHandler(req, commands);
1135
+ return options.mcpHandler(req, commands, {
1136
+ middlewares: options.middlewares,
1137
+ env: options.envSchema,
1138
+ vars: options.vars,
1139
+ });
1028
1140
  // .well-known/skills/ — Agent Skills Discovery (RFC)
1029
1141
  if (segments[0] === '.well-known' &&
1030
1142
  segments[1] === 'skills' &&
1031
1143
  segments.length >= 3 &&
1032
1144
  req.method === 'GET') {
1033
1145
  const groups = new Map();
1034
- const cmds = collectSkillCommands(commands, [], groups);
1146
+ const cmds = collectSkillCommands(commands, [], groups, options.rootCommand);
1035
1147
  // GET /.well-known/skills/index.json
1036
1148
  if (segments[2] === 'index.json' && segments.length === 3) {
1037
1149
  const files = Skill.split(name, cmds, 1, groups);
@@ -1093,15 +1205,19 @@ async function fetchImpl(name, commands, req, options = {}) {
1093
1205
  }, 404);
1094
1206
  }
1095
1207
  const resolved = resolveCommand(commands, segments);
1096
- if ('error' in resolved)
1208
+ if ('error' in resolved) {
1209
+ const parent = resolved.path ? `${name} ${resolved.path}` : name;
1210
+ const suggestion = suggest(resolved.error, resolved.commands.keys());
1211
+ const didYouMean = suggestion ? ` Did you mean '${suggestion}'?` : '';
1097
1212
  return jsonResponse({
1098
1213
  ok: false,
1099
1214
  error: {
1100
1215
  code: 'COMMAND_NOT_FOUND',
1101
- message: `'${resolved.error}' is not a command for '${resolved.path ? `${name} ${resolved.path}` : name}'.`,
1216
+ message: `'${resolved.error}' is not a command for '${parent}'.${didYouMean}`,
1102
1217
  },
1103
1218
  meta: { command: resolved.error, duration: `${Math.round(performance.now() - start)}ms` },
1104
1219
  }, 404);
1220
+ }
1105
1221
  if ('help' in resolved)
1106
1222
  return jsonResponse({
1107
1223
  ok: false,
@@ -1114,7 +1230,11 @@ async function fetchImpl(name, commands, req, options = {}) {
1114
1230
  if ('fetchGateway' in resolved)
1115
1231
  return resolved.fetchGateway.fetch(req);
1116
1232
  const { command, path, rest } = resolved;
1117
- return executeCommand(path, command, rest, inputOptions, start, options);
1233
+ const groupMiddlewares = 'middlewares' in resolved ? resolved.middlewares : [];
1234
+ return executeCommand(path, command, rest, inputOptions, start, {
1235
+ ...options,
1236
+ groupMiddlewares,
1237
+ });
1118
1238
  }
1119
1239
  /** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
1120
1240
  async function executeCommand(path, command, rest, inputOptions, start, options) {
@@ -1124,157 +1244,90 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
1124
1244
  headers: { 'content-type': 'application/json' },
1125
1245
  });
1126
1246
  }
1127
- const sentinel_ = Symbol.for('incur.sentinel');
1128
- const varsMap = options.vars ? options.vars.parse({}) : {};
1129
- let response;
1130
- const runCommand = async () => {
1131
- const { args } = Parser.parse(rest, { args: command.args });
1132
- const parsedOptions = command.options ? command.options.parse(inputOptions) : {};
1133
- const okFn = (data) => ({ [sentinel_]: 'ok', data });
1134
- const errorFn = (opts) => ({ [sentinel_]: 'error', ...opts });
1135
- const result = command.run({
1136
- agent: true,
1137
- args,
1138
- env: {},
1139
- error: errorFn,
1140
- format: 'json',
1141
- formatExplicit: true,
1142
- name: path,
1143
- ok: okFn,
1144
- options: parsedOptions,
1145
- var: varsMap,
1146
- version: undefined,
1147
- });
1148
- // Streaming path async generator → NDJSON response
1149
- if (isAsyncGenerator(result)) {
1150
- const stream = new ReadableStream({
1151
- async start(controller) {
1152
- const encoder = new TextEncoder();
1153
- try {
1154
- let returnValue;
1155
- while (true) {
1156
- const { value, done } = await result.next();
1157
- if (done) {
1158
- returnValue = value;
1159
- break;
1160
- }
1161
- if (isSentinel(value) && value[sentinel] === 'error') {
1162
- const tagged = value;
1163
- controller.enqueue(encoder.encode(JSON.stringify({
1164
- type: 'error',
1165
- ok: false,
1166
- error: { code: tagged.code, message: tagged.message },
1167
- }) + '\n'));
1168
- controller.close();
1169
- return;
1170
- }
1171
- controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'));
1172
- }
1173
- const meta = { command: path };
1174
- if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
1175
- const tagged = returnValue;
1176
- controller.enqueue(encoder.encode(JSON.stringify({
1177
- type: 'error',
1178
- ok: false,
1179
- error: { code: tagged.code, message: tagged.message },
1180
- }) + '\n'));
1181
- }
1182
- else {
1183
- controller.enqueue(encoder.encode(JSON.stringify({ type: 'done', ok: true, meta }) + '\n'));
1184
- }
1185
- }
1186
- catch (error) {
1187
- controller.enqueue(encoder.encode(JSON.stringify({
1188
- type: 'error',
1189
- ok: false,
1190
- error: {
1191
- code: 'UNKNOWN',
1192
- message: error instanceof Error ? error.message : String(error),
1193
- },
1194
- }) + '\n'));
1247
+ const allMiddleware = [
1248
+ ...(options.middlewares ?? []),
1249
+ ...(options.groupMiddlewares ?? []),
1250
+ ...(command.middleware ?? []),
1251
+ ];
1252
+ const result = await Command.execute(command, {
1253
+ agent: true,
1254
+ argv: rest,
1255
+ env: options.envSchema,
1256
+ format: 'json',
1257
+ formatExplicit: true,
1258
+ inputOptions,
1259
+ middlewares: allMiddleware,
1260
+ name: options.name ?? path,
1261
+ parseMode: 'split',
1262
+ path,
1263
+ vars: options.vars,
1264
+ version: options.version,
1265
+ });
1266
+ const duration = `${Math.round(performance.now() - start)}ms`;
1267
+ // Streaming path — async generator → NDJSON response
1268
+ if ('stream' in result) {
1269
+ const stream = new ReadableStream({
1270
+ async start(controller) {
1271
+ const encoder = new TextEncoder();
1272
+ try {
1273
+ for await (const value of result.stream) {
1274
+ controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'));
1195
1275
  }
1196
- controller.close();
1197
- },
1198
- });
1199
- response = new Response(stream, {
1200
- status: 200,
1201
- headers: { 'content-type': 'application/x-ndjson' },
1202
- });
1203
- return;
1204
- }
1205
- const awaited = await result;
1206
- const duration = `${Math.round(performance.now() - start)}ms`;
1207
- if (typeof awaited === 'object' && awaited !== null && sentinel_ in awaited) {
1208
- const tagged = awaited;
1209
- if (tagged[sentinel_] === 'error')
1210
- response = jsonResponse({
1211
- ok: false,
1212
- error: { code: tagged.code, message: tagged.message },
1213
- meta: { command: path, duration },
1214
- }, 500);
1215
- else
1216
- response = jsonResponse({ ok: true, data: tagged.data, meta: { command: path, duration } }, 200);
1217
- return;
1218
- }
1219
- response = jsonResponse({ ok: true, data: awaited, meta: { command: path, duration } }, 200);
1220
- };
1221
- try {
1222
- const allMiddleware = options.middlewares ?? [];
1223
- if (allMiddleware.length > 0) {
1224
- const errorFn = (opts) => {
1225
- const duration = `${Math.round(performance.now() - start)}ms`;
1226
- response = jsonResponse({
1227
- ok: false,
1228
- error: { code: opts.code, message: opts.message },
1229
- meta: { command: path, duration },
1230
- }, 500);
1231
- return undefined;
1232
- };
1233
- const mwCtx = {
1234
- agent: true,
1235
- command: path,
1236
- env: {},
1237
- error: errorFn,
1238
- format: 'json',
1239
- formatExplicit: true,
1240
- name: path,
1241
- options: {},
1242
- set(key, value) {
1243
- varsMap[key] = value;
1244
- },
1245
- var: varsMap,
1246
- version: undefined,
1247
- };
1248
- const composed = allMiddleware.reduceRight((next, mw) => async () => {
1249
- await mw(mwCtx, next);
1250
- }, runCommand);
1251
- await composed();
1252
- }
1253
- else {
1254
- await runCommand();
1255
- }
1276
+ controller.enqueue(encoder.encode(JSON.stringify({
1277
+ type: 'done',
1278
+ ok: true,
1279
+ meta: { command: path },
1280
+ }) + '\n'));
1281
+ }
1282
+ catch (error) {
1283
+ controller.enqueue(encoder.encode(JSON.stringify({
1284
+ type: 'error',
1285
+ ok: false,
1286
+ error: {
1287
+ code: 'UNKNOWN',
1288
+ message: error instanceof Error ? error.message : String(error),
1289
+ },
1290
+ }) + '\n'));
1291
+ }
1292
+ controller.close();
1293
+ },
1294
+ });
1295
+ return new Response(stream, {
1296
+ status: 200,
1297
+ headers: { 'content-type': 'application/x-ndjson' },
1298
+ });
1256
1299
  }
1257
- catch (error) {
1258
- const duration = `${Math.round(performance.now() - start)}ms`;
1259
- if (error instanceof ValidationError)
1260
- return jsonResponse({
1261
- ok: false,
1262
- error: { code: 'VALIDATION_ERROR', message: error.message },
1263
- meta: { command: path, duration },
1264
- }, 400);
1300
+ if (!result.ok) {
1301
+ const cta = formatCtaBlock(options.name ?? path, result.cta);
1265
1302
  return jsonResponse({
1266
1303
  ok: false,
1267
1304
  error: {
1268
- code: error instanceof IncurError ? error.code : 'UNKNOWN',
1269
- message: error instanceof Error ? error.message : String(error),
1305
+ code: result.error.code,
1306
+ message: result.error.message,
1307
+ ...(result.error.retryable !== undefined
1308
+ ? { retryable: result.error.retryable }
1309
+ : undefined),
1310
+ },
1311
+ meta: {
1312
+ command: path,
1313
+ duration,
1314
+ ...(cta ? { cta } : undefined),
1270
1315
  },
1271
- meta: { command: path, duration },
1272
- }, 500);
1316
+ }, result.error.code === 'VALIDATION_ERROR' ? 400 : 500);
1273
1317
  }
1274
- return response;
1318
+ const cta = formatCtaBlock(options.name ?? path, result.cta);
1319
+ return jsonResponse({
1320
+ ok: true,
1321
+ data: result.data,
1322
+ meta: {
1323
+ command: path,
1324
+ duration,
1325
+ ...(cta ? { cta } : undefined),
1326
+ },
1327
+ }, 200);
1275
1328
  }
1276
1329
  /** @internal Formats a validation error for TTY with usage hint. */
1277
- function formatHumanValidationError(cli, path, command, error, envSource) {
1330
+ function formatHumanValidationError(cli, path, command, error, envSource, configFlag) {
1278
1331
  const lines = [];
1279
1332
  for (const fe of error.fieldErrors)
1280
1333
  lines.push(`Error: missing required argument <${fe.path}>`);
@@ -1282,6 +1335,7 @@ function formatHumanValidationError(cli, path, command, error, envSource) {
1282
1335
  lines.push('');
1283
1336
  lines.push(Help.formatCommand(path === cli ? cli : `${cli} ${path}`, {
1284
1337
  alias: command.alias,
1338
+ configFlag,
1285
1339
  description: command.description,
1286
1340
  args: command.args,
1287
1341
  env: command.env,
@@ -1297,8 +1351,8 @@ function formatHumanValidationError(cli, path, command, error, envSource) {
1297
1351
  function resolveCommand(commands, tokens) {
1298
1352
  const [first, ...rest] = tokens;
1299
1353
  if (!first || !commands.has(first))
1300
- return { error: first ?? '(none)', path: '' };
1301
- let entry = commands.get(first);
1354
+ return { error: first ?? '(none)', path: '', commands, rest };
1355
+ let entry = resolveAlias(commands, commands.get(first));
1302
1356
  const path = [first];
1303
1357
  let remaining = rest;
1304
1358
  let inheritedOutputPolicy;
@@ -1327,10 +1381,16 @@ function resolveCommand(commands, tokens) {
1327
1381
  description: entry.description,
1328
1382
  commands: entry.commands,
1329
1383
  };
1330
- const child = entry.commands.get(next);
1331
- if (!child) {
1332
- return { error: next, path: path.join(' ') };
1384
+ const rawChild = entry.commands.get(next);
1385
+ if (!rawChild) {
1386
+ return {
1387
+ error: next,
1388
+ path: path.join(' '),
1389
+ commands: entry.commands,
1390
+ rest: remaining.slice(1),
1391
+ };
1333
1392
  }
1393
+ let child = resolveAlias(entry.commands, rawChild);
1334
1394
  path.push(next);
1335
1395
  remaining = remaining.slice(1);
1336
1396
  entry = child;
@@ -1354,9 +1414,10 @@ function resolveCommand(commands, tokens) {
1354
1414
  ...(outputPolicy ? { outputPolicy } : undefined),
1355
1415
  };
1356
1416
  }
1357
- /** @internal Extracts built-in flags (--verbose, --format, --json, --llms, --help, --version) from argv. */
1358
- function extractBuiltinFlags(argv) {
1359
- let verbose = false;
1417
+ /** @internal Extracts built-in flags (--full-output, --format, --json, --llms, --help, --version) from argv. */
1418
+ const validFormats = new Set(['toon', 'json', 'yaml', 'md', 'jsonl']);
1419
+ function extractBuiltinFlags(argv, options = {}) {
1420
+ let fullOutput = false;
1360
1421
  let llms = false;
1361
1422
  let llmsFull = false;
1362
1423
  let mcp = false;
@@ -1365,15 +1426,20 @@ function extractBuiltinFlags(argv) {
1365
1426
  let schema = false;
1366
1427
  let format = 'toon';
1367
1428
  let formatExplicit = false;
1429
+ let configPath;
1430
+ let configDisabled = false;
1368
1431
  let filterOutput;
1369
1432
  let tokenLimit;
1370
1433
  let tokenOffset;
1371
1434
  let tokenCount = false;
1372
1435
  const rest = [];
1436
+ const cfgFlag = options.configFlag ? `--${options.configFlag}` : undefined;
1437
+ const cfgFlagEq = options.configFlag ? `--${options.configFlag}=` : undefined;
1438
+ const noCfgFlag = options.configFlag ? `--no-${options.configFlag}` : undefined;
1373
1439
  for (let i = 0; i < argv.length; i++) {
1374
1440
  const token = argv[i];
1375
- if (token === '--verbose')
1376
- verbose = true;
1441
+ if (token === '--full-output')
1442
+ fullOutput = true;
1377
1443
  else if (token === '--llms')
1378
1444
  llms = true;
1379
1445
  else if (token === '--llms-full')
@@ -1391,20 +1457,49 @@ function extractBuiltinFlags(argv) {
1391
1457
  formatExplicit = true;
1392
1458
  }
1393
1459
  else if (token === '--format' && argv[i + 1]) {
1460
+ if (!validFormats.has(argv[i + 1]))
1461
+ throw new ParseError({
1462
+ message: `Invalid format: "${argv[i + 1]}". Expected one of: ${[...validFormats].join(', ')}`,
1463
+ });
1394
1464
  format = argv[i + 1];
1395
1465
  formatExplicit = true;
1396
1466
  i++;
1397
1467
  }
1468
+ else if (cfgFlag && token === cfgFlag) {
1469
+ const value = argv[i + 1];
1470
+ if (value === undefined)
1471
+ throw new ParseError({ message: `Missing value for flag: ${cfgFlag}` });
1472
+ configPath = value;
1473
+ configDisabled = false;
1474
+ i++;
1475
+ }
1476
+ else if (cfgFlagEq && token.startsWith(cfgFlagEq)) {
1477
+ const value = token.slice(cfgFlagEq.length);
1478
+ if (value.length === 0)
1479
+ throw new ParseError({ message: `Missing value for flag: ${cfgFlag}` });
1480
+ configPath = value;
1481
+ configDisabled = false;
1482
+ }
1483
+ else if (noCfgFlag && token === noCfgFlag) {
1484
+ configPath = undefined;
1485
+ configDisabled = true;
1486
+ }
1398
1487
  else if (token === '--filter-output' && argv[i + 1]) {
1399
1488
  filterOutput = argv[i + 1];
1400
1489
  i++;
1401
1490
  }
1402
1491
  else if (token === '--token-limit' && argv[i + 1]) {
1403
- tokenLimit = Number(argv[i + 1]);
1492
+ const n = Number(argv[i + 1]);
1493
+ if (!Number.isFinite(n) || argv[i + 1].trim() === '')
1494
+ throw new ParseError({ message: `Invalid value for --token-limit: "${argv[i + 1]}"` });
1495
+ tokenLimit = n;
1404
1496
  i++;
1405
1497
  }
1406
1498
  else if (token === '--token-offset' && argv[i + 1]) {
1407
- tokenOffset = Number(argv[i + 1]);
1499
+ const n = Number(argv[i + 1]);
1500
+ if (!Number.isFinite(n) || argv[i + 1].trim() === '')
1501
+ throw new ParseError({ message: `Invalid value for --token-offset: "${argv[i + 1]}"` });
1502
+ tokenOffset = n;
1408
1503
  i++;
1409
1504
  }
1410
1505
  else if (token === '--token-count')
@@ -1413,9 +1508,11 @@ function extractBuiltinFlags(argv) {
1413
1508
  rest.push(token);
1414
1509
  }
1415
1510
  return {
1416
- verbose,
1511
+ fullOutput,
1417
1512
  format,
1418
1513
  formatExplicit,
1514
+ configPath,
1515
+ configDisabled,
1419
1516
  filterOutput,
1420
1517
  tokenLimit,
1421
1518
  tokenOffset,
@@ -1429,14 +1526,156 @@ function extractBuiltinFlags(argv) {
1429
1526
  rest,
1430
1527
  };
1431
1528
  }
1529
+ /** @internal Loads config-backed option defaults for the active command. */
1530
+ async function loadCommandOptionDefaults(cli, path, options = {}) {
1531
+ if (options.configDisabled)
1532
+ return undefined;
1533
+ const { loader } = options;
1534
+ // Resolve the target file path
1535
+ let targetPath;
1536
+ if (options.configPath) {
1537
+ targetPath = resolveConfigPath(options.configPath);
1538
+ }
1539
+ else {
1540
+ const searchPaths = options.files ?? [`${cli}.json`];
1541
+ targetPath = await findFirstExisting(searchPaths);
1542
+ }
1543
+ // Load and parse the config
1544
+ let parsed;
1545
+ if (loader) {
1546
+ const result = await loader(targetPath);
1547
+ if (result === undefined)
1548
+ return undefined;
1549
+ if (!isRecord(result))
1550
+ throw new ParseError({ message: 'Config loader must return a plain object or undefined' });
1551
+ parsed = result;
1552
+ }
1553
+ else {
1554
+ if (!targetPath)
1555
+ return undefined;
1556
+ const result = await readJsonConfig(targetPath, !!options.configPath);
1557
+ if (!result)
1558
+ return undefined;
1559
+ parsed = result;
1560
+ }
1561
+ // Extract the command section from the config tree
1562
+ return extractCommandSection(parsed, cli, path);
1563
+ }
1564
+ /** @internal Resolves a config file path, expanding `~` to home dir. */
1565
+ function resolveConfigPath(filePath) {
1566
+ if (filePath.startsWith('~/') || filePath === '~') {
1567
+ return path.join(os.homedir(), filePath.slice(1));
1568
+ }
1569
+ return path.resolve(process.cwd(), filePath);
1570
+ }
1571
+ /** @internal Returns the first readable file from a list of paths, or `undefined`. */
1572
+ async function findFirstExisting(paths) {
1573
+ for (const p of paths) {
1574
+ const resolved = resolveConfigPath(p);
1575
+ try {
1576
+ await fs.access(resolved, fs.constants.R_OK);
1577
+ return resolved;
1578
+ }
1579
+ catch { }
1580
+ }
1581
+ return undefined;
1582
+ }
1583
+ /** @internal Reads and parses a JSON config file. */
1584
+ async function readJsonConfig(targetPath, explicit) {
1585
+ let raw;
1586
+ try {
1587
+ raw = await fs.readFile(targetPath, 'utf8');
1588
+ }
1589
+ catch (error) {
1590
+ if (error.code === 'ENOENT') {
1591
+ if (explicit)
1592
+ throw new ParseError({ message: `Config file not found: ${targetPath}` });
1593
+ return undefined;
1594
+ }
1595
+ throw error;
1596
+ }
1597
+ let parsed;
1598
+ try {
1599
+ parsed = JSON.parse(raw);
1600
+ }
1601
+ catch (error) {
1602
+ throw new ParseError({
1603
+ message: `Invalid JSON config file: ${targetPath}`,
1604
+ cause: error instanceof Error ? error : undefined,
1605
+ });
1606
+ }
1607
+ if (!isRecord(parsed))
1608
+ throw new ParseError({
1609
+ message: `Invalid config file: expected a top-level object in ${targetPath}`,
1610
+ });
1611
+ return parsed;
1612
+ }
1613
+ /** @internal Walks the nested config tree to extract option defaults for a command path. */
1614
+ function extractCommandSection(parsed, cli, path) {
1615
+ const segments = path === cli ? [] : path.split(' ');
1616
+ let node = parsed;
1617
+ for (const seg of segments) {
1618
+ if (!isRecord(node))
1619
+ return undefined;
1620
+ const commands = node.commands;
1621
+ if (!isRecord(commands))
1622
+ return undefined;
1623
+ node = commands[seg];
1624
+ if (node === undefined)
1625
+ return undefined;
1626
+ }
1627
+ if (!isRecord(node))
1628
+ throw new ParseError({
1629
+ message: `Invalid config section for '${path}': expected an object`,
1630
+ });
1631
+ const options = node.options;
1632
+ if (options === undefined)
1633
+ return undefined;
1634
+ if (!isRecord(options))
1635
+ throw new ParseError({
1636
+ message: `Invalid config 'options' for '${path}': expected an object`,
1637
+ });
1638
+ return Object.keys(options).length > 0 ? options : undefined;
1639
+ }
1432
1640
  /** @internal Collects immediate child commands/groups for help output. */
1433
1641
  function collectHelpCommands(commands) {
1434
1642
  const result = [];
1435
1643
  for (const [name, entry] of commands) {
1644
+ if (isAlias(entry))
1645
+ continue;
1436
1646
  result.push({ name, description: entry.description });
1437
1647
  }
1438
1648
  return result.sort((a, b) => a.name.localeCompare(b.name));
1439
1649
  }
1650
+ /** @internal Finds the index of a builtin command token in the filtered argv. Returns -1 if not found. */
1651
+ function builtinIdx(filtered, cliName, builtin) {
1652
+ // e.g. `skills add` or `skill add`
1653
+ if (findBuiltin(filtered[0])?.name === builtin)
1654
+ return 0;
1655
+ // e.g. `my-cli skills add`
1656
+ if (filtered[0] === cliName && findBuiltin(filtered[1])?.name === builtin)
1657
+ return 1;
1658
+ // not a match
1659
+ return -1;
1660
+ }
1661
+ /** @internal Formats group-level help for a built-in command (e.g. `cli skills`). */
1662
+ function formatBuiltinHelp(cli, builtin) {
1663
+ return Help.formatRoot(`${cli} ${builtin.name}`, {
1664
+ aliases: builtin.aliases,
1665
+ description: builtin.description,
1666
+ commands: builtin.subcommands?.map((s) => ({ name: s.name, description: s.description })),
1667
+ });
1668
+ }
1669
+ /** @internal Formats subcommand-level help for a built-in command (e.g. `cli skills add --help`). */
1670
+ function formatBuiltinSubcommandHelp(cli, builtin, subName) {
1671
+ const sub = builtin.subcommands?.find((s) => s.name === subName);
1672
+ return Help.formatCommand(`${cli} ${builtin.name} ${subName}`, {
1673
+ alias: sub?.alias,
1674
+ description: sub?.description,
1675
+ hideGlobalOptions: true,
1676
+ options: sub?.options,
1677
+ });
1678
+ }
1440
1679
  /** @internal Formats help text for a fetch gateway command. */
1441
1680
  function formatFetchHelp(name, description) {
1442
1681
  const lines = [];
@@ -1465,14 +1704,30 @@ function isGroup(entry) {
1465
1704
  function isFetchGateway(entry) {
1466
1705
  return '_fetch' in entry;
1467
1706
  }
1707
+ /** @internal Type guard for alias entries. */
1708
+ function isAlias(entry) {
1709
+ return '_alias' in entry;
1710
+ }
1711
+ /** @internal Follows an alias entry to its canonical target. Returns the entry unchanged if not an alias. */
1712
+ function resolveAlias(commands, entry) {
1713
+ if (isAlias(entry))
1714
+ return commands.get(entry.target);
1715
+ return entry;
1716
+ }
1468
1717
  /** @internal Maps CLI instances to their command maps. */
1469
1718
  export const toCommands = new WeakMap();
1470
1719
  /** @internal Maps CLI instances to their middleware arrays. */
1471
1720
  const toMiddlewares = new WeakMap();
1472
1721
  /** @internal Maps root CLI instances to their command definitions. */
1473
- const toRootDefinition = new WeakMap();
1722
+ export const toRootDefinition = new WeakMap();
1723
+ /** @internal Maps CLI instances to their root options schema. */
1724
+ export const toRootOptions = new WeakMap();
1725
+ /** @internal Maps CLI instances to whether config file loading is enabled. */
1726
+ export const toConfigEnabled = new WeakMap();
1474
1727
  /** @internal Maps CLI instances to their output policy. */
1475
1728
  const toOutputPolicy = new WeakMap();
1729
+ /** @internal Maps root CLI instances to their command aliases. */
1730
+ const toRootAliases = new WeakMap();
1476
1731
  /** @internal Sentinel symbol for `ok()` and `error()` return values. */
1477
1732
  const sentinel = Symbol.for('incur.sentinel');
1478
1733
  /** @internal Formats an error for human-readable TTY output. */
@@ -1489,8 +1744,9 @@ function formatHumanError(error) {
1489
1744
  /** @internal Formats a CTA block for human-readable TTY output. */
1490
1745
  function formatHumanCta(cta) {
1491
1746
  const lines = ['', cta.description];
1747
+ const maxLen = Math.max(...cta.commands.map((c) => c.command.length));
1492
1748
  for (const c of cta.commands) {
1493
- const desc = c.description ? ` # ${c.description}` : '';
1749
+ const desc = c.description ? ` ${''.padEnd(maxLen - c.command.length)}# ${c.description}` : '';
1494
1750
  lines.push(` ${c.command}${desc}`);
1495
1751
  }
1496
1752
  return lines.join('\n');
@@ -1502,13 +1758,6 @@ function hasRequiredArgs(args) {
1502
1758
  function isSentinel(value) {
1503
1759
  return typeof value === 'object' && value !== null && sentinel in value;
1504
1760
  }
1505
- /** @internal Type guard for async generators returned by streaming `run` handlers. */
1506
- function isAsyncGenerator(value) {
1507
- return (typeof value === 'object' &&
1508
- value !== null &&
1509
- Symbol.asyncIterator in value &&
1510
- typeof value.next === 'function');
1511
- }
1512
1761
  /** @internal Handles streaming output from an async generator `run` handler. */
1513
1762
  async function handleStreaming(generator, ctx) {
1514
1763
  // Incremental: no explicit format (default toon), or explicit jsonl
@@ -1686,7 +1935,8 @@ function formatCtaBlock(name, block) {
1686
1935
  if (!block || block.commands.length === 0)
1687
1936
  return undefined;
1688
1937
  return {
1689
- description: block.description ?? 'Suggested commands:',
1938
+ description: block.description ??
1939
+ (block.commands.length === 1 ? 'Suggested command:' : 'Suggested commands:'),
1690
1940
  commands: block.commands.map((c) => formatCta(name, c)),
1691
1941
  };
1692
1942
  }
@@ -1715,6 +1965,8 @@ function buildIndexManifest(commands, prefix = []) {
1715
1965
  function collectIndexCommands(commands, prefix) {
1716
1966
  const result = [];
1717
1967
  for (const [name, entry] of commands) {
1968
+ if (isAlias(entry))
1969
+ continue;
1718
1970
  const path = [...prefix, name];
1719
1971
  if (isGroup(entry)) {
1720
1972
  result.push(...collectIndexCommands(entry.commands, path));
@@ -1743,6 +1995,8 @@ function buildManifest(commands, prefix = []) {
1743
1995
  function collectCommands(commands, prefix) {
1744
1996
  const result = [];
1745
1997
  for (const [name, entry] of commands) {
1998
+ if (isAlias(entry))
1999
+ continue;
1746
2000
  const path = [...prefix, name];
1747
2001
  if (isFetchGateway(entry)) {
1748
2002
  const cmd = { name: path.join(' ') };
@@ -1784,9 +2038,30 @@ function collectCommands(commands, prefix) {
1784
2038
  return result;
1785
2039
  }
1786
2040
  /** @internal Recursively collects leaf commands as `Skill.CommandInfo` for `--llms --format md`. */
1787
- function collectSkillCommands(commands, prefix, groups) {
2041
+ function collectSkillCommands(commands, prefix, groups, rootCommand) {
1788
2042
  const result = [];
2043
+ if (rootCommand) {
2044
+ const cmd = {};
2045
+ if (rootCommand.description)
2046
+ cmd.description = rootCommand.description;
2047
+ if (rootCommand.args)
2048
+ cmd.args = rootCommand.args;
2049
+ if (rootCommand.env)
2050
+ cmd.env = rootCommand.env;
2051
+ if (rootCommand.hint)
2052
+ cmd.hint = rootCommand.hint;
2053
+ if (rootCommand.options)
2054
+ cmd.options = rootCommand.options;
2055
+ if (rootCommand.output)
2056
+ cmd.output = rootCommand.output;
2057
+ const examples = formatExamples(rootCommand.examples);
2058
+ if (examples)
2059
+ cmd.examples = examples;
2060
+ result.push(cmd);
2061
+ }
1789
2062
  for (const [name, entry] of commands) {
2063
+ if (isAlias(entry))
2064
+ continue;
1790
2065
  const path = [...prefix, name];
1791
2066
  if (isFetchGateway(entry)) {
1792
2067
  const cmd = { name: path.join(' ') };
@@ -1825,7 +2100,7 @@ function collectSkillCommands(commands, prefix, groups) {
1825
2100
  result.push(cmd);
1826
2101
  }
1827
2102
  }
1828
- return result.sort((a, b) => a.name.localeCompare(b.name));
2103
+ return result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
1829
2104
  }
1830
2105
  /** @internal Formats examples into `{ command, description }` objects. `command` is the args/options suffix only. */
1831
2106
  export function formatExamples(examples) {
@@ -1890,4 +2165,16 @@ function emitDeprecationWarnings(argv, optionsSchema, alias) {
1890
2165
  }
1891
2166
  }
1892
2167
  }
2168
+ /** @internal Resolves the display name from `process.argv[1]` basename. Returns the basename if it matches `name` or one of the `aliases`, otherwise falls back to `name`. */
2169
+ function resolveDisplayName(name, aliases) {
2170
+ const bin = process.argv[1];
2171
+ if (!bin)
2172
+ return name;
2173
+ const basename = path.basename(bin);
2174
+ if (basename === name)
2175
+ return name;
2176
+ if (aliases?.includes(basename))
2177
+ return basename;
2178
+ return name;
2179
+ }
1893
2180
  //# sourceMappingURL=Cli.js.map