incur 0.3.3 → 0.3.5

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/src/Cli.ts CHANGED
@@ -8,6 +8,8 @@ import * as Fetch from './Fetch.js'
8
8
  import * as Filter from './Filter.js'
9
9
  import * as Formatter from './Formatter.js'
10
10
  import * as Help from './Help.js'
11
+ import { builtinCommands, type CommandMeta, type Shell, shells } from './internal/command.js'
12
+ import * as Command from './internal/command.js'
11
13
  import { detectRunner } from './internal/pm.js'
12
14
  import type { OneOf } from './internal/types.js'
13
15
  import * as Mcp from './Mcp.js'
@@ -257,10 +259,13 @@ export function create(
257
259
  async fetch(req: Request) {
258
260
  if (pending.length > 0) await Promise.all(pending)
259
261
  return fetchImpl(name, commands, req, {
262
+ envSchema: def.env,
260
263
  mcpHandler,
261
264
  middlewares,
265
+ name,
262
266
  rootCommand: rootDef,
263
267
  vars: def.vars,
268
+ version: def.version,
264
269
  })
265
270
  },
266
271
 
@@ -442,12 +447,17 @@ async function serveImpl(
442
447
 
443
448
  // --mcp: start as MCP stdio server
444
449
  if (mcpFlag) {
445
- await Mcp.serve(name, options.version ?? '0.0.0', commands)
450
+ await Mcp.serve(name, options.version ?? '0.0.0', commands, {
451
+ middlewares: options.middlewares,
452
+ env: options.envSchema,
453
+ vars: options.vars,
454
+ version: options.version,
455
+ })
446
456
  return
447
457
  }
448
458
 
449
459
  // COMPLETE: dynamic shell completions (called by shell hook at tab-press)
450
- const completeShell = process.env.COMPLETE as Completions.Shell | undefined
460
+ const completeShell = process.env.COMPLETE as Shell | undefined
451
461
  if (completeShell) {
452
462
  // Remove separator `--` from argv
453
463
  const sepIdx = argv.indexOf('--')
@@ -459,6 +469,26 @@ async function serveImpl(
459
469
  } else {
460
470
  const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1)
461
471
  const candidates = Completions.complete(commands, options.rootCommand, words, index)
472
+ // Add built-in commands (completions, mcp, skills) to completions
473
+ const current = words[index] ?? ''
474
+ const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'))
475
+ if (nonFlags.length <= 1) {
476
+ for (const b of builtinCommands) {
477
+ if (b.name.startsWith(current) && !candidates.some((c) => c.value === b.name))
478
+ candidates.push({
479
+ value: b.name,
480
+ description: b.description,
481
+ ...(b.subcommands ? { noSpace: true } : undefined),
482
+ })
483
+ }
484
+ } else if (nonFlags.length === 2) {
485
+ const parent = nonFlags[nonFlags.length - 1]
486
+ const builtin = builtinCommands.find((b) => b.name === parent && b.subcommands)
487
+ if (builtin?.subcommands)
488
+ for (const sub of builtin.subcommands)
489
+ if (sub.name.startsWith(current))
490
+ candidates.push({ value: sub.name, description: sub.description })
491
+ }
462
492
  const out = Completions.format(completeShell, candidates)
463
493
  if (out) stdout(out)
464
494
  }
@@ -544,73 +574,48 @@ async function serveImpl(
544
574
  // not a completions invocation
545
575
  return -1
546
576
  })()
577
+ // TODO: refactor built-in command handlers (completions, skills, mcp) into a generic dispatch loop on `builtinCommands`
547
578
  if (completionsIdx !== -1 && filtered[completionsIdx] === 'completions') {
548
- if (help) {
579
+ const shell = filtered[completionsIdx + 1]
580
+ if (help || !shell) {
581
+ const b = builtinCommands.find((c) => c.name === 'completions')!
549
582
  writeln(
550
- [
551
- `${name} completions — Generate shell completion script`,
552
- '',
553
- `Usage: ${name} completions <shell>`,
554
- '',
555
- 'Shells:',
556
- ' bash',
557
- ' fish',
558
- ' nushell',
559
- ' zsh',
560
- '',
561
- 'Setup:',
562
- ...(() => {
563
- const rows = [
564
- ['bash', `eval "$(${name} completions bash)"`, '# add to ~/.bashrc'],
565
- ['zsh', `eval "$(${name} completions zsh)"`, '# add to ~/.zshrc'],
566
- ['fish', `${name} completions fish | source`, '# add to ~/.config/fish/config.fish'],
567
- ['nushell', `see \`${name} completions nushell\``, '# add to config.nu'],
568
- ]
569
- const shellW = Math.max(...rows.map((r) => r[0]!.length))
570
- const cmdW = Math.max(...rows.map((r) => r[1]!.length))
571
- return rows.map(
572
- ([shell, cmd, comment]) =>
573
- ` ${shell!.padEnd(shellW)} ${cmd!.padEnd(cmdW)} ${comment}`,
574
- )
575
- })(),
576
- ].join('\n'),
583
+ Help.formatCommand(`${name} completions`, {
584
+ args: b.args,
585
+ description: b.description,
586
+ hideGlobalOptions: true,
587
+ hint: b.hint?.(name),
588
+ }),
577
589
  )
578
590
  return
579
591
  }
580
- const shell = filtered[completionsIdx + 1]
581
- if (!shell || !['bash', 'fish', 'nushell', 'zsh'].includes(shell)) {
592
+ if (!shells.includes(shell as any)) {
582
593
  writeln(
583
594
  formatHumanError({
584
595
  code: 'INVALID_SHELL',
585
- message: shell
586
- ? `Unknown shell '${shell}'. Supported: bash, fish, nushell, zsh`
587
- : `Missing shell argument. Usage: ${name} completions <bash|fish|nushell|zsh>`,
596
+ message: `Unknown shell '${shell}'. Supported: ${shells.join(', ')}`,
588
597
  }),
589
598
  )
590
599
  exit(1)
591
600
  return
592
601
  }
593
602
  const names = [name, ...(options.aliases ?? [])]
594
- writeln(names.map((n) => Completions.register(shell as Completions.Shell, n)).join('\n'))
603
+ writeln(names.map((n) => Completions.register(shell as Shell, n)).join('\n'))
595
604
  return
596
605
  }
597
606
 
598
607
  // skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
599
608
  const skillsIdx =
600
609
  filtered[0] === 'skills' ? 0 : filtered[0] === name && filtered[1] === 'skills' ? 1 : -1
601
- if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills' && filtered[skillsIdx + 1] === 'add') {
610
+ if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills') {
611
+ if (filtered[skillsIdx + 1] !== 'add') {
612
+ const b = builtinCommands.find((c) => c.name === 'skills')!
613
+ writeln(formatBuiltinHelp(name, b))
614
+ return
615
+ }
602
616
  if (help) {
603
- writeln(
604
- [
605
- `${name} skills add — Sync skill files to your agent`,
606
- '',
607
- `Usage: ${name} skills add [options]`,
608
- '',
609
- 'Options:',
610
- ' --depth <number> Grouping depth for skill files (default: 1)',
611
- ' --no-global Install to project instead of globally',
612
- ].join('\n'),
613
- )
617
+ const b = builtinCommands.find((c) => c.name === 'skills')!
618
+ writeln(formatBuiltinSubcommandHelp(name, b, 'add'))
614
619
  return
615
620
  }
616
621
  const rest = filtered.slice(skillsIdx + 2)
@@ -674,20 +679,15 @@ async function serveImpl(
674
679
 
675
680
  // mcp add: register CLI as MCP server via `npx add-mcp`
676
681
  const mcpIdx = filtered[0] === 'mcp' ? 0 : filtered[0] === name && filtered[1] === 'mcp' ? 1 : -1
677
- if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp' && filtered[mcpIdx + 1] === 'add') {
682
+ if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp') {
683
+ if (filtered[mcpIdx + 1] !== 'add') {
684
+ const b = builtinCommands.find((c) => c.name === 'mcp')!
685
+ writeln(formatBuiltinHelp(name, b))
686
+ return
687
+ }
678
688
  if (help) {
679
- writeln(
680
- [
681
- `${name} mcp add — Register as an MCP server for your agent`,
682
- '',
683
- `Usage: ${name} mcp add [options]`,
684
- '',
685
- 'Options:',
686
- ' -c, --command <cmd> Override the command agents will run (e.g. "pnpm my-cli --mcp")',
687
- ' --no-global Install to project instead of globally',
688
- ' --agent <agent> Target a specific agent (e.g. claude-code, cursor)',
689
- ].join('\n'),
690
- )
689
+ const b = builtinCommands.find((c) => c.name === 'mcp')!
690
+ writeln(formatBuiltinSubcommandHelp(name, b, 'add'))
691
691
  return
692
692
  }
693
693
  const rest = filtered.slice(mcpIdx + 2)
@@ -1198,211 +1198,125 @@ async function serveImpl(
1198
1198
  ...((command.middleware as MiddlewareHandler[] | undefined) ?? []),
1199
1199
  ]
1200
1200
 
1201
- // Initialize vars from schema defaults
1202
- const varsMap: Record<string, unknown> = options.vars ? options.vars.parse({}) : {}
1203
- const envSource = options.env ?? process.env
1204
-
1205
- const runCommand = async () => {
1206
- const { args, options: parsedOptions } = Parser.parse(rest, {
1207
- alias: command.alias as Record<string, string> | undefined,
1208
- args: command.args,
1209
- options: command.options,
1210
- })
1211
-
1212
- if (human)
1213
- emitDeprecationWarnings(
1214
- rest,
1215
- command.options,
1216
- command.alias as Record<string, string> | undefined,
1217
- )
1201
+ if (human)
1202
+ emitDeprecationWarnings(
1203
+ rest,
1204
+ command.options,
1205
+ command.alias as Record<string, string> | undefined,
1206
+ )
1218
1207
 
1219
- const env = command.env ? Parser.parseEnv(command.env, envSource) : {}
1208
+ const result = await Command.execute(command, {
1209
+ agent: !human,
1210
+ argv: rest,
1211
+ env: options.envSchema,
1212
+ envSource: options.env,
1213
+ format,
1214
+ formatExplicit,
1215
+ inputOptions: {},
1216
+ middlewares: allMiddleware,
1217
+ name,
1218
+ path,
1219
+ vars: options.vars,
1220
+ version: options.version,
1221
+ })
1220
1222
 
1221
- const okFn = (data: unknown, meta: { cta?: CtaBlock | undefined } = {}): never => {
1222
- return { [sentinel]: 'ok', data, cta: meta.cta } as never
1223
- }
1224
- const errorFn = (opts: {
1225
- code: string
1226
- exitCode?: number | undefined
1227
- message: string
1228
- retryable?: boolean | undefined
1229
- cta?: CtaBlock | undefined
1230
- }): never => {
1231
- return { [sentinel]: 'error', ...opts } as never
1232
- }
1223
+ const duration = `${Math.round(performance.now() - start)}ms`
1233
1224
 
1234
- const result = command.run({
1235
- agent: !human,
1236
- args,
1237
- env,
1238
- error: errorFn,
1225
+ // Streaming path — async generator
1226
+ if ('stream' in result) {
1227
+ await handleStreaming(result.stream, {
1228
+ name,
1229
+ path,
1230
+ start,
1239
1231
  format,
1240
1232
  formatExplicit,
1241
- name,
1242
- ok: okFn,
1243
- options: parsedOptions,
1244
- var: varsMap,
1245
- version: options.version,
1233
+ human,
1234
+ renderOutput,
1235
+ verbose,
1236
+ truncate,
1237
+ write,
1238
+ writeln,
1239
+ exit,
1246
1240
  })
1247
-
1248
- // Streaming path — async generator
1249
- if (isAsyncGenerator(result)) {
1250
- await handleStreaming(result, {
1251
- name,
1252
- path,
1253
- start,
1254
- format,
1255
- formatExplicit,
1256
- human,
1257
- renderOutput,
1258
- verbose,
1259
- truncate,
1260
- write,
1261
- writeln,
1262
- exit,
1263
- })
1264
- return
1265
- }
1266
-
1267
- const awaited = await result
1268
-
1269
- if (isSentinel(awaited)) {
1270
- const cta = formatCtaBlock(name, awaited.cta)
1271
- if (awaited[sentinel] === 'ok') {
1272
- write({
1273
- ok: true,
1274
- data: awaited.data,
1275
- meta: {
1276
- command: path,
1277
- duration: `${Math.round(performance.now() - start)}ms`,
1278
- ...(cta ? { cta } : undefined),
1279
- },
1280
- })
1281
- } else {
1282
- const err = awaited as ErrorResult
1283
- write({
1284
- ok: false,
1285
- error: {
1286
- code: err.code,
1287
- message: err.message,
1288
- ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
1289
- },
1290
- meta: {
1291
- command: path,
1292
- duration: `${Math.round(performance.now() - start)}ms`,
1293
- ...(cta ? { cta } : undefined),
1294
- },
1295
- })
1296
- exit(err.exitCode ?? 1)
1297
- }
1298
- } else {
1299
- write({
1300
- ok: true,
1301
- data: awaited,
1302
- meta: {
1303
- command: path,
1304
- duration: `${Math.round(performance.now() - start)}ms`,
1305
- },
1306
- })
1307
- }
1241
+ return
1308
1242
  }
1309
1243
 
1310
- try {
1311
- const cliEnv = options.envSchema ? Parser.parseEnv(options.envSchema, envSource) : {}
1312
-
1313
- if (allMiddleware.length > 0) {
1314
- const errorFn = (opts: {
1315
- code: string
1316
- exitCode?: number | undefined
1317
- message: string
1318
- retryable?: boolean | undefined
1319
- cta?: CtaBlock | undefined
1320
- }): never => {
1321
- return { [sentinel]: 'error', ...opts } as never
1322
- }
1323
- const mwCtx: MiddlewareContext = {
1324
- agent: !human,
1244
+ if (result.ok) {
1245
+ const cta = formatCtaBlock(name, result.cta as CtaBlock | undefined)
1246
+ write({
1247
+ ok: true,
1248
+ data: result.data,
1249
+ meta: {
1325
1250
  command: path,
1326
- env: cliEnv,
1327
- error: errorFn,
1328
- format,
1329
- formatExplicit,
1330
- name,
1331
- set(key: string, value: unknown) {
1332
- varsMap[key] = value
1333
- },
1334
- var: varsMap,
1335
- version: options.version,
1336
- }
1337
- const handleMwSentinel = (result: unknown) => {
1338
- if (!isSentinel(result) || result[sentinel] !== 'error') return
1339
- const err = result as ErrorResult
1340
- const cta = formatCtaBlock(name, err.cta)
1341
- write({
1342
- ok: false,
1343
- error: {
1344
- code: err.code,
1345
- message: err.message,
1346
- ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
1347
- },
1348
- meta: {
1349
- command: path,
1350
- duration: `${Math.round(performance.now() - start)}ms`,
1351
- ...(cta ? { cta } : undefined),
1352
- },
1353
- })
1354
- exit(err.exitCode ?? 1)
1355
- }
1356
- const composed = allMiddleware.reduceRight(
1357
- (next: () => Promise<void>, mw) => async () => {
1358
- handleMwSentinel(await mw(mwCtx, next))
1359
- },
1360
- runCommand,
1251
+ duration,
1252
+ ...(cta ? { cta } : undefined),
1253
+ },
1254
+ })
1255
+ } else {
1256
+ const cta = formatCtaBlock(name, result.cta as CtaBlock | undefined)
1257
+
1258
+ if (human && !formatExplicit && result.error.fieldErrors) {
1259
+ writeln(
1260
+ formatHumanValidationError(
1261
+ name,
1262
+ path,
1263
+ command,
1264
+ new ValidationError({
1265
+ message: result.error.message,
1266
+ fieldErrors: result.error.fieldErrors,
1267
+ }),
1268
+ options.env,
1269
+ ),
1361
1270
  )
1362
- await composed()
1363
- } else {
1364
- await runCommand()
1271
+ exit(1)
1272
+ return
1365
1273
  }
1366
- } catch (error) {
1367
- const errorOutput: Output = {
1274
+
1275
+ write({
1368
1276
  ok: false,
1369
1277
  error: {
1370
- code:
1371
- error instanceof IncurError
1372
- ? error.code
1373
- : error instanceof ValidationError
1374
- ? 'VALIDATION_ERROR'
1375
- : 'UNKNOWN',
1376
- message: error instanceof Error ? error.message : String(error),
1377
- ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
1378
- ...(error instanceof ValidationError ? { fieldErrors: error.fieldErrors } : undefined),
1278
+ code: result.error.code,
1279
+ message: result.error.message,
1280
+ ...(result.error.retryable !== undefined
1281
+ ? { retryable: result.error.retryable }
1282
+ : undefined),
1283
+ ...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
1379
1284
  },
1380
1285
  meta: {
1381
1286
  command: path,
1382
- duration: `${Math.round(performance.now() - start)}ms`,
1287
+ duration,
1288
+ ...(cta ? { cta } : undefined),
1383
1289
  },
1384
- }
1385
-
1386
- if (human && !formatExplicit && error instanceof ValidationError) {
1387
- writeln(formatHumanValidationError(name, path, command, error, options.env))
1388
- exit(1)
1389
- return
1390
- }
1391
-
1392
- write(errorOutput)
1393
- exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1)
1290
+ })
1291
+ exit(result.exitCode ?? 1)
1394
1292
  }
1395
1293
  }
1396
1294
 
1397
1295
  /** @internal Options for fetchImpl. */
1398
1296
  declare namespace fetchImpl {
1399
1297
  type Options = {
1298
+ /** CLI-level env schema. */
1299
+ envSchema?: z.ZodObject<any> | undefined
1300
+ /** Group-level middleware collected during command resolution. */
1301
+ groupMiddlewares?: MiddlewareHandler[] | undefined
1400
1302
  mcpHandler?:
1401
- | ((req: Request, commands: Map<string, CommandEntry>) => Promise<Response>)
1303
+ | ((
1304
+ req: Request,
1305
+ commands: Map<string, CommandEntry>,
1306
+ mcpOptions?: {
1307
+ middlewares?: MiddlewareHandler[] | undefined
1308
+ env?: z.ZodObject<any> | undefined
1309
+ vars?: z.ZodObject<any> | undefined
1310
+ },
1311
+ ) => Promise<Response>)
1402
1312
  | undefined
1403
1313
  middlewares?: MiddlewareHandler[] | undefined
1314
+ /** CLI name. */
1315
+ name?: string | undefined
1404
1316
  rootCommand?: CommandDefinition<any, any, any> | undefined
1405
1317
  vars?: z.ZodObject<any> | undefined
1318
+ /** CLI version string. */
1319
+ version?: string | undefined
1406
1320
  }
1407
1321
  }
1408
1322
 
@@ -1410,7 +1324,15 @@ declare namespace fetchImpl {
1410
1324
  function createMcpHttpHandler(name: string, version: string) {
1411
1325
  let transport: any
1412
1326
 
1413
- return async (req: Request, commands: Map<string, CommandEntry>): Promise<Response> => {
1327
+ return async (
1328
+ req: Request,
1329
+ commands: Map<string, CommandEntry>,
1330
+ mcpOptions?: {
1331
+ middlewares?: MiddlewareHandler[] | undefined
1332
+ env?: z.ZodObject<any> | undefined
1333
+ vars?: z.ZodObject<any> | undefined
1334
+ },
1335
+ ): Promise<Response> => {
1414
1336
  if (!transport) {
1415
1337
  const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js')
1416
1338
  const { WebStandardStreamableHTTPServerTransport } =
@@ -1433,7 +1355,13 @@ function createMcpHttpHandler(name: string, version: string) {
1433
1355
  },
1434
1356
  async (...callArgs: any[]) => {
1435
1357
  const params = hasInput ? (callArgs[0] as Record<string, unknown>) : {}
1436
- return Mcp.callTool(tool, params)
1358
+ return Mcp.callTool(tool, params, {
1359
+ name,
1360
+ version,
1361
+ middlewares: mcpOptions?.middlewares,
1362
+ env: mcpOptions?.env,
1363
+ vars: mcpOptions?.vars,
1364
+ })
1437
1365
  },
1438
1366
  )
1439
1367
  }
@@ -1462,7 +1390,11 @@ async function fetchImpl(
1462
1390
 
1463
1391
  // MCP over HTTP: route /mcp to the MCP transport
1464
1392
  if (segments[0] === 'mcp' && segments.length === 1 && options.mcpHandler)
1465
- return options.mcpHandler(req, commands)
1393
+ return options.mcpHandler(req, commands, {
1394
+ middlewares: options.middlewares,
1395
+ env: options.envSchema,
1396
+ vars: options.vars,
1397
+ })
1466
1398
 
1467
1399
  // .well-known/skills/ — Agent Skills Discovery (RFC)
1468
1400
  if (
@@ -1571,7 +1503,11 @@ async function fetchImpl(
1571
1503
  if ('fetchGateway' in resolved) return resolved.fetchGateway.fetch(req)
1572
1504
 
1573
1505
  const { command, path, rest } = resolved
1574
- return executeCommand(path, command, rest, inputOptions, start, options)
1506
+ const groupMiddlewares = 'middlewares' in resolved ? resolved.middlewares : []
1507
+ return executeCommand(path, command, rest, inputOptions, start, {
1508
+ ...options,
1509
+ groupMiddlewares,
1510
+ })
1575
1511
  }
1576
1512
 
1577
1513
  /** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
@@ -1590,200 +1526,107 @@ async function executeCommand(
1590
1526
  })
1591
1527
  }
1592
1528
 
1593
- const sentinel_ = Symbol.for('incur.sentinel')
1594
- const varsMap: Record<string, unknown> = options.vars ? options.vars.parse({}) : {}
1595
- let response: Response | undefined
1529
+ const allMiddleware = [
1530
+ ...(options.middlewares ?? []),
1531
+ ...((options.groupMiddlewares as MiddlewareHandler[] | undefined) ?? []),
1532
+ ...((command.middleware as MiddlewareHandler[] | undefined) ?? []),
1533
+ ]
1596
1534
 
1597
- const runCommand = async () => {
1598
- const { args } = Parser.parse(rest, { args: command.args })
1599
- const parsedOptions = command.options ? command.options.parse(inputOptions) : {}
1535
+ const result = await Command.execute(command, {
1536
+ agent: true,
1537
+ argv: rest,
1538
+ env: options.envSchema,
1539
+ format: 'json',
1540
+ formatExplicit: true,
1541
+ inputOptions,
1542
+ middlewares: allMiddleware,
1543
+ name: options.name ?? path,
1544
+ parseMode: 'split',
1545
+ path,
1546
+ vars: options.vars,
1547
+ version: options.version,
1548
+ })
1600
1549
 
1601
- const okFn = (data: unknown): never => ({ [sentinel_]: 'ok', data }) as never
1602
- const errorFn = (opts: {
1603
- code: string
1604
- message: string
1605
- exitCode?: number | undefined
1606
- }): never => ({ [sentinel_]: 'error', ...opts }) as never
1607
-
1608
- const result = command.run({
1609
- agent: true,
1610
- args,
1611
- env: {},
1612
- error: errorFn,
1613
- format: 'json',
1614
- formatExplicit: true,
1615
- name: path,
1616
- ok: okFn,
1617
- options: parsedOptions,
1618
- var: varsMap,
1619
- version: undefined,
1620
- })
1550
+ const duration = `${Math.round(performance.now() - start)}ms`
1621
1551
 
1622
- // Streaming path — async generator → NDJSON response
1623
- if (isAsyncGenerator(result)) {
1624
- const stream = new ReadableStream({
1625
- async start(controller) {
1626
- const encoder = new TextEncoder()
1627
- try {
1628
- let returnValue: unknown
1629
- while (true) {
1630
- const { value, done } = await result.next()
1631
- if (done) {
1632
- returnValue = value
1633
- break
1634
- }
1635
- if (isSentinel(value) && (value as any)[sentinel] === 'error') {
1636
- const tagged = value as any
1637
- controller.enqueue(
1638
- encoder.encode(
1639
- JSON.stringify({
1640
- type: 'error',
1641
- ok: false,
1642
- error: { code: tagged.code, message: tagged.message },
1643
- }) + '\n',
1644
- ),
1645
- )
1646
- controller.close()
1647
- return
1648
- }
1649
- controller.enqueue(
1650
- encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'),
1651
- )
1652
- }
1653
- const meta: Record<string, unknown> = { command: path }
1654
- if (isSentinel(returnValue) && (returnValue as any)[sentinel] === 'error') {
1655
- const tagged = returnValue as any
1656
- controller.enqueue(
1657
- encoder.encode(
1658
- JSON.stringify({
1659
- type: 'error',
1660
- ok: false,
1661
- error: { code: tagged.code, message: tagged.message },
1662
- }) + '\n',
1663
- ),
1664
- )
1665
- } else {
1666
- controller.enqueue(
1667
- encoder.encode(JSON.stringify({ type: 'done', ok: true, meta }) + '\n'),
1668
- )
1669
- }
1670
- } catch (error) {
1552
+ // Streaming path — async generator → NDJSON response
1553
+ if ('stream' in result) {
1554
+ const stream = new ReadableStream({
1555
+ async start(controller) {
1556
+ const encoder = new TextEncoder()
1557
+ try {
1558
+ for await (const value of result.stream) {
1671
1559
  controller.enqueue(
1672
- encoder.encode(
1673
- JSON.stringify({
1674
- type: 'error',
1675
- ok: false,
1676
- error: {
1677
- code: 'UNKNOWN',
1678
- message: error instanceof Error ? error.message : String(error),
1679
- },
1680
- }) + '\n',
1681
- ),
1560
+ encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'),
1682
1561
  )
1683
1562
  }
1684
- controller.close()
1685
- },
1686
- })
1687
- response = new Response(stream, {
1688
- status: 200,
1689
- headers: { 'content-type': 'application/x-ndjson' },
1690
- })
1691
- return
1692
- }
1693
-
1694
- const awaited = await result
1695
- const duration = `${Math.round(performance.now() - start)}ms`
1696
-
1697
- if (typeof awaited === 'object' && awaited !== null && sentinel_ in awaited) {
1698
- const tagged = awaited as any
1699
- if (tagged[sentinel_] === 'error')
1700
- response = jsonResponse(
1701
- {
1702
- ok: false,
1703
- error: { code: tagged.code, message: tagged.message },
1704
- meta: { command: path, duration },
1705
- },
1706
- 500,
1707
- )
1708
- else
1709
- response = jsonResponse(
1710
- { ok: true, data: tagged.data, meta: { command: path, duration } },
1711
- 200,
1712
- )
1713
- return
1714
- }
1715
-
1716
- response = jsonResponse({ ok: true, data: awaited, meta: { command: path, duration } }, 200)
1563
+ controller.enqueue(
1564
+ encoder.encode(
1565
+ JSON.stringify({
1566
+ type: 'done',
1567
+ ok: true,
1568
+ meta: { command: path },
1569
+ }) + '\n',
1570
+ ),
1571
+ )
1572
+ } catch (error) {
1573
+ controller.enqueue(
1574
+ encoder.encode(
1575
+ JSON.stringify({
1576
+ type: 'error',
1577
+ ok: false,
1578
+ error: {
1579
+ code: 'UNKNOWN',
1580
+ message: error instanceof Error ? error.message : String(error),
1581
+ },
1582
+ }) + '\n',
1583
+ ),
1584
+ )
1585
+ }
1586
+ controller.close()
1587
+ },
1588
+ })
1589
+ return new Response(stream, {
1590
+ status: 200,
1591
+ headers: { 'content-type': 'application/x-ndjson' },
1592
+ })
1717
1593
  }
1718
1594
 
1719
- try {
1720
- const allMiddleware = options.middlewares ?? []
1721
- if (allMiddleware.length > 0) {
1722
- const errorFn = (opts: {
1723
- code: string
1724
- message: string
1725
- exitCode?: number | undefined
1726
- }): never => {
1727
- const duration = `${Math.round(performance.now() - start)}ms`
1728
- response = jsonResponse(
1729
- {
1730
- ok: false,
1731
- error: { code: opts.code, message: opts.message },
1732
- meta: { command: path, duration },
1733
- },
1734
- 500,
1735
- )
1736
- return undefined as never
1737
- }
1738
- const mwCtx: MiddlewareContext = {
1739
- agent: true,
1740
- command: path,
1741
- env: {},
1742
- error: errorFn,
1743
- format: 'json',
1744
- formatExplicit: true,
1745
- name: path,
1746
- set(key: string, value: unknown) {
1747
- varsMap[key] = value
1748
- },
1749
- var: varsMap,
1750
- version: undefined,
1751
- }
1752
- const composed = allMiddleware.reduceRight(
1753
- (next: () => Promise<void>, mw) => async () => {
1754
- await mw(mwCtx, next)
1755
- },
1756
- runCommand,
1757
- )
1758
- await composed()
1759
- } else {
1760
- await runCommand()
1761
- }
1762
- } catch (error) {
1763
- const duration = `${Math.round(performance.now() - start)}ms`
1764
- if (error instanceof ValidationError)
1765
- return jsonResponse(
1766
- {
1767
- ok: false,
1768
- error: { code: 'VALIDATION_ERROR', message: error.message },
1769
- meta: { command: path, duration },
1770
- },
1771
- 400,
1772
- )
1595
+ if (!result.ok) {
1596
+ const cta = formatCtaBlock(options.name ?? path, result.cta as CtaBlock | undefined)
1773
1597
  return jsonResponse(
1774
1598
  {
1775
1599
  ok: false,
1776
1600
  error: {
1777
- code: error instanceof IncurError ? error.code : 'UNKNOWN',
1778
- message: error instanceof Error ? error.message : String(error),
1601
+ code: result.error.code,
1602
+ message: result.error.message,
1603
+ ...(result.error.retryable !== undefined
1604
+ ? { retryable: result.error.retryable }
1605
+ : undefined),
1606
+ },
1607
+ meta: {
1608
+ command: path,
1609
+ duration,
1610
+ ...(cta ? { cta } : undefined),
1779
1611
  },
1780
- meta: { command: path, duration },
1781
1612
  },
1782
- 500,
1613
+ result.error.code === 'VALIDATION_ERROR' ? 400 : 500,
1783
1614
  )
1784
1615
  }
1785
1616
 
1786
- return response!
1617
+ const cta = formatCtaBlock(options.name ?? path, result.cta as CtaBlock | undefined)
1618
+ return jsonResponse(
1619
+ {
1620
+ ok: true,
1621
+ data: result.data,
1622
+ meta: {
1623
+ command: path,
1624
+ duration,
1625
+ ...(cta ? { cta } : undefined),
1626
+ },
1627
+ },
1628
+ 200,
1629
+ )
1787
1630
  }
1788
1631
 
1789
1632
  /** @internal Formats a validation error for TTY with usage hint. */
@@ -2018,6 +1861,29 @@ function collectHelpCommands(
2018
1861
  return result.sort((a, b) => a.name.localeCompare(b.name))
2019
1862
  }
2020
1863
 
1864
+ /** @internal Formats group-level help for a built-in command (e.g. `cli skills`). */
1865
+ function formatBuiltinHelp(cli: string, builtin: (typeof builtinCommands)[number]): string {
1866
+ return Help.formatRoot(`${cli} ${builtin.name}`, {
1867
+ description: builtin.description,
1868
+ commands: builtin.subcommands?.map((s) => ({ name: s.name, description: s.description })),
1869
+ })
1870
+ }
1871
+
1872
+ /** @internal Formats subcommand-level help for a built-in command (e.g. `cli skills add --help`). */
1873
+ function formatBuiltinSubcommandHelp(
1874
+ cli: string,
1875
+ builtin: (typeof builtinCommands)[number],
1876
+ subName: string,
1877
+ ): string {
1878
+ const sub = builtin.subcommands?.find((s) => s.name === subName)
1879
+ return Help.formatCommand(`${cli} ${builtin.name} ${subName}`, {
1880
+ alias: sub?.alias,
1881
+ description: sub?.description,
1882
+ hideGlobalOptions: true,
1883
+ options: sub?.options,
1884
+ })
1885
+ }
1886
+
2021
1887
  /** @internal Formats help text for a fetch gateway command. */
2022
1888
  function formatFetchHelp(name: string, description?: string): string {
2023
1889
  const lines: string[] = []
@@ -2138,8 +2004,9 @@ function formatHumanError(error: {
2138
2004
  /** @internal Formats a CTA block for human-readable TTY output. */
2139
2005
  function formatHumanCta(cta: FormattedCtaBlock): string {
2140
2006
  const lines: string[] = ['', cta.description]
2007
+ const maxLen = Math.max(...cta.commands.map((c) => c.command.length))
2141
2008
  for (const c of cta.commands) {
2142
- const desc = c.description ? ` # ${c.description}` : ''
2009
+ const desc = c.description ? ` ${''.padEnd(maxLen - c.command.length)}# ${c.description}` : ''
2143
2010
  lines.push(` ${c.command}${desc}`)
2144
2011
  }
2145
2012
  return lines.join('\n')
@@ -2154,16 +2021,6 @@ function isSentinel(value: unknown): value is OkResult | ErrorResult {
2154
2021
  return typeof value === 'object' && value !== null && sentinel in value
2155
2022
  }
2156
2023
 
2157
- /** @internal Type guard for async generators returned by streaming `run` handlers. */
2158
- function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unknown, unknown> {
2159
- return (
2160
- typeof value === 'object' &&
2161
- value !== null &&
2162
- Symbol.asyncIterator in value &&
2163
- typeof (value as any).next === 'function'
2164
- )
2165
- }
2166
-
2167
2024
  /** @internal Handles streaming output from an async generator `run` handler. */
2168
2025
  async function handleStreaming(
2169
2026
  generator: AsyncGenerator<unknown, unknown, unknown>,
@@ -2648,15 +2505,9 @@ type CommandDefinition<
2648
2505
  output extends z.ZodType | undefined = undefined,
2649
2506
  vars extends z.ZodObject<any> | undefined = undefined,
2650
2507
  cliEnv extends z.ZodObject<any> | undefined = undefined,
2651
- > = {
2652
- /** Map of option names to single-char aliases. */
2653
- alias?: options extends z.ZodObject<any>
2654
- ? Partial<Record<keyof z.output<options>, string>>
2655
- : Record<string, string> | undefined
2508
+ > = CommandMeta<options> & {
2656
2509
  /** Zod schema for positional arguments. */
2657
2510
  args?: args | undefined
2658
- /** A short description of what the command does. */
2659
- description?: string | undefined
2660
2511
  /** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
2661
2512
  env?: env | undefined
2662
2513
  /** Usage examples for this command. */
@@ -2665,8 +2516,6 @@ type CommandDefinition<
2665
2516
  format?: Formatter.Format | undefined
2666
2517
  /** Plain text hint displayed after examples and before global options. */
2667
2518
  hint?: string | undefined
2668
- /** Zod schema for named options/flags. */
2669
- options?: options | undefined
2670
2519
  /** Zod schema for the command's return value. */
2671
2520
  output?: output | undefined
2672
2521
  /**