incur 0.1.16 → 0.2.0
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 +86 -0
- package/SKILL.md +42 -0
- package/dist/Cli.d.ts +25 -2
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +233 -25
- package/dist/Cli.js.map +1 -1
- package/dist/Fetch.d.ts +26 -0
- package/dist/Fetch.d.ts.map +1 -0
- package/dist/Fetch.js +150 -0
- package/dist/Fetch.js.map +1 -0
- package/dist/Help.d.ts +4 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +11 -9
- package/dist/Help.js.map +1 -1
- package/dist/Openapi.d.ts +20 -0
- package/dist/Openapi.d.ts.map +1 -0
- package/dist/Openapi.js +136 -0
- package/dist/Openapi.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -1
- package/src/Cli.test-d.ts +2 -2
- package/src/Cli.test.ts +206 -2
- package/src/Cli.ts +273 -26
- package/src/Completions.test.ts +49 -0
- package/src/Fetch.test.ts +274 -0
- package/src/Fetch.ts +170 -0
- package/src/Help.test.ts +48 -0
- package/src/Help.ts +13 -6
- package/src/Openapi.test.ts +320 -0
- package/src/Openapi.ts +196 -0
- package/src/bun-compile.test.ts +84 -0
- package/src/e2e.test.ts +179 -5
- package/src/index.ts +2 -0
package/src/Cli.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { z } from 'zod'
|
|
|
3
3
|
import * as Completions from './Completions.js'
|
|
4
4
|
import type { FieldError } from './Errors.js'
|
|
5
5
|
import { IncurError, ValidationError } from './Errors.js'
|
|
6
|
+
import * as Fetch from './Fetch.js'
|
|
7
|
+
import * as Openapi from './Openapi.js'
|
|
6
8
|
import * as Formatter from './Formatter.js'
|
|
7
9
|
import * as Help from './Help.js'
|
|
8
10
|
import { detectRunner } from './internal/pm.js'
|
|
@@ -56,6 +58,11 @@ export type Cli<
|
|
|
56
58
|
vars,
|
|
57
59
|
env
|
|
58
60
|
>
|
|
61
|
+
/** Mounts a fetch handler as a command, optionally with OpenAPI spec for typed subcommands. */
|
|
62
|
+
<const name extends string>(
|
|
63
|
+
name: name,
|
|
64
|
+
definition: { basePath?: string | undefined; description?: string | undefined; fetch: FetchHandler; openapi?: Openapi.OpenAPISpec | undefined; outputPolicy?: OutputPolicy | undefined },
|
|
65
|
+
): Cli<commands, vars, env>
|
|
59
66
|
}
|
|
60
67
|
/** A short description of the CLI. */
|
|
61
68
|
description?: string | undefined
|
|
@@ -174,9 +181,11 @@ export function create(
|
|
|
174
181
|
const name = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name
|
|
175
182
|
const def = typeof nameOrDefinition === 'string' ? (definition ?? {}) : nameOrDefinition
|
|
176
183
|
const rootDef = 'run' in def ? (def as CommandDefinition<any, any, any>) : undefined
|
|
184
|
+
const rootFetch = 'fetch' in def ? (def.fetch as FetchHandler) : undefined
|
|
177
185
|
|
|
178
186
|
const commands = new Map<string, CommandEntry>()
|
|
179
187
|
const middlewares: MiddlewareHandler[] = []
|
|
188
|
+
const pending: Promise<void>[] = []
|
|
180
189
|
|
|
181
190
|
const cli: Cli = {
|
|
182
191
|
name,
|
|
@@ -186,6 +195,30 @@ export function create(
|
|
|
186
195
|
|
|
187
196
|
command(nameOrCli: any, def?: any): any {
|
|
188
197
|
if (typeof nameOrCli === 'string') {
|
|
198
|
+
if (def && 'fetch' in def && typeof def.fetch === 'function') {
|
|
199
|
+
// OpenAPI + fetch → generate typed command group (async, resolved before serve)
|
|
200
|
+
if (def.openapi) {
|
|
201
|
+
pending.push(
|
|
202
|
+
Openapi.generateCommands(def.openapi, def.fetch, { basePath: def.basePath }).then((generated) => {
|
|
203
|
+
commands.set(nameOrCli, {
|
|
204
|
+
_group: true,
|
|
205
|
+
description: def.description,
|
|
206
|
+
commands: generated as Map<string, CommandEntry>,
|
|
207
|
+
...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
|
|
208
|
+
} as InternalGroup)
|
|
209
|
+
}),
|
|
210
|
+
)
|
|
211
|
+
return cli
|
|
212
|
+
}
|
|
213
|
+
commands.set(nameOrCli, {
|
|
214
|
+
_fetch: true,
|
|
215
|
+
basePath: def.basePath,
|
|
216
|
+
description: def.description,
|
|
217
|
+
fetch: def.fetch,
|
|
218
|
+
...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
|
|
219
|
+
} as InternalFetchGateway)
|
|
220
|
+
return cli
|
|
221
|
+
}
|
|
189
222
|
commands.set(nameOrCli, def)
|
|
190
223
|
return cli
|
|
191
224
|
}
|
|
@@ -209,8 +242,10 @@ export function create(
|
|
|
209
242
|
},
|
|
210
243
|
|
|
211
244
|
async serve(argv = process.argv.slice(2), serveOptions: serve.Options = {}) {
|
|
245
|
+
if (pending.length > 0) await Promise.all(pending)
|
|
212
246
|
return serveImpl(name, commands, argv, {
|
|
213
247
|
...serveOptions,
|
|
248
|
+
aliases: def.aliases,
|
|
214
249
|
description: def.description,
|
|
215
250
|
envSchema: def.env,
|
|
216
251
|
format: def.format,
|
|
@@ -218,6 +253,7 @@ export function create(
|
|
|
218
253
|
middlewares,
|
|
219
254
|
outputPolicy: def.outputPolicy,
|
|
220
255
|
rootCommand: rootDef,
|
|
256
|
+
rootFetch,
|
|
221
257
|
sync: def.sync,
|
|
222
258
|
vars: def.vars,
|
|
223
259
|
version: def.version,
|
|
@@ -250,6 +286,8 @@ export declare namespace create {
|
|
|
250
286
|
alias?: options extends z.ZodObject<any>
|
|
251
287
|
? Partial<Record<keyof z.output<options>, string>>
|
|
252
288
|
: Record<string, string> | undefined
|
|
289
|
+
/** Alternative binary names for this CLI (e.g. shorter aliases in package.json `bin`). Shell completions are registered for all names. */
|
|
290
|
+
aliases?: string[] | undefined
|
|
253
291
|
/** Zod schema for positional arguments. */
|
|
254
292
|
args?: args | undefined
|
|
255
293
|
/** A short description of what the CLI does. */
|
|
@@ -258,6 +296,8 @@ export declare namespace create {
|
|
|
258
296
|
env?: env | undefined
|
|
259
297
|
/** Usage examples for this command. */
|
|
260
298
|
examples?: Example<args, options>[] | undefined
|
|
299
|
+
/** A fetch handler to use as the root command. All argv tokens are interpreted as path segments and curl-style flags. */
|
|
300
|
+
fetch?: FetchHandler | undefined
|
|
261
301
|
/** Default output format. Overridden by `--format` or `--json`. */
|
|
262
302
|
format?: Formatter.Format | undefined
|
|
263
303
|
/** Zod schema for named options/flags. */
|
|
@@ -379,8 +419,9 @@ async function serveImpl(
|
|
|
379
419
|
const sepIdx = argv.indexOf('--')
|
|
380
420
|
const words = sepIdx !== -1 ? argv.slice(sepIdx + 1) : argv
|
|
381
421
|
if (words.length === 0) {
|
|
382
|
-
// Registration mode: print shell hook script
|
|
383
|
-
|
|
422
|
+
// Registration mode: print shell hook script for primary name + aliases
|
|
423
|
+
const names = [name, ...(options.aliases ?? [])]
|
|
424
|
+
stdout(names.map((n) => Completions.register(completeShell, n)).join('\n'))
|
|
384
425
|
} else {
|
|
385
426
|
const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1)
|
|
386
427
|
const candidates = Completions.complete(commands, options.rootCommand, words, index)
|
|
@@ -501,7 +542,8 @@ async function serveImpl(
|
|
|
501
542
|
exit(1)
|
|
502
543
|
return
|
|
503
544
|
}
|
|
504
|
-
|
|
545
|
+
const names = [name, ...(options.aliases ?? [])]
|
|
546
|
+
writeln(names.map((n) => Completions.register(shell as Completions.Shell, n)).join('\n'))
|
|
505
547
|
return
|
|
506
548
|
}
|
|
507
549
|
|
|
@@ -668,6 +710,7 @@ async function serveImpl(
|
|
|
668
710
|
writeln(
|
|
669
711
|
Help.formatCommand(name, {
|
|
670
712
|
alias: cmd.alias as Record<string, string> | undefined,
|
|
713
|
+
aliases: options.aliases,
|
|
671
714
|
description: cmd.description ?? options.description,
|
|
672
715
|
version: options.version,
|
|
673
716
|
args: cmd.args,
|
|
@@ -683,11 +726,12 @@ async function serveImpl(
|
|
|
683
726
|
)
|
|
684
727
|
return
|
|
685
728
|
}
|
|
686
|
-
if (options.rootCommand) {
|
|
687
|
-
// Root command with no args — treat as root invocation
|
|
729
|
+
if (options.rootCommand || options.rootFetch) {
|
|
730
|
+
// Root command/fetch with no args — treat as root invocation
|
|
688
731
|
} else {
|
|
689
732
|
writeln(
|
|
690
733
|
Help.formatRoot(name, {
|
|
734
|
+
aliases: options.aliases,
|
|
691
735
|
description: options.description,
|
|
692
736
|
version: options.version,
|
|
693
737
|
commands: collectHelpCommands(commands),
|
|
@@ -701,7 +745,16 @@ async function serveImpl(
|
|
|
701
745
|
const resolved =
|
|
702
746
|
filtered.length === 0 && options.rootCommand
|
|
703
747
|
? { command: options.rootCommand, path: name, rest: [] as string[] }
|
|
704
|
-
:
|
|
748
|
+
: filtered.length === 0 && options.rootFetch
|
|
749
|
+
? { fetchGateway: { _fetch: true as const, fetch: options.rootFetch, description: options.description }, middlewares: [] as MiddlewareHandler[], path: name, rest: [] as string[] }
|
|
750
|
+
: resolveCommand(commands, filtered)
|
|
751
|
+
|
|
752
|
+
// --help on a fetch gateway → show fetch-specific help
|
|
753
|
+
if (help && 'fetchGateway' in resolved) {
|
|
754
|
+
const commandName = resolved.path === name ? name : `${name} ${resolved.path}`
|
|
755
|
+
writeln(formatFetchHelp(commandName, resolved.fetchGateway.description))
|
|
756
|
+
return
|
|
757
|
+
}
|
|
705
758
|
|
|
706
759
|
// --help after a command → show help for that command
|
|
707
760
|
if (help) {
|
|
@@ -717,6 +770,7 @@ async function serveImpl(
|
|
|
717
770
|
writeln(
|
|
718
771
|
Help.formatCommand(name, {
|
|
719
772
|
alias: cmd.alias as Record<string, string> | undefined,
|
|
773
|
+
aliases: options.aliases,
|
|
720
774
|
description: cmd.description ?? options.description,
|
|
721
775
|
version: options.version,
|
|
722
776
|
args: cmd.args,
|
|
@@ -733,6 +787,7 @@ async function serveImpl(
|
|
|
733
787
|
} else {
|
|
734
788
|
writeln(
|
|
735
789
|
Help.formatRoot(helpName, {
|
|
790
|
+
aliases: isRoot ? options.aliases : undefined,
|
|
736
791
|
description: helpDesc,
|
|
737
792
|
version: isRoot ? options.version : undefined,
|
|
738
793
|
commands: collectHelpCommands(helpCmds),
|
|
@@ -740,7 +795,8 @@ async function serveImpl(
|
|
|
740
795
|
}),
|
|
741
796
|
)
|
|
742
797
|
}
|
|
743
|
-
} else {
|
|
798
|
+
} else if ('command' in resolved) {
|
|
799
|
+
const cmd = resolved.command
|
|
744
800
|
const isRootCmd = resolved.path === name
|
|
745
801
|
const commandName = isRootCmd ? name : `${name} ${resolved.path}`
|
|
746
802
|
const helpSubcommands =
|
|
@@ -749,16 +805,17 @@ async function serveImpl(
|
|
|
749
805
|
: undefined
|
|
750
806
|
writeln(
|
|
751
807
|
Help.formatCommand(commandName, {
|
|
752
|
-
alias:
|
|
753
|
-
|
|
808
|
+
alias: cmd.alias as Record<string, string> | undefined,
|
|
809
|
+
aliases: isRootCmd ? options.aliases : undefined,
|
|
810
|
+
description: cmd.description,
|
|
754
811
|
version: isRootCmd ? options.version : undefined,
|
|
755
|
-
args:
|
|
756
|
-
env:
|
|
812
|
+
args: cmd.args,
|
|
813
|
+
env: cmd.env,
|
|
757
814
|
envSource: options.env,
|
|
758
|
-
hint:
|
|
759
|
-
options:
|
|
760
|
-
examples: formatExamples(
|
|
761
|
-
usage:
|
|
815
|
+
hint: cmd.hint,
|
|
816
|
+
options: cmd.options,
|
|
817
|
+
examples: formatExamples(cmd.examples),
|
|
818
|
+
usage: cmd.usage,
|
|
762
819
|
commands: helpSubcommands,
|
|
763
820
|
root: isRootCmd,
|
|
764
821
|
}),
|
|
@@ -780,14 +837,16 @@ async function serveImpl(
|
|
|
780
837
|
const start = performance.now()
|
|
781
838
|
|
|
782
839
|
// Resolve effective format: explicit --format/--json → command default → CLI default → toon
|
|
783
|
-
const resolvedFormat = 'command' in resolved && resolved.command.format
|
|
840
|
+
const resolvedFormat = 'command' in resolved && (resolved as any).command.format
|
|
784
841
|
const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon'
|
|
785
842
|
|
|
786
|
-
// Fall back to root
|
|
843
|
+
// Fall back to root fetch when no subcommand matches
|
|
787
844
|
const effective =
|
|
788
|
-
'error' in resolved && options.
|
|
789
|
-
? {
|
|
790
|
-
: resolved
|
|
845
|
+
'error' in resolved && options.rootFetch && !resolved.path
|
|
846
|
+
? { fetchGateway: { _fetch: true as const, fetch: options.rootFetch, description: options.description }, middlewares: [] as MiddlewareHandler[], path: name, rest: filtered }
|
|
847
|
+
: 'error' in resolved && options.rootCommand && !resolved.path
|
|
848
|
+
? { command: options.rootCommand, path: name, rest: filtered }
|
|
849
|
+
: resolved
|
|
791
850
|
|
|
792
851
|
// Resolve outputPolicy: command/group → CLI-level → default ('all')
|
|
793
852
|
const effectiveOutputPolicy =
|
|
@@ -842,6 +901,116 @@ async function serveImpl(
|
|
|
842
901
|
return
|
|
843
902
|
}
|
|
844
903
|
|
|
904
|
+
// Fetch gateway execution path
|
|
905
|
+
if ('fetchGateway' in effective) {
|
|
906
|
+
const { fetchGateway, path, rest: fetchRest } = effective
|
|
907
|
+
const fetchMiddleware = [
|
|
908
|
+
...(options.middlewares ?? []),
|
|
909
|
+
...((effective as any).middlewares ?? []),
|
|
910
|
+
]
|
|
911
|
+
|
|
912
|
+
const runFetch = async () => {
|
|
913
|
+
const input = Fetch.parseArgv(fetchRest)
|
|
914
|
+
if (fetchGateway.basePath) input.path = fetchGateway.basePath + input.path
|
|
915
|
+
const request = Fetch.buildRequest(input)
|
|
916
|
+
const response = await fetchGateway.fetch(request)
|
|
917
|
+
|
|
918
|
+
// Streaming path — NDJSON responses pipe through handleStreaming
|
|
919
|
+
if (Fetch.isStreamingResponse(response)) {
|
|
920
|
+
const generator = Fetch.parseStreamingResponse(response)
|
|
921
|
+
await handleStreaming(generator, {
|
|
922
|
+
name,
|
|
923
|
+
path,
|
|
924
|
+
start,
|
|
925
|
+
format,
|
|
926
|
+
formatExplicit,
|
|
927
|
+
human,
|
|
928
|
+
renderOutput,
|
|
929
|
+
verbose,
|
|
930
|
+
write,
|
|
931
|
+
writeln,
|
|
932
|
+
exit,
|
|
933
|
+
})
|
|
934
|
+
return
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
const output = await Fetch.parseResponse(response)
|
|
938
|
+
|
|
939
|
+
if (output.ok) {
|
|
940
|
+
write({
|
|
941
|
+
ok: true,
|
|
942
|
+
data: output.data,
|
|
943
|
+
meta: {
|
|
944
|
+
command: path,
|
|
945
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
946
|
+
},
|
|
947
|
+
})
|
|
948
|
+
} else {
|
|
949
|
+
write({
|
|
950
|
+
ok: false,
|
|
951
|
+
error: {
|
|
952
|
+
code: `HTTP_${output.status}`,
|
|
953
|
+
message: typeof output.data === 'object' && output.data !== null && 'message' in output.data
|
|
954
|
+
? String((output.data as any).message)
|
|
955
|
+
: typeof output.data === 'string' ? output.data : `HTTP ${output.status}`,
|
|
956
|
+
},
|
|
957
|
+
meta: {
|
|
958
|
+
command: path,
|
|
959
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
960
|
+
},
|
|
961
|
+
})
|
|
962
|
+
exit(1)
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
try {
|
|
967
|
+
const cliEnv = options.envSchema ? Parser.parseEnv(options.envSchema, options.env ?? process.env) : {}
|
|
968
|
+
if (fetchMiddleware.length > 0) {
|
|
969
|
+
const varsMap: Record<string, unknown> = options.vars ? options.vars.parse({}) : {}
|
|
970
|
+
const errorFn = (opts: { code: string; message: string; retryable?: boolean | undefined; cta?: CtaBlock | undefined }): never =>
|
|
971
|
+
({ [sentinel]: 'error', ...opts }) as never
|
|
972
|
+
const mwCtx: MiddlewareContext = {
|
|
973
|
+
agent: !human,
|
|
974
|
+
command: path,
|
|
975
|
+
env: cliEnv,
|
|
976
|
+
error: errorFn,
|
|
977
|
+
name,
|
|
978
|
+
set(key: string, value: unknown) { varsMap[key] = value },
|
|
979
|
+
var: varsMap,
|
|
980
|
+
version: options.version,
|
|
981
|
+
}
|
|
982
|
+
const handleMwSentinel = (result: unknown) => {
|
|
983
|
+
if (!isSentinel(result) || result[sentinel] !== 'error') return
|
|
984
|
+
const cta = formatCtaBlock(name, result.cta)
|
|
985
|
+
write({
|
|
986
|
+
ok: false,
|
|
987
|
+
error: { code: result.code, message: result.message, ...(result.retryable !== undefined ? { retryable: result.retryable } : undefined) },
|
|
988
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms`, ...(cta ? { cta } : undefined) },
|
|
989
|
+
})
|
|
990
|
+
exit(1)
|
|
991
|
+
}
|
|
992
|
+
const composed = fetchMiddleware.reduceRight(
|
|
993
|
+
(next: () => Promise<void>, mw) => async () => { handleMwSentinel(await mw(mwCtx, next)) },
|
|
994
|
+
runFetch,
|
|
995
|
+
)
|
|
996
|
+
await composed()
|
|
997
|
+
} else {
|
|
998
|
+
await runFetch()
|
|
999
|
+
}
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
write({
|
|
1002
|
+
ok: false,
|
|
1003
|
+
error: {
|
|
1004
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
1005
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1006
|
+
},
|
|
1007
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
|
|
1008
|
+
})
|
|
1009
|
+
exit(1)
|
|
1010
|
+
}
|
|
1011
|
+
return
|
|
1012
|
+
}
|
|
1013
|
+
|
|
845
1014
|
const { command, path, rest } = effective
|
|
846
1015
|
|
|
847
1016
|
// Collect middleware: root CLI + groups traversed + per-command
|
|
@@ -1080,6 +1249,13 @@ function resolveCommand(
|
|
|
1080
1249
|
path: string
|
|
1081
1250
|
rest: string[]
|
|
1082
1251
|
}
|
|
1252
|
+
| {
|
|
1253
|
+
fetchGateway: InternalFetchGateway
|
|
1254
|
+
middlewares: MiddlewareHandler[]
|
|
1255
|
+
outputPolicy?: OutputPolicy | undefined
|
|
1256
|
+
path: string
|
|
1257
|
+
rest: string[]
|
|
1258
|
+
}
|
|
1083
1259
|
| {
|
|
1084
1260
|
help: true
|
|
1085
1261
|
path: string
|
|
@@ -1097,6 +1273,18 @@ function resolveCommand(
|
|
|
1097
1273
|
let inheritedOutputPolicy: OutputPolicy | undefined
|
|
1098
1274
|
const collectedMiddlewares: MiddlewareHandler[] = []
|
|
1099
1275
|
|
|
1276
|
+
// Fetch gateway — all remaining tokens go to the fetch handler
|
|
1277
|
+
if (isFetchGateway(entry)) {
|
|
1278
|
+
const outputPolicy = entry.outputPolicy ?? inheritedOutputPolicy
|
|
1279
|
+
return {
|
|
1280
|
+
fetchGateway: entry,
|
|
1281
|
+
middlewares: collectedMiddlewares,
|
|
1282
|
+
path: path.join(' '),
|
|
1283
|
+
rest: remaining,
|
|
1284
|
+
...(outputPolicy ? { outputPolicy } : undefined),
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1100
1288
|
while (isGroup(entry)) {
|
|
1101
1289
|
if (entry.outputPolicy) inheritedOutputPolicy = entry.outputPolicy
|
|
1102
1290
|
if (entry.middlewares) collectedMiddlewares.push(...entry.middlewares)
|
|
@@ -1117,6 +1305,17 @@ function resolveCommand(
|
|
|
1117
1305
|
path.push(next)
|
|
1118
1306
|
remaining = remaining.slice(1)
|
|
1119
1307
|
entry = child
|
|
1308
|
+
|
|
1309
|
+
if (isFetchGateway(entry)) {
|
|
1310
|
+
const outputPolicy = entry.outputPolicy ?? inheritedOutputPolicy
|
|
1311
|
+
return {
|
|
1312
|
+
fetchGateway: entry,
|
|
1313
|
+
middlewares: collectedMiddlewares,
|
|
1314
|
+
path: path.join(' '),
|
|
1315
|
+
rest: remaining,
|
|
1316
|
+
...(outputPolicy ? { outputPolicy } : undefined),
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1120
1319
|
}
|
|
1121
1320
|
|
|
1122
1321
|
const outputPolicy = entry.outputPolicy ?? inheritedOutputPolicy
|
|
@@ -1132,6 +1331,8 @@ function resolveCommand(
|
|
|
1132
1331
|
/** @internal Options for serveImpl, extending public serve.Options with internal metadata. */
|
|
1133
1332
|
declare namespace serveImpl {
|
|
1134
1333
|
type Options = serve.Options & {
|
|
1334
|
+
/** Alternative binary names for this CLI. */
|
|
1335
|
+
aliases?: string[] | undefined
|
|
1135
1336
|
description?: string | undefined
|
|
1136
1337
|
/** CLI-level env schema. Parsed before middleware runs. */
|
|
1137
1338
|
envSchema?: z.ZodObject<any> | undefined
|
|
@@ -1149,6 +1350,8 @@ declare namespace serveImpl {
|
|
|
1149
1350
|
| undefined
|
|
1150
1351
|
/** Root command handler, invoked when no subcommand matches. */
|
|
1151
1352
|
rootCommand?: CommandDefinition<any, any, any> | undefined
|
|
1353
|
+
/** Root fetch handler, invoked when no subcommand matches and no rootCommand is set. */
|
|
1354
|
+
rootFetch?: FetchHandler | undefined
|
|
1152
1355
|
sync?:
|
|
1153
1356
|
| {
|
|
1154
1357
|
cwd?: string | undefined
|
|
@@ -1200,24 +1403,45 @@ function collectHelpCommands(
|
|
|
1200
1403
|
): { name: string; description?: string | undefined }[] {
|
|
1201
1404
|
const result: { name: string; description?: string | undefined }[] = []
|
|
1202
1405
|
for (const [name, entry] of commands) {
|
|
1203
|
-
|
|
1204
|
-
else result.push({ name, description: entry.description })
|
|
1406
|
+
result.push({ name, description: entry.description })
|
|
1205
1407
|
}
|
|
1206
1408
|
return result.sort((a, b) => a.name.localeCompare(b.name))
|
|
1207
1409
|
}
|
|
1208
1410
|
|
|
1411
|
+
/** @internal Formats help text for a fetch gateway command. */
|
|
1412
|
+
function formatFetchHelp(name: string, description?: string): string {
|
|
1413
|
+
const lines: string[] = []
|
|
1414
|
+
if (description) lines.push(`${name} — ${description}`)
|
|
1415
|
+
else lines.push(name)
|
|
1416
|
+
lines.push('')
|
|
1417
|
+
lines.push(`Usage: ${name} <path> [options]`)
|
|
1418
|
+
lines.push('')
|
|
1419
|
+
lines.push('Path segments are joined into the request URL path.')
|
|
1420
|
+
lines.push('')
|
|
1421
|
+
lines.push('Options:')
|
|
1422
|
+
lines.push(' -X, --method <METHOD> HTTP method (default: GET, POST if body present)')
|
|
1423
|
+
lines.push(' -H, --header "Key: Val" Set a request header (repeatable)')
|
|
1424
|
+
lines.push(' -d, --data <json> Request body (implies POST)')
|
|
1425
|
+
lines.push(' --body <json> Request body (implies POST)')
|
|
1426
|
+
lines.push(' --<key> <value> Query string parameter')
|
|
1427
|
+
return lines.join('\n')
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1209
1430
|
/** Shape of the commands map accumulated through `.command()` chains. */
|
|
1210
1431
|
export type CommandsMap = Record<
|
|
1211
1432
|
string,
|
|
1212
1433
|
{ args: Record<string, unknown>; options: Record<string, unknown> }
|
|
1213
1434
|
>
|
|
1214
1435
|
|
|
1215
|
-
/** @internal Entry stored in a command map — either a leaf definition or a
|
|
1216
|
-
type CommandEntry = CommandDefinition<any, any, any> | InternalGroup
|
|
1436
|
+
/** @internal Entry stored in a command map — either a leaf definition, a group, or a fetch gateway. */
|
|
1437
|
+
type CommandEntry = CommandDefinition<any, any, any> | InternalGroup | InternalFetchGateway
|
|
1217
1438
|
|
|
1218
1439
|
/** Controls when output data is displayed. `'all'` displays to both humans and agents. `'agent-only'` suppresses data output in human/TTY mode. */
|
|
1219
1440
|
export type OutputPolicy = 'agent-only' | 'all'
|
|
1220
1441
|
|
|
1442
|
+
/** A standard Fetch API handler. */
|
|
1443
|
+
type FetchHandler = (req: Request) => Response | Promise<Response>
|
|
1444
|
+
|
|
1221
1445
|
/** @internal A command group's internal storage. */
|
|
1222
1446
|
type InternalGroup = {
|
|
1223
1447
|
_group: true
|
|
@@ -1227,11 +1451,25 @@ type InternalGroup = {
|
|
|
1227
1451
|
commands: Map<string, CommandEntry>
|
|
1228
1452
|
}
|
|
1229
1453
|
|
|
1454
|
+
/** @internal A fetch gateway entry. */
|
|
1455
|
+
type InternalFetchGateway = {
|
|
1456
|
+
_fetch: true
|
|
1457
|
+
basePath?: string | undefined
|
|
1458
|
+
description?: string | undefined
|
|
1459
|
+
fetch: FetchHandler
|
|
1460
|
+
outputPolicy?: OutputPolicy | undefined
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1230
1463
|
/** @internal Type guard for command groups. */
|
|
1231
1464
|
function isGroup(entry: CommandEntry): entry is InternalGroup {
|
|
1232
1465
|
return '_group' in entry
|
|
1233
1466
|
}
|
|
1234
1467
|
|
|
1468
|
+
/** @internal Type guard for fetch gateways. */
|
|
1469
|
+
function isFetchGateway(entry: CommandEntry): entry is InternalFetchGateway {
|
|
1470
|
+
return '_fetch' in entry
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1235
1473
|
/** @internal Maps CLI instances to their command maps. */
|
|
1236
1474
|
export const toCommands = new WeakMap<Cli, Map<string, CommandEntry>>()
|
|
1237
1475
|
|
|
@@ -1558,7 +1796,11 @@ function collectCommands(
|
|
|
1558
1796
|
const result: ReturnType<typeof collectCommands> = []
|
|
1559
1797
|
for (const [name, entry] of commands) {
|
|
1560
1798
|
const path = [...prefix, name]
|
|
1561
|
-
if (
|
|
1799
|
+
if (isFetchGateway(entry)) {
|
|
1800
|
+
const cmd: (typeof result)[number] = { name: path.join(' ') }
|
|
1801
|
+
if (entry.description) cmd.description = entry.description
|
|
1802
|
+
result.push(cmd)
|
|
1803
|
+
} else if (isGroup(entry)) {
|
|
1562
1804
|
result.push(...collectCommands(entry.commands, path))
|
|
1563
1805
|
} else {
|
|
1564
1806
|
const cmd: (typeof result)[number] = { name: path.join(' ') }
|
|
@@ -1597,7 +1839,12 @@ function collectSkillCommands(
|
|
|
1597
1839
|
const result: Skill.CommandInfo[] = []
|
|
1598
1840
|
for (const [name, entry] of commands) {
|
|
1599
1841
|
const path = [...prefix, name]
|
|
1600
|
-
if (
|
|
1842
|
+
if (isFetchGateway(entry)) {
|
|
1843
|
+
const cmd: Skill.CommandInfo = { name: path.join(' ') }
|
|
1844
|
+
if (entry.description) cmd.description = entry.description
|
|
1845
|
+
cmd.hint = 'Fetch gateway. Pass path segments and curl-style flags (-X, -H, -d, --key value).'
|
|
1846
|
+
result.push(cmd)
|
|
1847
|
+
} else if (isGroup(entry)) {
|
|
1601
1848
|
if (entry.description) groups.set(path.join(' '), entry.description)
|
|
1602
1849
|
result.push(...collectSkillCommands(entry.commands, path, groups))
|
|
1603
1850
|
} else {
|
package/src/Completions.test.ts
CHANGED
|
@@ -324,6 +324,55 @@ describe('completions built-in command', () => {
|
|
|
324
324
|
})
|
|
325
325
|
})
|
|
326
326
|
|
|
327
|
+
describe('aliases', () => {
|
|
328
|
+
function makeAliasedCli() {
|
|
329
|
+
const cli = Cli.create('my-tool', {
|
|
330
|
+
version: '1.0.0',
|
|
331
|
+
description: 'A test CLI',
|
|
332
|
+
aliases: ['mt', 'myt'],
|
|
333
|
+
})
|
|
334
|
+
cli.command('fetch', {
|
|
335
|
+
description: 'Fetch a URL',
|
|
336
|
+
run() {
|
|
337
|
+
return { ok: true }
|
|
338
|
+
},
|
|
339
|
+
})
|
|
340
|
+
return cli
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
test('completions fish outputs registration for all names', async () => {
|
|
344
|
+
const cli = makeAliasedCli()
|
|
345
|
+
const output = await serve(cli, ['completions', 'fish'])
|
|
346
|
+
expect(output).toContain('--command my-tool')
|
|
347
|
+
expect(output).toContain('--command mt')
|
|
348
|
+
expect(output).toContain('--command myt')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
test('completions bash outputs registration for all names', async () => {
|
|
352
|
+
const cli = makeAliasedCli()
|
|
353
|
+
const output = await serve(cli, ['completions', 'bash'])
|
|
354
|
+
expect(output).toContain('_incur_complete_my_tool()')
|
|
355
|
+
expect(output).toContain('_incur_complete_mt()')
|
|
356
|
+
expect(output).toContain('_incur_complete_myt()')
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('completions zsh outputs registration for all names', async () => {
|
|
360
|
+
const cli = makeAliasedCli()
|
|
361
|
+
const output = await serve(cli, ['completions', 'zsh'])
|
|
362
|
+
expect(output).toContain('#compdef my-tool')
|
|
363
|
+
expect(output).toContain('compdef _incur_complete_mt mt')
|
|
364
|
+
expect(output).toContain('compdef _incur_complete_myt myt')
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
test('COMPLETE env var registers all names', async () => {
|
|
368
|
+
const cli = makeAliasedCli()
|
|
369
|
+
const output = await serve(cli, [], { COMPLETE: 'fish' })
|
|
370
|
+
expect(output).toContain('--command my-tool')
|
|
371
|
+
expect(output).toContain('--command mt')
|
|
372
|
+
expect(output).toContain('--command myt')
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
327
376
|
describe('serve integration', () => {
|
|
328
377
|
test('COMPLETE=bash with no words outputs registration script', async () => {
|
|
329
378
|
const cli = makeCli()
|