incur 0.3.4 → 0.3.6
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/README.md +62 -1
- package/dist/Cli.d.ts +17 -7
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +435 -365
- package/dist/Cli.js.map +1 -1
- package/dist/Completions.d.ts +1 -2
- package/dist/Completions.d.ts.map +1 -1
- package/dist/Completions.js.map +1 -1
- package/dist/Filter.js +0 -18
- package/dist/Filter.js.map +1 -1
- package/dist/Help.d.ts +6 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +35 -22
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +25 -5
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +61 -69
- package/dist/Mcp.js.map +1 -1
- package/dist/Parser.d.ts +2 -0
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +69 -37
- package/dist/Parser.js.map +1 -1
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +5 -1
- package/dist/Skill.js.map +1 -1
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +10 -1
- package/dist/SyncSkills.js.map +1 -1
- package/dist/bin.d.ts +1 -0
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +17 -2
- package/dist/bin.js.map +1 -1
- package/dist/internal/command.d.ts +118 -0
- package/dist/internal/command.d.ts.map +1 -0
- package/dist/internal/command.js +276 -0
- package/dist/internal/command.js.map +1 -0
- package/dist/internal/configSchema.d.ts +8 -0
- package/dist/internal/configSchema.d.ts.map +1 -0
- package/dist/internal/configSchema.js +57 -0
- package/dist/internal/configSchema.js.map +1 -0
- package/dist/internal/helpers.d.ts +5 -0
- package/dist/internal/helpers.d.ts.map +1 -0
- package/dist/internal/helpers.js +9 -0
- package/dist/internal/helpers.js.map +1 -0
- package/examples/npm/.npmrc.json +21 -0
- package/examples/npm/config.schema.json +137 -0
- package/package.json +1 -1
- package/src/Cli.test-d.ts +39 -0
- package/src/Cli.test.ts +704 -6
- package/src/Cli.ts +551 -448
- package/src/Completions.test.ts +35 -9
- package/src/Completions.ts +1 -2
- package/src/Filter.ts +0 -17
- package/src/Help.test.ts +77 -0
- package/src/Help.ts +39 -21
- package/src/Mcp.test.ts +143 -0
- package/src/Mcp.ts +92 -84
- package/src/Parser.test-d.ts +22 -0
- package/src/Parser.test.ts +89 -0
- package/src/Parser.ts +86 -35
- package/src/Skill.ts +5 -1
- package/src/SyncSkills.ts +11 -1
- package/src/bin.ts +21 -2
- package/src/e2e.test.ts +30 -17
- package/src/internal/command.ts +428 -0
- package/src/internal/configSchema.test.ts +193 -0
- package/src/internal/configSchema.ts +66 -0
- package/src/internal/helpers.ts +9 -0
package/src/Cli.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
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'
|
|
2
5
|
import type { z } from 'zod'
|
|
3
6
|
|
|
4
7
|
import * as Completions from './Completions.js'
|
|
5
8
|
import type { FieldError } from './Errors.js'
|
|
6
|
-
import { IncurError, ValidationError } from './Errors.js'
|
|
9
|
+
import { IncurError, ParseError, ValidationError } from './Errors.js'
|
|
7
10
|
import * as Fetch from './Fetch.js'
|
|
8
11
|
import * as Filter from './Filter.js'
|
|
9
12
|
import * as Formatter from './Formatter.js'
|
|
10
13
|
import * as Help from './Help.js'
|
|
14
|
+
import { builtinCommands, type CommandMeta, type Shell, shells } from './internal/command.js'
|
|
15
|
+
import * as Command from './internal/command.js'
|
|
16
|
+
import { isRecord } from './internal/helpers.js'
|
|
11
17
|
import { detectRunner } from './internal/pm.js'
|
|
12
18
|
import type { OneOf } from './internal/types.js'
|
|
13
19
|
import * as Mcp from './Mcp.js'
|
|
@@ -257,10 +263,13 @@ export function create(
|
|
|
257
263
|
async fetch(req: Request) {
|
|
258
264
|
if (pending.length > 0) await Promise.all(pending)
|
|
259
265
|
return fetchImpl(name, commands, req, {
|
|
266
|
+
envSchema: def.env,
|
|
260
267
|
mcpHandler,
|
|
261
268
|
middlewares,
|
|
269
|
+
name,
|
|
262
270
|
rootCommand: rootDef,
|
|
263
271
|
vars: def.vars,
|
|
272
|
+
version: def.version,
|
|
264
273
|
})
|
|
265
274
|
},
|
|
266
275
|
|
|
@@ -269,6 +278,7 @@ export function create(
|
|
|
269
278
|
return serveImpl(name, commands, argv, {
|
|
270
279
|
...serveOptions,
|
|
271
280
|
aliases: def.aliases,
|
|
281
|
+
config: def.config,
|
|
272
282
|
description: def.description,
|
|
273
283
|
envSchema: def.env,
|
|
274
284
|
format: def.format,
|
|
@@ -290,6 +300,8 @@ export function create(
|
|
|
290
300
|
}
|
|
291
301
|
|
|
292
302
|
if (rootDef) toRootDefinition.set(cli as unknown as Root, rootDef)
|
|
303
|
+
if (def.options) toRootOptions.set(cli, def.options)
|
|
304
|
+
if (def.config !== undefined) toConfigEnabled.set(cli, true)
|
|
293
305
|
if (def.outputPolicy) toOutputPolicy.set(cli, def.outputPolicy)
|
|
294
306
|
toMiddlewares.set(cli, middlewares)
|
|
295
307
|
toCommands.set(cli, commands)
|
|
@@ -313,6 +325,24 @@ export declare namespace create {
|
|
|
313
325
|
aliases?: string[] | undefined
|
|
314
326
|
/** Zod schema for positional arguments. */
|
|
315
327
|
args?: args | undefined
|
|
328
|
+
/** Enable config-file defaults for command options. */
|
|
329
|
+
config?:
|
|
330
|
+
| {
|
|
331
|
+
/** Global flag name for specifying a config file path (e.g. `'config'` → `--config <path>`). Omit to auto-load only, with no CLI flag. */
|
|
332
|
+
flag?: string | undefined
|
|
333
|
+
/** Ordered list of file paths to search. First existing file wins. Supports `~` for home dir. Defaults to `['<cli>.json']` relative to cwd. */
|
|
334
|
+
files?: string[] | undefined
|
|
335
|
+
/** Custom config loader. Receives the resolved file path (or `undefined` if no file was found). Returns the parsed config tree, or `undefined` for no defaults. When omitted, the framework reads and parses JSON. */
|
|
336
|
+
loader?:
|
|
337
|
+
| ((
|
|
338
|
+
path: string | undefined,
|
|
339
|
+
) =>
|
|
340
|
+
| Record<string, unknown>
|
|
341
|
+
| undefined
|
|
342
|
+
| Promise<Record<string, unknown> | undefined>)
|
|
343
|
+
| undefined
|
|
344
|
+
}
|
|
345
|
+
| undefined
|
|
316
346
|
/** A short description of what the CLI does. */
|
|
317
347
|
description?: string | undefined
|
|
318
348
|
/** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
|
|
@@ -422,6 +452,24 @@ async function serveImpl(
|
|
|
422
452
|
) {
|
|
423
453
|
const stdout = options.stdout ?? ((s: string) => process.stdout.write(s))
|
|
424
454
|
const exit = options.exit ?? ((code: number) => process.exit(code))
|
|
455
|
+
const human = process.stdout.isTTY === true
|
|
456
|
+
const configEnabled = options.config !== undefined
|
|
457
|
+
const configFlag = options.config?.flag
|
|
458
|
+
|
|
459
|
+
function writeln(s: string) {
|
|
460
|
+
stdout(s.endsWith('\n') ? s : `${s}\n`)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
let builtinFlags: ReturnType<typeof extractBuiltinFlags>
|
|
464
|
+
try {
|
|
465
|
+
builtinFlags = extractBuiltinFlags(argv, { configFlag })
|
|
466
|
+
} catch (error) {
|
|
467
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
468
|
+
if (human) writeln(formatHumanError({ code: 'UNKNOWN', message }))
|
|
469
|
+
else writeln(Formatter.format({ code: 'UNKNOWN', message }, 'toon'))
|
|
470
|
+
exit(1)
|
|
471
|
+
return
|
|
472
|
+
}
|
|
425
473
|
|
|
426
474
|
const {
|
|
427
475
|
verbose,
|
|
@@ -437,17 +485,24 @@ async function serveImpl(
|
|
|
437
485
|
help,
|
|
438
486
|
version,
|
|
439
487
|
schema,
|
|
488
|
+
configPath,
|
|
489
|
+
configDisabled,
|
|
440
490
|
rest: filtered,
|
|
441
|
-
} =
|
|
491
|
+
} = builtinFlags
|
|
442
492
|
|
|
443
493
|
// --mcp: start as MCP stdio server
|
|
444
494
|
if (mcpFlag) {
|
|
445
|
-
await Mcp.serve(name, options.version ?? '0.0.0', commands
|
|
495
|
+
await Mcp.serve(name, options.version ?? '0.0.0', commands, {
|
|
496
|
+
middlewares: options.middlewares,
|
|
497
|
+
env: options.envSchema,
|
|
498
|
+
vars: options.vars,
|
|
499
|
+
version: options.version,
|
|
500
|
+
})
|
|
446
501
|
return
|
|
447
502
|
}
|
|
448
503
|
|
|
449
504
|
// COMPLETE: dynamic shell completions (called by shell hook at tab-press)
|
|
450
|
-
const completeShell = process.env.COMPLETE as
|
|
505
|
+
const completeShell = process.env.COMPLETE as Shell | undefined
|
|
451
506
|
if (completeShell) {
|
|
452
507
|
// Remove separator `--` from argv
|
|
453
508
|
const sepIdx = argv.indexOf('--')
|
|
@@ -459,19 +514,32 @@ async function serveImpl(
|
|
|
459
514
|
} else {
|
|
460
515
|
const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1)
|
|
461
516
|
const candidates = Completions.complete(commands, options.rootCommand, words, index)
|
|
517
|
+
// Add built-in commands (completions, mcp, skills) to completions
|
|
518
|
+
const current = words[index] ?? ''
|
|
519
|
+
const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'))
|
|
520
|
+
if (nonFlags.length <= 1) {
|
|
521
|
+
for (const b of builtinCommands) {
|
|
522
|
+
if (b.name.startsWith(current) && !candidates.some((c) => c.value === b.name))
|
|
523
|
+
candidates.push({
|
|
524
|
+
value: b.name,
|
|
525
|
+
description: b.description,
|
|
526
|
+
...(b.subcommands ? { noSpace: true } : undefined),
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
} else if (nonFlags.length === 2) {
|
|
530
|
+
const parent = nonFlags[nonFlags.length - 1]
|
|
531
|
+
const builtin = builtinCommands.find((b) => b.name === parent && b.subcommands)
|
|
532
|
+
if (builtin?.subcommands)
|
|
533
|
+
for (const sub of builtin.subcommands)
|
|
534
|
+
if (sub.name.startsWith(current))
|
|
535
|
+
candidates.push({ value: sub.name, description: sub.description })
|
|
536
|
+
}
|
|
462
537
|
const out = Completions.format(completeShell, candidates)
|
|
463
538
|
if (out) stdout(out)
|
|
464
539
|
}
|
|
465
540
|
return
|
|
466
541
|
}
|
|
467
542
|
|
|
468
|
-
// Human mode: stdout is a TTY.
|
|
469
|
-
const human = process.stdout.isTTY === true
|
|
470
|
-
|
|
471
|
-
function writeln(s: string) {
|
|
472
|
-
stdout(s.endsWith('\n') ? s : `${s}\n`)
|
|
473
|
-
}
|
|
474
|
-
|
|
475
543
|
// Skills staleness check (skip for built-in commands)
|
|
476
544
|
if (!llms && !llmsFull && !schema && !help && !version) {
|
|
477
545
|
const isSkillsAdd =
|
|
@@ -544,73 +612,48 @@ async function serveImpl(
|
|
|
544
612
|
// not a completions invocation
|
|
545
613
|
return -1
|
|
546
614
|
})()
|
|
615
|
+
// TODO: refactor built-in command handlers (completions, skills, mcp) into a generic dispatch loop on `builtinCommands`
|
|
547
616
|
if (completionsIdx !== -1 && filtered[completionsIdx] === 'completions') {
|
|
548
|
-
|
|
617
|
+
const shell = filtered[completionsIdx + 1]
|
|
618
|
+
if (help || !shell) {
|
|
619
|
+
const b = builtinCommands.find((c) => c.name === 'completions')!
|
|
549
620
|
writeln(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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'),
|
|
621
|
+
Help.formatCommand(`${name} completions`, {
|
|
622
|
+
args: b.args,
|
|
623
|
+
description: b.description,
|
|
624
|
+
hideGlobalOptions: true,
|
|
625
|
+
hint: b.hint?.(name),
|
|
626
|
+
}),
|
|
577
627
|
)
|
|
578
628
|
return
|
|
579
629
|
}
|
|
580
|
-
|
|
581
|
-
if (!shell || !['bash', 'fish', 'nushell', 'zsh'].includes(shell)) {
|
|
630
|
+
if (!shells.includes(shell as any)) {
|
|
582
631
|
writeln(
|
|
583
632
|
formatHumanError({
|
|
584
633
|
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>`,
|
|
634
|
+
message: `Unknown shell '${shell}'. Supported: ${shells.join(', ')}`,
|
|
588
635
|
}),
|
|
589
636
|
)
|
|
590
637
|
exit(1)
|
|
591
638
|
return
|
|
592
639
|
}
|
|
593
640
|
const names = [name, ...(options.aliases ?? [])]
|
|
594
|
-
writeln(names.map((n) => Completions.register(shell as
|
|
641
|
+
writeln(names.map((n) => Completions.register(shell as Shell, n)).join('\n'))
|
|
595
642
|
return
|
|
596
643
|
}
|
|
597
644
|
|
|
598
645
|
// skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
|
|
599
646
|
const skillsIdx =
|
|
600
647
|
filtered[0] === 'skills' ? 0 : filtered[0] === name && filtered[1] === 'skills' ? 1 : -1
|
|
601
|
-
if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills'
|
|
648
|
+
if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills') {
|
|
649
|
+
if (filtered[skillsIdx + 1] !== 'add') {
|
|
650
|
+
const b = builtinCommands.find((c) => c.name === 'skills')!
|
|
651
|
+
writeln(formatBuiltinHelp(name, b))
|
|
652
|
+
return
|
|
653
|
+
}
|
|
602
654
|
if (help) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
`${name} skills add — Sync skill files to agents`,
|
|
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
|
-
)
|
|
655
|
+
const b = builtinCommands.find((c) => c.name === 'skills')!
|
|
656
|
+
writeln(formatBuiltinSubcommandHelp(name, b, 'add'))
|
|
614
657
|
return
|
|
615
658
|
}
|
|
616
659
|
const rest = filtered.slice(skillsIdx + 2)
|
|
@@ -674,20 +717,15 @@ async function serveImpl(
|
|
|
674
717
|
|
|
675
718
|
// mcp add: register CLI as MCP server via `npx add-mcp`
|
|
676
719
|
const mcpIdx = filtered[0] === 'mcp' ? 0 : filtered[0] === name && filtered[1] === 'mcp' ? 1 : -1
|
|
677
|
-
if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp'
|
|
720
|
+
if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp') {
|
|
721
|
+
if (filtered[mcpIdx + 1] !== 'add') {
|
|
722
|
+
const b = builtinCommands.find((c) => c.name === 'mcp')!
|
|
723
|
+
writeln(formatBuiltinHelp(name, b))
|
|
724
|
+
return
|
|
725
|
+
}
|
|
678
726
|
if (help) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
`${name} mcp add — Register as 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
|
-
)
|
|
727
|
+
const b = builtinCommands.find((c) => c.name === 'mcp')!
|
|
728
|
+
writeln(formatBuiltinSubcommandHelp(name, b, 'add'))
|
|
691
729
|
return
|
|
692
730
|
}
|
|
693
731
|
const rest = filtered.slice(mcpIdx + 2)
|
|
@@ -759,6 +797,7 @@ async function serveImpl(
|
|
|
759
797
|
Help.formatCommand(name, {
|
|
760
798
|
alias: cmd.alias as Record<string, string> | undefined,
|
|
761
799
|
aliases: options.aliases,
|
|
800
|
+
configFlag,
|
|
762
801
|
description: cmd.description ?? options.description,
|
|
763
802
|
version: options.version,
|
|
764
803
|
args: cmd.args,
|
|
@@ -780,6 +819,7 @@ async function serveImpl(
|
|
|
780
819
|
writeln(
|
|
781
820
|
Help.formatRoot(name, {
|
|
782
821
|
aliases: options.aliases,
|
|
822
|
+
configFlag,
|
|
783
823
|
description: options.description,
|
|
784
824
|
version: options.version,
|
|
785
825
|
commands: collectHelpCommands(commands),
|
|
@@ -828,6 +868,7 @@ async function serveImpl(
|
|
|
828
868
|
Help.formatCommand(name, {
|
|
829
869
|
alias: cmd.alias as Record<string, string> | undefined,
|
|
830
870
|
aliases: options.aliases,
|
|
871
|
+
configFlag,
|
|
831
872
|
description: cmd.description ?? options.description,
|
|
832
873
|
version: options.version,
|
|
833
874
|
args: cmd.args,
|
|
@@ -845,6 +886,7 @@ async function serveImpl(
|
|
|
845
886
|
writeln(
|
|
846
887
|
Help.formatRoot(helpName, {
|
|
847
888
|
aliases: isRoot ? options.aliases : undefined,
|
|
889
|
+
configFlag,
|
|
848
890
|
description: helpDesc,
|
|
849
891
|
version: isRoot ? options.version : undefined,
|
|
850
892
|
commands: collectHelpCommands(helpCmds),
|
|
@@ -864,6 +906,7 @@ async function serveImpl(
|
|
|
864
906
|
Help.formatCommand(commandName, {
|
|
865
907
|
alias: cmd.alias as Record<string, string> | undefined,
|
|
866
908
|
aliases: isRootCmd ? options.aliases : undefined,
|
|
909
|
+
configFlag,
|
|
867
910
|
description: cmd.description,
|
|
868
911
|
version: isRootCmd ? options.version : undefined,
|
|
869
912
|
args: cmd.args,
|
|
@@ -886,6 +929,7 @@ async function serveImpl(
|
|
|
886
929
|
if ('help' in resolved) {
|
|
887
930
|
writeln(
|
|
888
931
|
Help.formatRoot(`${name} ${resolved.path}`, {
|
|
932
|
+
configFlag,
|
|
889
933
|
description: resolved.description,
|
|
890
934
|
commands: collectHelpCommands(resolved.commands),
|
|
891
935
|
}),
|
|
@@ -917,6 +961,7 @@ async function serveImpl(
|
|
|
917
961
|
if ('help' in resolved) {
|
|
918
962
|
writeln(
|
|
919
963
|
Help.formatRoot(`${name} ${resolved.path}`, {
|
|
964
|
+
configFlag,
|
|
920
965
|
description: resolved.description,
|
|
921
966
|
commands: collectHelpCommands(resolved.commands),
|
|
922
967
|
}),
|
|
@@ -1198,211 +1243,150 @@ async function serveImpl(
|
|
|
1198
1243
|
...((command.middleware as MiddlewareHandler[] | undefined) ?? []),
|
|
1199
1244
|
]
|
|
1200
1245
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
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
|
-
)
|
|
1218
|
-
|
|
1219
|
-
const env = command.env ? Parser.parseEnv(command.env, envSource) : {}
|
|
1220
|
-
|
|
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
|
-
}
|
|
1233
|
-
|
|
1234
|
-
const result = command.run({
|
|
1235
|
-
agent: !human,
|
|
1236
|
-
args,
|
|
1237
|
-
env,
|
|
1238
|
-
error: errorFn,
|
|
1239
|
-
format,
|
|
1240
|
-
formatExplicit,
|
|
1241
|
-
name,
|
|
1242
|
-
ok: okFn,
|
|
1243
|
-
options: parsedOptions,
|
|
1244
|
-
var: varsMap,
|
|
1245
|
-
version: options.version,
|
|
1246
|
-
})
|
|
1246
|
+
if (human)
|
|
1247
|
+
emitDeprecationWarnings(
|
|
1248
|
+
rest,
|
|
1249
|
+
command.options,
|
|
1250
|
+
command.alias as Record<string, string> | undefined,
|
|
1251
|
+
)
|
|
1247
1252
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
human,
|
|
1257
|
-
renderOutput,
|
|
1258
|
-
verbose,
|
|
1259
|
-
truncate,
|
|
1260
|
-
write,
|
|
1261
|
-
writeln,
|
|
1262
|
-
exit,
|
|
1253
|
+
let defaults: Record<string, unknown> | undefined
|
|
1254
|
+
if (configEnabled) {
|
|
1255
|
+
try {
|
|
1256
|
+
defaults = await loadCommandOptionDefaults(name, path, {
|
|
1257
|
+
configDisabled,
|
|
1258
|
+
configPath,
|
|
1259
|
+
files: options.config?.files,
|
|
1260
|
+
loader: options.config?.loader,
|
|
1263
1261
|
})
|
|
1264
|
-
|
|
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 {
|
|
1262
|
+
} catch (error) {
|
|
1299
1263
|
write({
|
|
1300
|
-
ok:
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
duration: `${Math.round(performance.now() - start)}ms`,
|
|
1264
|
+
ok: false,
|
|
1265
|
+
error: {
|
|
1266
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
1267
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1305
1268
|
},
|
|
1269
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
|
|
1306
1270
|
})
|
|
1271
|
+
exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1)
|
|
1272
|
+
return
|
|
1307
1273
|
}
|
|
1308
1274
|
}
|
|
1309
1275
|
|
|
1310
|
-
|
|
1311
|
-
|
|
1276
|
+
const result = await Command.execute(command, {
|
|
1277
|
+
agent: !human,
|
|
1278
|
+
argv: rest,
|
|
1279
|
+
defaults,
|
|
1280
|
+
env: options.envSchema,
|
|
1281
|
+
envSource: options.env,
|
|
1282
|
+
format,
|
|
1283
|
+
formatExplicit,
|
|
1284
|
+
inputOptions: {},
|
|
1285
|
+
middlewares: allMiddleware,
|
|
1286
|
+
name,
|
|
1287
|
+
path,
|
|
1288
|
+
vars: options.vars,
|
|
1289
|
+
version: options.version,
|
|
1290
|
+
})
|
|
1312
1291
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1292
|
+
const duration = `${Math.round(performance.now() - start)}ms`
|
|
1293
|
+
|
|
1294
|
+
// Streaming path — async generator
|
|
1295
|
+
if ('stream' in result) {
|
|
1296
|
+
await handleStreaming(result.stream, {
|
|
1297
|
+
name,
|
|
1298
|
+
path,
|
|
1299
|
+
start,
|
|
1300
|
+
format,
|
|
1301
|
+
formatExplicit,
|
|
1302
|
+
human,
|
|
1303
|
+
renderOutput,
|
|
1304
|
+
verbose,
|
|
1305
|
+
truncate,
|
|
1306
|
+
write,
|
|
1307
|
+
writeln,
|
|
1308
|
+
exit,
|
|
1309
|
+
})
|
|
1310
|
+
return
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if (result.ok) {
|
|
1314
|
+
const cta = formatCtaBlock(name, result.cta as CtaBlock | undefined)
|
|
1315
|
+
write({
|
|
1316
|
+
ok: true,
|
|
1317
|
+
data: result.data,
|
|
1318
|
+
meta: {
|
|
1325
1319
|
command: path,
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
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,
|
|
1320
|
+
duration,
|
|
1321
|
+
...(cta ? { cta } : undefined),
|
|
1322
|
+
},
|
|
1323
|
+
})
|
|
1324
|
+
} else {
|
|
1325
|
+
const cta = formatCtaBlock(name, result.cta as CtaBlock | undefined)
|
|
1326
|
+
|
|
1327
|
+
if (human && !formatExplicit && result.error.fieldErrors) {
|
|
1328
|
+
writeln(
|
|
1329
|
+
formatHumanValidationError(
|
|
1330
|
+
name,
|
|
1331
|
+
path,
|
|
1332
|
+
command,
|
|
1333
|
+
new ValidationError({
|
|
1334
|
+
message: result.error.message,
|
|
1335
|
+
fieldErrors: result.error.fieldErrors,
|
|
1336
|
+
}),
|
|
1337
|
+
options.env,
|
|
1338
|
+
configFlag,
|
|
1339
|
+
),
|
|
1361
1340
|
)
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
await runCommand()
|
|
1341
|
+
exit(1)
|
|
1342
|
+
return
|
|
1365
1343
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1344
|
+
|
|
1345
|
+
write({
|
|
1368
1346
|
ok: false,
|
|
1369
1347
|
error: {
|
|
1370
|
-
code:
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
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),
|
|
1348
|
+
code: result.error.code,
|
|
1349
|
+
message: result.error.message,
|
|
1350
|
+
...(result.error.retryable !== undefined
|
|
1351
|
+
? { retryable: result.error.retryable }
|
|
1352
|
+
: undefined),
|
|
1353
|
+
...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
|
|
1379
1354
|
},
|
|
1380
1355
|
meta: {
|
|
1381
1356
|
command: path,
|
|
1382
|
-
duration
|
|
1357
|
+
duration,
|
|
1358
|
+
...(cta ? { cta } : undefined),
|
|
1383
1359
|
},
|
|
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)
|
|
1360
|
+
})
|
|
1361
|
+
exit(result.exitCode ?? 1)
|
|
1394
1362
|
}
|
|
1395
1363
|
}
|
|
1396
1364
|
|
|
1397
1365
|
/** @internal Options for fetchImpl. */
|
|
1398
1366
|
declare namespace fetchImpl {
|
|
1399
1367
|
type Options = {
|
|
1368
|
+
/** CLI-level env schema. */
|
|
1369
|
+
envSchema?: z.ZodObject<any> | undefined
|
|
1370
|
+
/** Group-level middleware collected during command resolution. */
|
|
1371
|
+
groupMiddlewares?: MiddlewareHandler[] | undefined
|
|
1400
1372
|
mcpHandler?:
|
|
1401
|
-
| ((
|
|
1373
|
+
| ((
|
|
1374
|
+
req: Request,
|
|
1375
|
+
commands: Map<string, CommandEntry>,
|
|
1376
|
+
mcpOptions?: {
|
|
1377
|
+
middlewares?: MiddlewareHandler[] | undefined
|
|
1378
|
+
env?: z.ZodObject<any> | undefined
|
|
1379
|
+
vars?: z.ZodObject<any> | undefined
|
|
1380
|
+
},
|
|
1381
|
+
) => Promise<Response>)
|
|
1402
1382
|
| undefined
|
|
1403
1383
|
middlewares?: MiddlewareHandler[] | undefined
|
|
1384
|
+
/** CLI name. */
|
|
1385
|
+
name?: string | undefined
|
|
1404
1386
|
rootCommand?: CommandDefinition<any, any, any> | undefined
|
|
1405
1387
|
vars?: z.ZodObject<any> | undefined
|
|
1388
|
+
/** CLI version string. */
|
|
1389
|
+
version?: string | undefined
|
|
1406
1390
|
}
|
|
1407
1391
|
}
|
|
1408
1392
|
|
|
@@ -1410,7 +1394,15 @@ declare namespace fetchImpl {
|
|
|
1410
1394
|
function createMcpHttpHandler(name: string, version: string) {
|
|
1411
1395
|
let transport: any
|
|
1412
1396
|
|
|
1413
|
-
return async (
|
|
1397
|
+
return async (
|
|
1398
|
+
req: Request,
|
|
1399
|
+
commands: Map<string, CommandEntry>,
|
|
1400
|
+
mcpOptions?: {
|
|
1401
|
+
middlewares?: MiddlewareHandler[] | undefined
|
|
1402
|
+
env?: z.ZodObject<any> | undefined
|
|
1403
|
+
vars?: z.ZodObject<any> | undefined
|
|
1404
|
+
},
|
|
1405
|
+
): Promise<Response> => {
|
|
1414
1406
|
if (!transport) {
|
|
1415
1407
|
const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js')
|
|
1416
1408
|
const { WebStandardStreamableHTTPServerTransport } =
|
|
@@ -1433,7 +1425,13 @@ function createMcpHttpHandler(name: string, version: string) {
|
|
|
1433
1425
|
},
|
|
1434
1426
|
async (...callArgs: any[]) => {
|
|
1435
1427
|
const params = hasInput ? (callArgs[0] as Record<string, unknown>) : {}
|
|
1436
|
-
return Mcp.callTool(tool, params
|
|
1428
|
+
return Mcp.callTool(tool, params, {
|
|
1429
|
+
name,
|
|
1430
|
+
version,
|
|
1431
|
+
middlewares: mcpOptions?.middlewares,
|
|
1432
|
+
env: mcpOptions?.env,
|
|
1433
|
+
vars: mcpOptions?.vars,
|
|
1434
|
+
})
|
|
1437
1435
|
},
|
|
1438
1436
|
)
|
|
1439
1437
|
}
|
|
@@ -1462,7 +1460,11 @@ async function fetchImpl(
|
|
|
1462
1460
|
|
|
1463
1461
|
// MCP over HTTP: route /mcp to the MCP transport
|
|
1464
1462
|
if (segments[0] === 'mcp' && segments.length === 1 && options.mcpHandler)
|
|
1465
|
-
return options.mcpHandler(req, commands
|
|
1463
|
+
return options.mcpHandler(req, commands, {
|
|
1464
|
+
middlewares: options.middlewares,
|
|
1465
|
+
env: options.envSchema,
|
|
1466
|
+
vars: options.vars,
|
|
1467
|
+
})
|
|
1466
1468
|
|
|
1467
1469
|
// .well-known/skills/ — Agent Skills Discovery (RFC)
|
|
1468
1470
|
if (
|
|
@@ -1571,7 +1573,11 @@ async function fetchImpl(
|
|
|
1571
1573
|
if ('fetchGateway' in resolved) return resolved.fetchGateway.fetch(req)
|
|
1572
1574
|
|
|
1573
1575
|
const { command, path, rest } = resolved
|
|
1574
|
-
|
|
1576
|
+
const groupMiddlewares = 'middlewares' in resolved ? resolved.middlewares : []
|
|
1577
|
+
return executeCommand(path, command, rest, inputOptions, start, {
|
|
1578
|
+
...options,
|
|
1579
|
+
groupMiddlewares,
|
|
1580
|
+
})
|
|
1575
1581
|
}
|
|
1576
1582
|
|
|
1577
1583
|
/** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
|
|
@@ -1590,200 +1596,107 @@ async function executeCommand(
|
|
|
1590
1596
|
})
|
|
1591
1597
|
}
|
|
1592
1598
|
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
|
|
1599
|
+
const allMiddleware = [
|
|
1600
|
+
...(options.middlewares ?? []),
|
|
1601
|
+
...((options.groupMiddlewares as MiddlewareHandler[] | undefined) ?? []),
|
|
1602
|
+
...((command.middleware as MiddlewareHandler[] | undefined) ?? []),
|
|
1603
|
+
]
|
|
1596
1604
|
|
|
1597
|
-
const
|
|
1598
|
-
|
|
1599
|
-
|
|
1605
|
+
const result = await Command.execute(command, {
|
|
1606
|
+
agent: true,
|
|
1607
|
+
argv: rest,
|
|
1608
|
+
env: options.envSchema,
|
|
1609
|
+
format: 'json',
|
|
1610
|
+
formatExplicit: true,
|
|
1611
|
+
inputOptions,
|
|
1612
|
+
middlewares: allMiddleware,
|
|
1613
|
+
name: options.name ?? path,
|
|
1614
|
+
parseMode: 'split',
|
|
1615
|
+
path,
|
|
1616
|
+
vars: options.vars,
|
|
1617
|
+
version: options.version,
|
|
1618
|
+
})
|
|
1600
1619
|
|
|
1601
|
-
|
|
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
|
-
})
|
|
1620
|
+
const duration = `${Math.round(performance.now() - start)}ms`
|
|
1621
1621
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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) {
|
|
1622
|
+
// Streaming path — async generator → NDJSON response
|
|
1623
|
+
if ('stream' in result) {
|
|
1624
|
+
const stream = new ReadableStream({
|
|
1625
|
+
async start(controller) {
|
|
1626
|
+
const encoder = new TextEncoder()
|
|
1627
|
+
try {
|
|
1628
|
+
for await (const value of result.stream) {
|
|
1671
1629
|
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
|
-
),
|
|
1630
|
+
encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'),
|
|
1682
1631
|
)
|
|
1683
1632
|
}
|
|
1684
|
-
controller.
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
)
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
response = jsonResponse({ ok: true, data: awaited, meta: { command: path, duration } }, 200)
|
|
1633
|
+
controller.enqueue(
|
|
1634
|
+
encoder.encode(
|
|
1635
|
+
JSON.stringify({
|
|
1636
|
+
type: 'done',
|
|
1637
|
+
ok: true,
|
|
1638
|
+
meta: { command: path },
|
|
1639
|
+
}) + '\n',
|
|
1640
|
+
),
|
|
1641
|
+
)
|
|
1642
|
+
} catch (error) {
|
|
1643
|
+
controller.enqueue(
|
|
1644
|
+
encoder.encode(
|
|
1645
|
+
JSON.stringify({
|
|
1646
|
+
type: 'error',
|
|
1647
|
+
ok: false,
|
|
1648
|
+
error: {
|
|
1649
|
+
code: 'UNKNOWN',
|
|
1650
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1651
|
+
},
|
|
1652
|
+
}) + '\n',
|
|
1653
|
+
),
|
|
1654
|
+
)
|
|
1655
|
+
}
|
|
1656
|
+
controller.close()
|
|
1657
|
+
},
|
|
1658
|
+
})
|
|
1659
|
+
return new Response(stream, {
|
|
1660
|
+
status: 200,
|
|
1661
|
+
headers: { 'content-type': 'application/x-ndjson' },
|
|
1662
|
+
})
|
|
1717
1663
|
}
|
|
1718
1664
|
|
|
1719
|
-
|
|
1720
|
-
const
|
|
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
|
-
)
|
|
1665
|
+
if (!result.ok) {
|
|
1666
|
+
const cta = formatCtaBlock(options.name ?? path, result.cta as CtaBlock | undefined)
|
|
1773
1667
|
return jsonResponse(
|
|
1774
1668
|
{
|
|
1775
1669
|
ok: false,
|
|
1776
1670
|
error: {
|
|
1777
|
-
code: error
|
|
1778
|
-
message: error
|
|
1671
|
+
code: result.error.code,
|
|
1672
|
+
message: result.error.message,
|
|
1673
|
+
...(result.error.retryable !== undefined
|
|
1674
|
+
? { retryable: result.error.retryable }
|
|
1675
|
+
: undefined),
|
|
1676
|
+
},
|
|
1677
|
+
meta: {
|
|
1678
|
+
command: path,
|
|
1679
|
+
duration,
|
|
1680
|
+
...(cta ? { cta } : undefined),
|
|
1779
1681
|
},
|
|
1780
|
-
meta: { command: path, duration },
|
|
1781
1682
|
},
|
|
1782
|
-
500,
|
|
1683
|
+
result.error.code === 'VALIDATION_ERROR' ? 400 : 500,
|
|
1783
1684
|
)
|
|
1784
1685
|
}
|
|
1785
1686
|
|
|
1786
|
-
|
|
1687
|
+
const cta = formatCtaBlock(options.name ?? path, result.cta as CtaBlock | undefined)
|
|
1688
|
+
return jsonResponse(
|
|
1689
|
+
{
|
|
1690
|
+
ok: true,
|
|
1691
|
+
data: result.data,
|
|
1692
|
+
meta: {
|
|
1693
|
+
command: path,
|
|
1694
|
+
duration,
|
|
1695
|
+
...(cta ? { cta } : undefined),
|
|
1696
|
+
},
|
|
1697
|
+
},
|
|
1698
|
+
200,
|
|
1699
|
+
)
|
|
1787
1700
|
}
|
|
1788
1701
|
|
|
1789
1702
|
/** @internal Formats a validation error for TTY with usage hint. */
|
|
@@ -1793,6 +1706,7 @@ function formatHumanValidationError(
|
|
|
1793
1706
|
command: CommandDefinition<any, any, any>,
|
|
1794
1707
|
error: ValidationError,
|
|
1795
1708
|
envSource?: Record<string, string | undefined>,
|
|
1709
|
+
configFlag?: string,
|
|
1796
1710
|
): string {
|
|
1797
1711
|
const lines: string[] = []
|
|
1798
1712
|
for (const fe of error.fieldErrors) lines.push(`Error: missing required argument <${fe.path}>`)
|
|
@@ -1801,6 +1715,7 @@ function formatHumanValidationError(
|
|
|
1801
1715
|
lines.push(
|
|
1802
1716
|
Help.formatCommand(path === cli ? cli : `${cli} ${path}`, {
|
|
1803
1717
|
alias: command.alias as Record<string, string> | undefined,
|
|
1718
|
+
configFlag,
|
|
1804
1719
|
description: command.description,
|
|
1805
1720
|
args: command.args,
|
|
1806
1721
|
env: command.env,
|
|
@@ -1910,6 +1825,20 @@ declare namespace serveImpl {
|
|
|
1910
1825
|
type Options = serve.Options & {
|
|
1911
1826
|
/** Alternative binary names for this CLI. */
|
|
1912
1827
|
aliases?: string[] | undefined
|
|
1828
|
+
config?:
|
|
1829
|
+
| {
|
|
1830
|
+
flag?: string | undefined
|
|
1831
|
+
files?: string[] | undefined
|
|
1832
|
+
loader?:
|
|
1833
|
+
| ((
|
|
1834
|
+
path: string | undefined,
|
|
1835
|
+
) =>
|
|
1836
|
+
| Record<string, unknown>
|
|
1837
|
+
| undefined
|
|
1838
|
+
| Promise<Record<string, unknown> | undefined>)
|
|
1839
|
+
| undefined
|
|
1840
|
+
}
|
|
1841
|
+
| undefined
|
|
1913
1842
|
description?: string | undefined
|
|
1914
1843
|
/** CLI-level env schema. Parsed before middleware runs. */
|
|
1915
1844
|
envSchema?: z.ZodObject<any> | undefined
|
|
@@ -1944,7 +1873,7 @@ declare namespace serveImpl {
|
|
|
1944
1873
|
}
|
|
1945
1874
|
|
|
1946
1875
|
/** @internal Extracts built-in flags (--verbose, --format, --json, --llms, --help, --version) from argv. */
|
|
1947
|
-
function extractBuiltinFlags(argv: string[]) {
|
|
1876
|
+
function extractBuiltinFlags(argv: string[], options: extractBuiltinFlags.Options = {}) {
|
|
1948
1877
|
let verbose = false
|
|
1949
1878
|
let llms = false
|
|
1950
1879
|
let llmsFull = false
|
|
@@ -1954,12 +1883,18 @@ function extractBuiltinFlags(argv: string[]) {
|
|
|
1954
1883
|
let schema = false
|
|
1955
1884
|
let format: Formatter.Format = 'toon'
|
|
1956
1885
|
let formatExplicit = false
|
|
1886
|
+
let configPath: string | undefined
|
|
1887
|
+
let configDisabled = false
|
|
1957
1888
|
let filterOutput: string | undefined
|
|
1958
1889
|
let tokenLimit: number | undefined
|
|
1959
1890
|
let tokenOffset: number | undefined
|
|
1960
1891
|
let tokenCount = false
|
|
1961
1892
|
const rest: string[] = []
|
|
1962
1893
|
|
|
1894
|
+
const cfgFlag = options.configFlag ? `--${options.configFlag}` : undefined
|
|
1895
|
+
const cfgFlagEq = options.configFlag ? `--${options.configFlag}=` : undefined
|
|
1896
|
+
const noCfgFlag = options.configFlag ? `--no-${options.configFlag}` : undefined
|
|
1897
|
+
|
|
1963
1898
|
for (let i = 0; i < argv.length; i++) {
|
|
1964
1899
|
const token = argv[i]!
|
|
1965
1900
|
if (token === '--verbose') verbose = true
|
|
@@ -1976,6 +1911,22 @@ function extractBuiltinFlags(argv: string[]) {
|
|
|
1976
1911
|
format = argv[i + 1] as Formatter.Format
|
|
1977
1912
|
formatExplicit = true
|
|
1978
1913
|
i++
|
|
1914
|
+
} else if (cfgFlag && token === cfgFlag) {
|
|
1915
|
+
const value = argv[i + 1]
|
|
1916
|
+
if (value === undefined)
|
|
1917
|
+
throw new ParseError({ message: `Missing value for flag: ${cfgFlag}` })
|
|
1918
|
+
configPath = value
|
|
1919
|
+
configDisabled = false
|
|
1920
|
+
i++
|
|
1921
|
+
} else if (cfgFlagEq && token.startsWith(cfgFlagEq)) {
|
|
1922
|
+
const value = token.slice(cfgFlagEq.length)
|
|
1923
|
+
if (value.length === 0)
|
|
1924
|
+
throw new ParseError({ message: `Missing value for flag: ${cfgFlag}` })
|
|
1925
|
+
configPath = value
|
|
1926
|
+
configDisabled = false
|
|
1927
|
+
} else if (noCfgFlag && token === noCfgFlag) {
|
|
1928
|
+
configPath = undefined
|
|
1929
|
+
configDisabled = true
|
|
1979
1930
|
} else if (token === '--filter-output' && argv[i + 1]) {
|
|
1980
1931
|
filterOutput = argv[i + 1]!
|
|
1981
1932
|
i++
|
|
@@ -1993,6 +1944,8 @@ function extractBuiltinFlags(argv: string[]) {
|
|
|
1993
1944
|
verbose,
|
|
1994
1945
|
format,
|
|
1995
1946
|
formatExplicit,
|
|
1947
|
+
configPath,
|
|
1948
|
+
configDisabled,
|
|
1996
1949
|
filterOutput,
|
|
1997
1950
|
tokenLimit,
|
|
1998
1951
|
tokenOffset,
|
|
@@ -2007,6 +1960,145 @@ function extractBuiltinFlags(argv: string[]) {
|
|
|
2007
1960
|
}
|
|
2008
1961
|
}
|
|
2009
1962
|
|
|
1963
|
+
declare namespace extractBuiltinFlags {
|
|
1964
|
+
type Options = {
|
|
1965
|
+
configFlag?: string | undefined
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/** @internal Loads config-backed option defaults for the active command. */
|
|
1970
|
+
async function loadCommandOptionDefaults(
|
|
1971
|
+
cli: string,
|
|
1972
|
+
path: string,
|
|
1973
|
+
options: loadCommandOptionDefaults.Options = {},
|
|
1974
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
1975
|
+
if (options.configDisabled) return undefined
|
|
1976
|
+
|
|
1977
|
+
const { loader } = options
|
|
1978
|
+
|
|
1979
|
+
// Resolve the target file path
|
|
1980
|
+
let targetPath: string | undefined
|
|
1981
|
+
if (options.configPath) {
|
|
1982
|
+
targetPath = resolveConfigPath(options.configPath)
|
|
1983
|
+
} else {
|
|
1984
|
+
const searchPaths = options.files ?? [`${cli}.json`]
|
|
1985
|
+
targetPath = await findFirstExisting(searchPaths)
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// Load and parse the config
|
|
1989
|
+
let parsed: Record<string, unknown>
|
|
1990
|
+
if (loader) {
|
|
1991
|
+
const result = await loader(targetPath)
|
|
1992
|
+
if (result === undefined) return undefined
|
|
1993
|
+
if (!isRecord(result))
|
|
1994
|
+
throw new ParseError({ message: 'Config loader must return a plain object or undefined' })
|
|
1995
|
+
parsed = result
|
|
1996
|
+
} else {
|
|
1997
|
+
if (!targetPath) return undefined
|
|
1998
|
+
const result = await readJsonConfig(targetPath, !!options.configPath)
|
|
1999
|
+
if (!result) return undefined
|
|
2000
|
+
parsed = result
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// Extract the command section from the config tree
|
|
2004
|
+
return extractCommandSection(parsed, cli, path)
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
declare namespace loadCommandOptionDefaults {
|
|
2008
|
+
type Options = {
|
|
2009
|
+
configDisabled?: boolean | undefined
|
|
2010
|
+
configPath?: string | undefined
|
|
2011
|
+
files?: string[] | undefined
|
|
2012
|
+
loader?:
|
|
2013
|
+
| ((
|
|
2014
|
+
path: string | undefined,
|
|
2015
|
+
) => Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>)
|
|
2016
|
+
| undefined
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
/** @internal Resolves a config file path, expanding `~` to home dir. */
|
|
2021
|
+
function resolveConfigPath(filePath: string): string {
|
|
2022
|
+
if (filePath.startsWith('~/') || filePath === '~') {
|
|
2023
|
+
return path.join(os.homedir(), filePath.slice(1))
|
|
2024
|
+
}
|
|
2025
|
+
return path.resolve(process.cwd(), filePath)
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
/** @internal Returns the first readable file from a list of paths, or `undefined`. */
|
|
2029
|
+
async function findFirstExisting(paths: string[]): Promise<string | undefined> {
|
|
2030
|
+
for (const p of paths) {
|
|
2031
|
+
const resolved = resolveConfigPath(p)
|
|
2032
|
+
try {
|
|
2033
|
+
await fs.access(resolved, fs.constants.R_OK)
|
|
2034
|
+
return resolved
|
|
2035
|
+
} catch {}
|
|
2036
|
+
}
|
|
2037
|
+
return undefined
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
/** @internal Reads and parses a JSON config file. */
|
|
2041
|
+
async function readJsonConfig(
|
|
2042
|
+
targetPath: string,
|
|
2043
|
+
explicit: boolean,
|
|
2044
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
2045
|
+
let raw: string
|
|
2046
|
+
try {
|
|
2047
|
+
raw = await fs.readFile(targetPath, 'utf8')
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
2050
|
+
if (explicit) throw new ParseError({ message: `Config file not found: ${targetPath}` })
|
|
2051
|
+
return undefined
|
|
2052
|
+
}
|
|
2053
|
+
throw error
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
let parsed: unknown
|
|
2057
|
+
try {
|
|
2058
|
+
parsed = JSON.parse(raw)
|
|
2059
|
+
} catch (error) {
|
|
2060
|
+
throw new ParseError({
|
|
2061
|
+
message: `Invalid JSON config file: ${targetPath}`,
|
|
2062
|
+
cause: error instanceof Error ? error : undefined,
|
|
2063
|
+
})
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
if (!isRecord(parsed))
|
|
2067
|
+
throw new ParseError({
|
|
2068
|
+
message: `Invalid config file: expected a top-level object in ${targetPath}`,
|
|
2069
|
+
})
|
|
2070
|
+
return parsed
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/** @internal Walks the nested config tree to extract option defaults for a command path. */
|
|
2074
|
+
function extractCommandSection(
|
|
2075
|
+
parsed: Record<string, unknown>,
|
|
2076
|
+
cli: string,
|
|
2077
|
+
path: string,
|
|
2078
|
+
): Record<string, unknown> | undefined {
|
|
2079
|
+
const segments = path === cli ? [] : path.split(' ')
|
|
2080
|
+
let node: unknown = parsed
|
|
2081
|
+
for (const seg of segments) {
|
|
2082
|
+
if (!isRecord(node)) return undefined
|
|
2083
|
+
const commands = node.commands
|
|
2084
|
+
if (!isRecord(commands)) return undefined
|
|
2085
|
+
node = commands[seg]
|
|
2086
|
+
if (node === undefined) return undefined
|
|
2087
|
+
}
|
|
2088
|
+
if (!isRecord(node))
|
|
2089
|
+
throw new ParseError({
|
|
2090
|
+
message: `Invalid config section for '${path}': expected an object`,
|
|
2091
|
+
})
|
|
2092
|
+
|
|
2093
|
+
const options = node.options
|
|
2094
|
+
if (options === undefined) return undefined
|
|
2095
|
+
if (!isRecord(options))
|
|
2096
|
+
throw new ParseError({
|
|
2097
|
+
message: `Invalid config 'options' for '${path}': expected an object`,
|
|
2098
|
+
})
|
|
2099
|
+
return Object.keys(options).length > 0 ? options : undefined
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2010
2102
|
/** @internal Collects immediate child commands/groups for help output. */
|
|
2011
2103
|
function collectHelpCommands(
|
|
2012
2104
|
commands: Map<string, CommandEntry>,
|
|
@@ -2018,6 +2110,29 @@ function collectHelpCommands(
|
|
|
2018
2110
|
return result.sort((a, b) => a.name.localeCompare(b.name))
|
|
2019
2111
|
}
|
|
2020
2112
|
|
|
2113
|
+
/** @internal Formats group-level help for a built-in command (e.g. `cli skills`). */
|
|
2114
|
+
function formatBuiltinHelp(cli: string, builtin: (typeof builtinCommands)[number]): string {
|
|
2115
|
+
return Help.formatRoot(`${cli} ${builtin.name}`, {
|
|
2116
|
+
description: builtin.description,
|
|
2117
|
+
commands: builtin.subcommands?.map((s) => ({ name: s.name, description: s.description })),
|
|
2118
|
+
})
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
/** @internal Formats subcommand-level help for a built-in command (e.g. `cli skills add --help`). */
|
|
2122
|
+
function formatBuiltinSubcommandHelp(
|
|
2123
|
+
cli: string,
|
|
2124
|
+
builtin: (typeof builtinCommands)[number],
|
|
2125
|
+
subName: string,
|
|
2126
|
+
): string {
|
|
2127
|
+
const sub = builtin.subcommands?.find((s) => s.name === subName)
|
|
2128
|
+
return Help.formatCommand(`${cli} ${builtin.name} ${subName}`, {
|
|
2129
|
+
alias: sub?.alias,
|
|
2130
|
+
description: sub?.description,
|
|
2131
|
+
hideGlobalOptions: true,
|
|
2132
|
+
options: sub?.options,
|
|
2133
|
+
})
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2021
2136
|
/** @internal Formats help text for a fetch gateway command. */
|
|
2022
2137
|
function formatFetchHelp(name: string, description?: string): string {
|
|
2023
2138
|
const lines: string[] = []
|
|
@@ -2087,7 +2202,13 @@ export const toCommands = new WeakMap<Cli, Map<string, CommandEntry>>()
|
|
|
2087
2202
|
const toMiddlewares = new WeakMap<Cli, MiddlewareHandler[]>()
|
|
2088
2203
|
|
|
2089
2204
|
/** @internal Maps root CLI instances to their command definitions. */
|
|
2090
|
-
const toRootDefinition = new WeakMap<Root, CommandDefinition<any, any, any>>()
|
|
2205
|
+
export const toRootDefinition = new WeakMap<Root, CommandDefinition<any, any, any>>()
|
|
2206
|
+
|
|
2207
|
+
/** @internal Maps CLI instances to their root options schema. */
|
|
2208
|
+
export const toRootOptions = new WeakMap<Cli, z.ZodObject<any>>()
|
|
2209
|
+
|
|
2210
|
+
/** @internal Maps CLI instances to whether config file loading is enabled. */
|
|
2211
|
+
export const toConfigEnabled = new WeakMap<Cli, boolean>()
|
|
2091
2212
|
|
|
2092
2213
|
/** @internal Maps CLI instances to their output policy. */
|
|
2093
2214
|
const toOutputPolicy = new WeakMap<Cli, OutputPolicy>()
|
|
@@ -2155,16 +2276,6 @@ function isSentinel(value: unknown): value is OkResult | ErrorResult {
|
|
|
2155
2276
|
return typeof value === 'object' && value !== null && sentinel in value
|
|
2156
2277
|
}
|
|
2157
2278
|
|
|
2158
|
-
/** @internal Type guard for async generators returned by streaming `run` handlers. */
|
|
2159
|
-
function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unknown, unknown> {
|
|
2160
|
-
return (
|
|
2161
|
-
typeof value === 'object' &&
|
|
2162
|
-
value !== null &&
|
|
2163
|
-
Symbol.asyncIterator in value &&
|
|
2164
|
-
typeof (value as any).next === 'function'
|
|
2165
|
-
)
|
|
2166
|
-
}
|
|
2167
|
-
|
|
2168
2279
|
/** @internal Handles streaming output from an async generator `run` handler. */
|
|
2169
2280
|
async function handleStreaming(
|
|
2170
2281
|
generator: AsyncGenerator<unknown, unknown, unknown>,
|
|
@@ -2649,15 +2760,9 @@ type CommandDefinition<
|
|
|
2649
2760
|
output extends z.ZodType | undefined = undefined,
|
|
2650
2761
|
vars extends z.ZodObject<any> | undefined = undefined,
|
|
2651
2762
|
cliEnv extends z.ZodObject<any> | undefined = undefined,
|
|
2652
|
-
> = {
|
|
2653
|
-
/** Map of option names to single-char aliases. */
|
|
2654
|
-
alias?: options extends z.ZodObject<any>
|
|
2655
|
-
? Partial<Record<keyof z.output<options>, string>>
|
|
2656
|
-
: Record<string, string> | undefined
|
|
2763
|
+
> = CommandMeta<options> & {
|
|
2657
2764
|
/** Zod schema for positional arguments. */
|
|
2658
2765
|
args?: args | undefined
|
|
2659
|
-
/** A short description of what the command does. */
|
|
2660
|
-
description?: string | undefined
|
|
2661
2766
|
/** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
|
|
2662
2767
|
env?: env | undefined
|
|
2663
2768
|
/** Usage examples for this command. */
|
|
@@ -2666,8 +2771,6 @@ type CommandDefinition<
|
|
|
2666
2771
|
format?: Formatter.Format | undefined
|
|
2667
2772
|
/** Plain text hint displayed after examples and before global options. */
|
|
2668
2773
|
hint?: string | undefined
|
|
2669
|
-
/** Zod schema for named options/flags. */
|
|
2670
|
-
options?: options | undefined
|
|
2671
2774
|
/** Zod schema for the command's return value. */
|
|
2672
2775
|
output?: output | undefined
|
|
2673
2776
|
/**
|