incur 0.4.7 → 0.4.9
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/dist/Cli.d.ts +65 -19
- package/dist/Cli.d.ts.map +1 -1
- package/dist/Cli.js +294 -66
- package/dist/Cli.js.map +1 -1
- package/dist/Completions.d.ts +2 -1
- package/dist/Completions.d.ts.map +1 -1
- package/dist/Completions.js +52 -13
- package/dist/Completions.js.map +1 -1
- package/dist/Formatter.d.ts.map +1 -1
- package/dist/Formatter.js +6 -5
- package/dist/Formatter.js.map +1 -1
- package/dist/Help.d.ts +5 -0
- package/dist/Help.d.ts.map +1 -1
- package/dist/Help.js +19 -5
- package/dist/Help.js.map +1 -1
- package/dist/Mcp.d.ts +21 -0
- package/dist/Mcp.d.ts.map +1 -1
- package/dist/Mcp.js +25 -8
- package/dist/Mcp.js.map +1 -1
- package/dist/Openapi.d.ts +14 -0
- package/dist/Openapi.d.ts.map +1 -1
- package/dist/Openapi.js +85 -1
- package/dist/Openapi.js.map +1 -1
- package/dist/Parser.d.ts +14 -0
- package/dist/Parser.d.ts.map +1 -1
- package/dist/Parser.js +127 -1
- package/dist/Parser.js.map +1 -1
- package/dist/Skill.d.ts.map +1 -1
- package/dist/Skill.js +5 -2
- package/dist/Skill.js.map +1 -1
- package/dist/Skillgen.d.ts.map +1 -1
- package/dist/Skillgen.js +4 -38
- package/dist/Skillgen.js.map +1 -1
- package/dist/SyncMcp.d.ts.map +1 -1
- package/dist/SyncMcp.js +75 -8
- package/dist/SyncMcp.js.map +1 -1
- package/dist/SyncSkills.d.ts +9 -0
- package/dist/SyncSkills.d.ts.map +1 -1
- package/dist/SyncSkills.js +13 -74
- package/dist/SyncSkills.js.map +1 -1
- package/dist/bin.d.ts +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/internal/command.d.ts +2 -0
- package/dist/internal/command.d.ts.map +1 -1
- package/dist/internal/command.js +6 -5
- package/dist/internal/command.js.map +1 -1
- package/dist/internal/cta.d.ts +22 -0
- package/dist/internal/cta.d.ts.map +1 -0
- package/dist/internal/cta.js +25 -0
- package/dist/internal/cta.js.map +1 -0
- package/dist/internal/json.d.ts +5 -0
- package/dist/internal/json.d.ts.map +1 -0
- package/dist/internal/json.js +13 -0
- package/dist/internal/json.js.map +1 -0
- package/dist/internal/yaml.d.ts +12 -0
- package/dist/internal/yaml.d.ts.map +1 -0
- package/dist/internal/yaml.js +20 -0
- package/dist/internal/yaml.js.map +1 -0
- package/dist/middleware.d.ts +8 -4
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +1 -1
- package/dist/middleware.js.map +1 -1
- package/package.json +1 -1
- package/src/Cli.test-d.ts +71 -0
- package/src/Cli.test.ts +1164 -65
- package/src/Cli.ts +456 -101
- package/src/Completions.test.ts +84 -0
- package/src/Completions.ts +48 -9
- package/src/Formatter.test.ts +17 -0
- package/src/Formatter.ts +7 -5
- package/src/Help.test.ts +31 -0
- package/src/Help.ts +42 -4
- package/src/Mcp.test.ts +193 -0
- package/src/Mcp.ts +49 -8
- package/src/Openapi.test.ts +179 -0
- package/src/Openapi.ts +122 -2
- package/src/Parser.test.ts +173 -0
- package/src/Parser.ts +132 -1
- package/src/Skill.test.ts +6 -0
- package/src/Skill.ts +8 -5
- package/src/Skillgen.test.ts +55 -6
- package/src/Skillgen.ts +9 -35
- package/src/SyncMcp.test.ts +89 -0
- package/src/SyncMcp.ts +72 -8
- package/src/SyncSkills.test.ts +78 -1
- package/src/SyncSkills.ts +20 -78
- package/src/bun-compile.test.ts +5 -0
- package/src/e2e.test.ts +162 -8
- package/src/internal/command.ts +8 -4
- package/src/internal/cta.ts +58 -0
- package/src/internal/json.ts +16 -0
- package/src/internal/yaml.ts +23 -0
- package/src/lazy-imports.test.ts +94 -0
- package/src/middleware.ts +12 -3
package/dist/Cli.js
CHANGED
|
@@ -2,7 +2,6 @@ import * as fs from 'node:fs/promises';
|
|
|
2
2
|
import * as os from 'node:os';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { estimateTokenCount, sliceByTokens } from 'tokenx';
|
|
5
|
-
import { parse as yamlParse, stringify as yamlStringify } from 'yaml';
|
|
6
5
|
import { z } from 'zod';
|
|
7
6
|
import * as Completions from './Completions.js';
|
|
8
7
|
import { IncurError, ParseError, ValidationError } from './Errors.js';
|
|
@@ -12,8 +11,11 @@ import * as Formatter from './Formatter.js';
|
|
|
12
11
|
import * as Help from './Help.js';
|
|
13
12
|
import { builtinCommands, findBuiltin, findBuiltinSubcommand, shells, } from './internal/command.js';
|
|
14
13
|
import * as Command from './internal/command.js';
|
|
14
|
+
import { formatCtaBlock } from './internal/cta.js';
|
|
15
15
|
import { isRecord, suggest, toKebab } from './internal/helpers.js';
|
|
16
|
+
import * as Json from './internal/json.js';
|
|
16
17
|
import { detectRunner } from './internal/pm.js';
|
|
18
|
+
import * as Yaml from './internal/yaml.js';
|
|
17
19
|
import * as Mcp from './Mcp.js';
|
|
18
20
|
import * as Openapi from './Openapi.js';
|
|
19
21
|
import * as Parser from './Parser.js';
|
|
@@ -21,6 +23,7 @@ import * as Schema from './Schema.js';
|
|
|
21
23
|
import * as Skill from './Skill.js';
|
|
22
24
|
import * as SyncMcp from './SyncMcp.js';
|
|
23
25
|
import * as SyncSkills from './SyncSkills.js';
|
|
26
|
+
const destructiveCommandHint = 'Confirm with the user before executing this destructive command.';
|
|
24
27
|
export function create(nameOrDefinition, definition) {
|
|
25
28
|
const name = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
|
|
26
29
|
const def = typeof nameOrDefinition === 'string' ? (definition ?? {}) : nameOrDefinition;
|
|
@@ -31,7 +34,9 @@ export function create(nameOrDefinition, definition) {
|
|
|
31
34
|
const commands = new Map();
|
|
32
35
|
const middlewares = [];
|
|
33
36
|
const pending = [];
|
|
34
|
-
const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0'
|
|
37
|
+
const mcpHandler = createMcpHttpHandler(name, def.version ?? '0.0.0', {
|
|
38
|
+
stateless: def.mcp?.stateless,
|
|
39
|
+
});
|
|
35
40
|
if (def.openapi && rootFetch) {
|
|
36
41
|
pending.push((async () => {
|
|
37
42
|
const spec = await Openapi.resolve(def.openapi, { baseUrl: rootFetchBaseUrl });
|
|
@@ -61,12 +66,14 @@ export function create(nameOrDefinition, definition) {
|
|
|
61
66
|
basePath: def.basePath,
|
|
62
67
|
config: def.openapiConfig,
|
|
63
68
|
});
|
|
64
|
-
|
|
69
|
+
const entry = {
|
|
65
70
|
_group: true,
|
|
66
71
|
description: def.description,
|
|
67
72
|
commands: generated,
|
|
68
73
|
...(def.outputPolicy ? { outputPolicy: def.outputPolicy } : undefined),
|
|
69
|
-
}
|
|
74
|
+
};
|
|
75
|
+
assertNoGlobalOptionConflicts(nameOrCli, entry, toGlobals.get(cli));
|
|
76
|
+
commands.set(nameOrCli, entry);
|
|
70
77
|
})());
|
|
71
78
|
return cli;
|
|
72
79
|
}
|
|
@@ -79,6 +86,7 @@ export function create(nameOrDefinition, definition) {
|
|
|
79
86
|
});
|
|
80
87
|
return cli;
|
|
81
88
|
}
|
|
89
|
+
assertNoGlobalOptionConflicts(nameOrCli, def, toGlobals.get(cli));
|
|
82
90
|
commands.set(nameOrCli, def);
|
|
83
91
|
if (def.aliases)
|
|
84
92
|
for (const a of def.aliases)
|
|
@@ -87,6 +95,7 @@ export function create(nameOrDefinition, definition) {
|
|
|
87
95
|
}
|
|
88
96
|
const mountedRootDef = toRootDefinition.get(nameOrCli);
|
|
89
97
|
if (mountedRootDef) {
|
|
98
|
+
assertNoGlobalOptionConflicts(nameOrCli.name, mountedRootDef, toGlobals.get(cli));
|
|
90
99
|
commands.set(nameOrCli.name, mountedRootDef);
|
|
91
100
|
const rootAliases = toRootAliases.get(nameOrCli);
|
|
92
101
|
if (rootAliases)
|
|
@@ -98,21 +107,25 @@ export function create(nameOrDefinition, definition) {
|
|
|
98
107
|
const subCommands = toCommands.get(sub);
|
|
99
108
|
const subOutputPolicy = toOutputPolicy.get(sub);
|
|
100
109
|
const subMiddlewares = toMiddlewares.get(sub);
|
|
101
|
-
|
|
110
|
+
const entry = {
|
|
102
111
|
_group: true,
|
|
103
112
|
description: sub.description,
|
|
104
113
|
commands: subCommands,
|
|
105
114
|
...(subOutputPolicy ? { outputPolicy: subOutputPolicy } : undefined),
|
|
106
115
|
...(subMiddlewares?.length ? { middlewares: subMiddlewares } : undefined),
|
|
107
|
-
}
|
|
116
|
+
};
|
|
117
|
+
assertNoGlobalOptionConflicts(sub.name, entry, toGlobals.get(cli));
|
|
118
|
+
commands.set(sub.name, entry);
|
|
108
119
|
return cli;
|
|
109
120
|
},
|
|
110
121
|
async fetch(req) {
|
|
111
122
|
if (pending.length > 0)
|
|
112
123
|
await Promise.all(pending);
|
|
124
|
+
const globalsDesc = toGlobals.get(cli);
|
|
113
125
|
return fetchImpl(name, commands, req, {
|
|
114
126
|
description: def.description,
|
|
115
127
|
envSchema: def.env,
|
|
128
|
+
globals: globalsDesc,
|
|
116
129
|
mcpHandler,
|
|
117
130
|
middlewares,
|
|
118
131
|
name,
|
|
@@ -124,13 +137,16 @@ export function create(nameOrDefinition, definition) {
|
|
|
124
137
|
async serve(argv = process.argv.slice(2), serveOptions = {}) {
|
|
125
138
|
if (pending.length > 0)
|
|
126
139
|
await Promise.all(pending);
|
|
140
|
+
const globalsDesc = toGlobals.get(cli);
|
|
127
141
|
return serveImpl(name, commands, argv, {
|
|
128
142
|
...serveOptions,
|
|
129
143
|
aliases: def.aliases,
|
|
144
|
+
banner: def.banner,
|
|
130
145
|
config: def.config,
|
|
131
146
|
description: def.description,
|
|
132
147
|
envSchema: def.env,
|
|
133
148
|
format: def.format,
|
|
149
|
+
globals: globalsDesc,
|
|
134
150
|
mcp: def.mcp,
|
|
135
151
|
middlewares,
|
|
136
152
|
outputPolicy: def.outputPolicy,
|
|
@@ -156,6 +172,40 @@ export function create(nameOrDefinition, definition) {
|
|
|
156
172
|
toConfigEnabled.set(cli, true);
|
|
157
173
|
if (def.outputPolicy)
|
|
158
174
|
toOutputPolicy.set(cli, def.outputPolicy);
|
|
175
|
+
if (def.globals) {
|
|
176
|
+
toGlobals.set(cli, { schema: def.globals, alias: def.globalAlias });
|
|
177
|
+
const builtinNames = [
|
|
178
|
+
'verbose',
|
|
179
|
+
'format',
|
|
180
|
+
'json',
|
|
181
|
+
'llms',
|
|
182
|
+
'llmsFull',
|
|
183
|
+
'mcp',
|
|
184
|
+
'help',
|
|
185
|
+
'version',
|
|
186
|
+
'schema',
|
|
187
|
+
'filterOutput',
|
|
188
|
+
'tokenLimit',
|
|
189
|
+
'tokenOffset',
|
|
190
|
+
'tokenCount',
|
|
191
|
+
...(def.config?.flag
|
|
192
|
+
? [def.config.flag, `no${def.config.flag[0].toUpperCase()}${def.config.flag.slice(1)}`]
|
|
193
|
+
: []),
|
|
194
|
+
];
|
|
195
|
+
const globalKeys = Object.keys(def.globals.shape);
|
|
196
|
+
for (const key of globalKeys) {
|
|
197
|
+
if (builtinNames.includes(key))
|
|
198
|
+
throw new Error(`Global option '${key}' conflicts with a built-in flag. Choose a different name.`);
|
|
199
|
+
}
|
|
200
|
+
// Check globalAlias values against reserved short aliases
|
|
201
|
+
const reservedShorts = new Set(['h']);
|
|
202
|
+
if (def.globalAlias) {
|
|
203
|
+
for (const [name, short] of Object.entries(def.globalAlias)) {
|
|
204
|
+
if (reservedShorts.has(short))
|
|
205
|
+
throw new Error(`Global alias '-${short}' for '${name}' conflicts with a built-in short flag. Choose a different alias.`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
159
209
|
toMiddlewares.set(cli, middlewares);
|
|
160
210
|
toCommands.set(cli, commands);
|
|
161
211
|
return cli;
|
|
@@ -172,6 +222,22 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
172
222
|
function writeln(s) {
|
|
173
223
|
stdout(s.endsWith('\n') ? s : `${s}\n`);
|
|
174
224
|
}
|
|
225
|
+
async function writeBanner() {
|
|
226
|
+
if (!options.banner || help)
|
|
227
|
+
return;
|
|
228
|
+
const banner = typeof options.banner === 'function'
|
|
229
|
+
? { render: options.banner, mode: 'all' }
|
|
230
|
+
: options.banner;
|
|
231
|
+
const mode = banner.mode ?? 'all';
|
|
232
|
+
if (mode !== 'all' && mode !== (human ? 'human' : 'agent'))
|
|
233
|
+
return;
|
|
234
|
+
try {
|
|
235
|
+
const text = await banner.render();
|
|
236
|
+
if (text)
|
|
237
|
+
writeln(text);
|
|
238
|
+
}
|
|
239
|
+
catch { }
|
|
240
|
+
}
|
|
175
241
|
let builtinFlags;
|
|
176
242
|
try {
|
|
177
243
|
builtinFlags = extractBuiltinFlags(argv, { configFlag });
|
|
@@ -185,7 +251,36 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
185
251
|
exit(1);
|
|
186
252
|
return;
|
|
187
253
|
}
|
|
188
|
-
const { fullOutput, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, configPath, configDisabled, rest
|
|
254
|
+
const { fullOutput, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, configPath, configDisabled, rest, } = builtinFlags;
|
|
255
|
+
let globals = {};
|
|
256
|
+
let filtered = rest;
|
|
257
|
+
function parseGlobalOptions(validate) {
|
|
258
|
+
if (!options.globals)
|
|
259
|
+
return true;
|
|
260
|
+
try {
|
|
261
|
+
const result = Parser.parseGlobals(rest, options.globals.schema, options.globals.alias, {
|
|
262
|
+
validate,
|
|
263
|
+
});
|
|
264
|
+
if (validate)
|
|
265
|
+
globals = result.parsed;
|
|
266
|
+
filtered = result.rest;
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
271
|
+
if (human)
|
|
272
|
+
writeln(formatHumanError({ code: 'UNKNOWN', message }));
|
|
273
|
+
else
|
|
274
|
+
writeln(Formatter.format({ code: 'UNKNOWN', message }, 'toon'));
|
|
275
|
+
exit(1);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!parseGlobalOptions(false))
|
|
280
|
+
return;
|
|
281
|
+
// Pre-load yaml for the sync formatting paths below (yaml is loaded lazily -- see internal/yaml.ts).
|
|
282
|
+
if (formatFlag === 'yaml')
|
|
283
|
+
await Yaml.load();
|
|
189
284
|
// --mcp: start as MCP stdio server
|
|
190
285
|
if (mcpFlag) {
|
|
191
286
|
await Mcp.serve(name, options.version ?? '0.0.0', commands, {
|
|
@@ -193,6 +288,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
193
288
|
env: options.envSchema,
|
|
194
289
|
vars: options.vars,
|
|
195
290
|
version: options.version,
|
|
291
|
+
...(options.mcp?.instructions ? { instructions: options.mcp.instructions } : undefined),
|
|
196
292
|
});
|
|
197
293
|
return;
|
|
198
294
|
}
|
|
@@ -209,7 +305,9 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
209
305
|
}
|
|
210
306
|
else {
|
|
211
307
|
const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1);
|
|
212
|
-
const candidates = Completions.complete(commands, options.rootCommand, words, index
|
|
308
|
+
const candidates = Completions.complete(commands, options.rootCommand, words, index, options.globals
|
|
309
|
+
? { schema: options.globals.schema, alias: options.globals.alias }
|
|
310
|
+
: undefined);
|
|
213
311
|
// Add built-in commands (completions, mcp, skills) to completions
|
|
214
312
|
const current = words[index] ?? '';
|
|
215
313
|
const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'));
|
|
@@ -282,25 +380,29 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
282
380
|
}
|
|
283
381
|
}
|
|
284
382
|
const scopedRoot = prefix.length === 0 ? options.rootCommand : undefined;
|
|
383
|
+
// Markdown skill output renders scopedName separately. Passing prefix again
|
|
384
|
+
// to those collect helpers would double the group segment in command names
|
|
385
|
+
// (e.g. "cli auth auth login" instead of "cli auth login").
|
|
386
|
+
const collectPrefix = prefix.length > 0 ? [] : prefix;
|
|
285
387
|
if (llmsFull) {
|
|
286
388
|
if (!formatExplicit || formatFlag === 'md') {
|
|
287
389
|
const groups = new Map();
|
|
288
|
-
const cmds = collectSkillCommands(scopedCommands,
|
|
390
|
+
const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups, scopedRoot);
|
|
289
391
|
const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name;
|
|
290
392
|
writeln(Skill.generate(scopedName, cmds, groups));
|
|
291
393
|
return;
|
|
292
394
|
}
|
|
293
|
-
writeln(Formatter.format(buildManifest(scopedCommands, prefix), formatFlag));
|
|
395
|
+
writeln(Formatter.format(buildManifest(scopedCommands, prefix, options.globals?.schema), formatFlag));
|
|
294
396
|
return;
|
|
295
397
|
}
|
|
296
398
|
if (!formatExplicit || formatFlag === 'md') {
|
|
297
399
|
const groups = new Map();
|
|
298
|
-
const cmds = collectSkillCommands(scopedCommands,
|
|
400
|
+
const cmds = collectSkillCommands(scopedCommands, collectPrefix, groups, scopedRoot);
|
|
299
401
|
const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name;
|
|
300
402
|
writeln(Skill.index(scopedName, cmds, scopedDescription));
|
|
301
403
|
return;
|
|
302
404
|
}
|
|
303
|
-
writeln(Formatter.format(buildIndexManifest(scopedCommands, prefix), formatFlag));
|
|
405
|
+
writeln(Formatter.format(buildIndexManifest(scopedCommands, prefix, options.globals?.schema), formatFlag));
|
|
304
406
|
return;
|
|
305
407
|
}
|
|
306
408
|
// completions <shell>: print shell hook script to stdout
|
|
@@ -556,11 +658,13 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
556
658
|
hasRequiredArgs(options.rootCommand.args)) {
|
|
557
659
|
// Root command with args but none provided (human mode) — show help
|
|
558
660
|
const cmd = options.rootCommand;
|
|
661
|
+
await writeBanner();
|
|
559
662
|
writeln(Help.formatCommand(name, {
|
|
560
663
|
alias: cmd.alias,
|
|
561
664
|
aliases: options.aliases,
|
|
562
665
|
configFlag,
|
|
563
666
|
description: cmd.description ?? options.description,
|
|
667
|
+
globals: options.globals,
|
|
564
668
|
version: options.version,
|
|
565
669
|
args: cmd.args,
|
|
566
670
|
env: cmd.env,
|
|
@@ -578,10 +682,12 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
578
682
|
// Root command/fetch with no args — treat as root invocation
|
|
579
683
|
}
|
|
580
684
|
else {
|
|
685
|
+
await writeBanner();
|
|
581
686
|
writeln(Help.formatRoot(name, {
|
|
582
687
|
aliases: options.aliases,
|
|
583
688
|
configFlag,
|
|
584
689
|
description: options.description,
|
|
690
|
+
globals: options.globals,
|
|
585
691
|
version: options.version,
|
|
586
692
|
commands: collectHelpCommands(commands),
|
|
587
693
|
root: true,
|
|
@@ -635,6 +741,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
635
741
|
aliases: options.aliases,
|
|
636
742
|
configFlag,
|
|
637
743
|
description: cmd.description ?? options.description,
|
|
744
|
+
globals: options.globals,
|
|
638
745
|
version: options.version,
|
|
639
746
|
args: cmd.args,
|
|
640
747
|
env: cmd.env,
|
|
@@ -652,6 +759,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
652
759
|
aliases: isRoot ? options.aliases : undefined,
|
|
653
760
|
configFlag,
|
|
654
761
|
description: helpDesc,
|
|
762
|
+
globals: options.globals,
|
|
655
763
|
version: isRoot ? options.version : undefined,
|
|
656
764
|
commands: collectHelpCommands(helpCmds),
|
|
657
765
|
root: isRoot,
|
|
@@ -670,6 +778,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
670
778
|
aliases: isRootCmd ? options.aliases : cmd.aliases,
|
|
671
779
|
configFlag,
|
|
672
780
|
description: cmd.description,
|
|
781
|
+
globals: options.globals,
|
|
673
782
|
version: isRootCmd ? options.version : undefined,
|
|
674
783
|
args: cmd.args,
|
|
675
784
|
env: cmd.env,
|
|
@@ -690,6 +799,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
690
799
|
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
691
800
|
configFlag,
|
|
692
801
|
description: resolved.description,
|
|
802
|
+
globals: options.globals,
|
|
693
803
|
commands: collectHelpCommands(resolved.commands),
|
|
694
804
|
}));
|
|
695
805
|
return;
|
|
@@ -718,6 +828,8 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
718
828
|
result.options = Schema.toJsonSchema(cmd.options);
|
|
719
829
|
if (cmd.output)
|
|
720
830
|
result.output = Schema.toJsonSchema(cmd.output);
|
|
831
|
+
if (options.globals?.schema)
|
|
832
|
+
result.globals = Schema.toJsonSchema(options.globals.schema);
|
|
721
833
|
writeln(Formatter.format(result, format));
|
|
722
834
|
return;
|
|
723
835
|
}
|
|
@@ -725,6 +837,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
725
837
|
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
726
838
|
configFlag,
|
|
727
839
|
description: resolved.description,
|
|
840
|
+
globals: options.globals,
|
|
728
841
|
commands: collectHelpCommands(resolved.commands),
|
|
729
842
|
}));
|
|
730
843
|
return;
|
|
@@ -733,6 +846,8 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
733
846
|
// Resolve effective format: explicit --format/--json → command default → CLI default → toon
|
|
734
847
|
const resolvedFormat = 'command' in resolved && resolved.command.format;
|
|
735
848
|
const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon';
|
|
849
|
+
if (format === 'yaml')
|
|
850
|
+
await Yaml.load();
|
|
736
851
|
// Fall back to root fetch/command when no subcommand matches,
|
|
737
852
|
// but only if the token doesn't look like a typo of a known command.
|
|
738
853
|
const rootFallbackBlocked = 'error' in resolved &&
|
|
@@ -888,6 +1003,8 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
888
1003
|
}
|
|
889
1004
|
// Fetch gateway execution path
|
|
890
1005
|
if ('fetchGateway' in effective) {
|
|
1006
|
+
if (!parseGlobalOptions(true))
|
|
1007
|
+
return;
|
|
891
1008
|
const { fetchGateway, path, rest: fetchRest } = effective;
|
|
892
1009
|
const fetchMiddleware = [
|
|
893
1010
|
...(options.middlewares ?? []),
|
|
@@ -963,6 +1080,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
963
1080
|
error: errorFn,
|
|
964
1081
|
format,
|
|
965
1082
|
formatExplicit,
|
|
1083
|
+
globals,
|
|
966
1084
|
name,
|
|
967
1085
|
set(key, value) {
|
|
968
1086
|
varsMap[key] = value;
|
|
@@ -1012,7 +1130,9 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
1012
1130
|
}
|
|
1013
1131
|
return;
|
|
1014
1132
|
}
|
|
1015
|
-
const { command, path, rest } = effective;
|
|
1133
|
+
const { command, path, rest: commandRest } = effective;
|
|
1134
|
+
if (!parseGlobalOptions(true))
|
|
1135
|
+
return;
|
|
1016
1136
|
// Collect middleware: root CLI + groups traversed + per-command
|
|
1017
1137
|
const allMiddleware = [
|
|
1018
1138
|
...(options.middlewares ?? []),
|
|
@@ -1022,7 +1142,7 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
1022
1142
|
...(command.middleware ?? []),
|
|
1023
1143
|
];
|
|
1024
1144
|
if (human)
|
|
1025
|
-
emitDeprecationWarnings(
|
|
1145
|
+
emitDeprecationWarnings(commandRest, command.options, command.alias);
|
|
1026
1146
|
let defaults;
|
|
1027
1147
|
if (configEnabled) {
|
|
1028
1148
|
try {
|
|
@@ -1048,13 +1168,14 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
1048
1168
|
}
|
|
1049
1169
|
const result = await Command.execute(command, {
|
|
1050
1170
|
agent: !human,
|
|
1051
|
-
argv:
|
|
1171
|
+
argv: commandRest,
|
|
1052
1172
|
defaults,
|
|
1053
1173
|
displayName,
|
|
1054
1174
|
env: options.envSchema,
|
|
1055
1175
|
envSource: options.env,
|
|
1056
1176
|
format,
|
|
1057
1177
|
formatExplicit,
|
|
1178
|
+
globals,
|
|
1058
1179
|
inputOptions: {},
|
|
1059
1180
|
middlewares: allMiddleware,
|
|
1060
1181
|
name,
|
|
@@ -1123,11 +1244,14 @@ async function serveImpl(name, commands, argv, options = {}) {
|
|
|
1123
1244
|
}
|
|
1124
1245
|
}
|
|
1125
1246
|
/** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
|
|
1126
|
-
function createMcpHttpHandler(name, version) {
|
|
1247
|
+
function createMcpHttpHandler(name, version, options = {}) {
|
|
1127
1248
|
let transport;
|
|
1128
1249
|
return async (req, commands, mcpOptions) => {
|
|
1250
|
+
const stateless = options.stateless ?? true;
|
|
1251
|
+
if (stateless && req.method !== 'POST')
|
|
1252
|
+
return new Response(null, { status: 405, headers: { Allow: 'POST' } });
|
|
1129
1253
|
if (!transport) {
|
|
1130
|
-
const { McpServer, WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/server');
|
|
1254
|
+
const { fromJsonSchema, McpServer, WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/server');
|
|
1131
1255
|
const server = new McpServer({ name, version });
|
|
1132
1256
|
for (const tool of Mcp.collectTools(commands, [])) {
|
|
1133
1257
|
const mergedShape = {
|
|
@@ -1138,6 +1262,9 @@ function createMcpHttpHandler(name, version) {
|
|
|
1138
1262
|
server.registerTool(tool.name, {
|
|
1139
1263
|
...(tool.description ? { description: tool.description } : undefined),
|
|
1140
1264
|
...(hasInput ? { inputSchema: z.object(mergedShape) } : undefined),
|
|
1265
|
+
...(tool.outputSchema
|
|
1266
|
+
? { outputSchema: fromJsonSchema(tool.outputSchema) }
|
|
1267
|
+
: undefined),
|
|
1141
1268
|
}, async (...callArgs) => {
|
|
1142
1269
|
const params = hasInput ? callArgs[0] : {};
|
|
1143
1270
|
return Mcp.callTool(tool, params, {
|
|
@@ -1149,10 +1276,13 @@ function createMcpHttpHandler(name, version) {
|
|
|
1149
1276
|
});
|
|
1150
1277
|
});
|
|
1151
1278
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1279
|
+
const transportOptions = stateless
|
|
1280
|
+
? { enableJsonResponse: true }
|
|
1281
|
+
: {
|
|
1282
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
1283
|
+
enableJsonResponse: true,
|
|
1284
|
+
};
|
|
1285
|
+
transport = new WebStandardStreamableHTTPServerTransport(transportOptions);
|
|
1156
1286
|
await server.connect(transport);
|
|
1157
1287
|
}
|
|
1158
1288
|
return transport.handleRequest(req);
|
|
@@ -1185,7 +1315,7 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1185
1315
|
if (req.method === 'GET' && isOpenapiRoute(segments)) {
|
|
1186
1316
|
const spec = generatedOpenapi(name, commands, options);
|
|
1187
1317
|
const yaml = segments[0] === 'openapi.yml' || segments[0] === 'openapi.yaml';
|
|
1188
|
-
return new Response(yaml ?
|
|
1318
|
+
return new Response(yaml ? (await Yaml.load()).stringify(spec) : JSON.stringify(spec), {
|
|
1189
1319
|
status: 200,
|
|
1190
1320
|
headers: {
|
|
1191
1321
|
'content-type': yaml ? 'application/yaml' : 'application/json',
|
|
@@ -1205,6 +1335,8 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1205
1335
|
segments[1] === 'skills' &&
|
|
1206
1336
|
segments.length >= 3 &&
|
|
1207
1337
|
req.method === 'GET') {
|
|
1338
|
+
// Pre-load yaml for the sync call paths below (`Skill.split`, frontmatter parsing).
|
|
1339
|
+
await Yaml.load();
|
|
1208
1340
|
const groups = new Map();
|
|
1209
1341
|
const cmds = collectSkillCommands(commands, [], groups, options.rootCommand);
|
|
1210
1342
|
// GET /.well-known/skills/index.json
|
|
@@ -1212,7 +1344,7 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1212
1344
|
const files = Skill.split(name, cmds, 1, groups);
|
|
1213
1345
|
const skills = files.map((f) => {
|
|
1214
1346
|
const fmMatch = f.content.match(/^---\n([\s\S]*?)\n---/);
|
|
1215
|
-
const meta = fmMatch ?
|
|
1347
|
+
const meta = fmMatch ? Yaml.loadSync().parse(fmMatch[1]) : {};
|
|
1216
1348
|
return {
|
|
1217
1349
|
name: f.dir || name,
|
|
1218
1350
|
description: meta.description ?? '',
|
|
@@ -1252,7 +1384,7 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1252
1384
|
catch { }
|
|
1253
1385
|
}
|
|
1254
1386
|
function jsonResponse(body, status) {
|
|
1255
|
-
return new Response(
|
|
1387
|
+
return new Response(Json.stringify(body), {
|
|
1256
1388
|
status,
|
|
1257
1389
|
headers: { 'content-type': 'application/json' },
|
|
1258
1390
|
});
|
|
@@ -1303,7 +1435,7 @@ async function fetchImpl(name, commands, req, options = {}) {
|
|
|
1303
1435
|
/** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
|
|
1304
1436
|
async function executeCommand(path, command, rest, inputOptions, start, options) {
|
|
1305
1437
|
function jsonResponse(body, status) {
|
|
1306
|
-
return new Response(
|
|
1438
|
+
return new Response(Json.stringify(body), {
|
|
1307
1439
|
status,
|
|
1308
1440
|
headers: { 'content-type': 'application/json' },
|
|
1309
1441
|
});
|
|
@@ -1313,13 +1445,39 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
|
|
|
1313
1445
|
...(options.groupMiddlewares ?? []),
|
|
1314
1446
|
...(command.middleware ?? []),
|
|
1315
1447
|
];
|
|
1448
|
+
let globals = {};
|
|
1449
|
+
let commandInputOptions = inputOptions;
|
|
1450
|
+
if (options.globals) {
|
|
1451
|
+
const globalKeys = new Set(Object.keys(options.globals.schema.shape));
|
|
1452
|
+
const rawGlobals = {};
|
|
1453
|
+
commandInputOptions = {};
|
|
1454
|
+
for (const [key, value] of Object.entries(inputOptions)) {
|
|
1455
|
+
if (globalKeys.has(key))
|
|
1456
|
+
rawGlobals[key] = value;
|
|
1457
|
+
else
|
|
1458
|
+
commandInputOptions[key] = value;
|
|
1459
|
+
}
|
|
1460
|
+
try {
|
|
1461
|
+
globals = options.globals.schema.parse(rawGlobals);
|
|
1462
|
+
}
|
|
1463
|
+
catch (error) {
|
|
1464
|
+
const issues = error?.issues ?? error?.error?.issues ?? [];
|
|
1465
|
+
const message = issues.map((i) => i.message).join('; ') || 'Validation failed';
|
|
1466
|
+
return jsonResponse({
|
|
1467
|
+
ok: false,
|
|
1468
|
+
error: { code: 'VALIDATION_ERROR', message },
|
|
1469
|
+
meta: { command: path, duration: `${Math.round(performance.now() - start)}ms` },
|
|
1470
|
+
}, 400);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1316
1473
|
const result = await Command.execute(command, {
|
|
1317
1474
|
agent: true,
|
|
1318
1475
|
argv: rest,
|
|
1319
1476
|
env: options.envSchema,
|
|
1320
1477
|
format: 'json',
|
|
1321
1478
|
formatExplicit: true,
|
|
1322
|
-
|
|
1479
|
+
globals,
|
|
1480
|
+
inputOptions: commandInputOptions,
|
|
1323
1481
|
middlewares: allMiddleware,
|
|
1324
1482
|
name: options.name ?? path,
|
|
1325
1483
|
parseMode: 'split',
|
|
@@ -1330,30 +1488,68 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
|
|
|
1330
1488
|
const duration = `${Math.round(performance.now() - start)}ms`;
|
|
1331
1489
|
// Streaming path — async generator → NDJSON response
|
|
1332
1490
|
if ('stream' in result) {
|
|
1491
|
+
const iterator = result.stream;
|
|
1492
|
+
const encoder = new TextEncoder();
|
|
1493
|
+
const meta = (cta) => ({
|
|
1494
|
+
command: path,
|
|
1495
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
1496
|
+
...(cta ? { cta } : undefined),
|
|
1497
|
+
});
|
|
1498
|
+
const errorRecord = (err) => ({
|
|
1499
|
+
type: 'error',
|
|
1500
|
+
ok: false,
|
|
1501
|
+
error: {
|
|
1502
|
+
code: err.code,
|
|
1503
|
+
message: err.message,
|
|
1504
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
1505
|
+
},
|
|
1506
|
+
meta: meta(formatCtaBlock(options.name ?? path, err.cta)),
|
|
1507
|
+
});
|
|
1333
1508
|
const stream = new ReadableStream({
|
|
1334
|
-
async
|
|
1335
|
-
|
|
1509
|
+
async cancel() {
|
|
1510
|
+
await iterator.return(undefined);
|
|
1511
|
+
},
|
|
1512
|
+
async pull(controller) {
|
|
1336
1513
|
try {
|
|
1337
|
-
|
|
1338
|
-
|
|
1514
|
+
const { value, done } = await iterator.next();
|
|
1515
|
+
if (done) {
|
|
1516
|
+
if (isSentinel(value) && value[sentinel] === 'error') {
|
|
1517
|
+
controller.enqueue(encoder.encode(Json.stringify(errorRecord(value)) + '\n'));
|
|
1518
|
+
controller.close();
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const cta = isSentinel(value) && value[sentinel] === 'ok'
|
|
1522
|
+
? formatCtaBlock(options.name ?? path, value.cta)
|
|
1523
|
+
: undefined;
|
|
1524
|
+
controller.enqueue(encoder.encode(Json.stringify({
|
|
1525
|
+
type: 'done',
|
|
1526
|
+
ok: true,
|
|
1527
|
+
meta: meta(cta),
|
|
1528
|
+
}) + '\n'));
|
|
1529
|
+
controller.close();
|
|
1530
|
+
return;
|
|
1339
1531
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1532
|
+
if (isSentinel(value) && value[sentinel] === 'error') {
|
|
1533
|
+
controller.enqueue(encoder.encode(Json.stringify(errorRecord(value)) + '\n'));
|
|
1534
|
+
await iterator.return(undefined);
|
|
1535
|
+
controller.close();
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
controller.enqueue(encoder.encode(Json.stringify({ type: 'chunk', data: value }) + '\n'));
|
|
1345
1539
|
}
|
|
1346
1540
|
catch (error) {
|
|
1347
|
-
controller.enqueue(encoder.encode(
|
|
1541
|
+
controller.enqueue(encoder.encode(Json.stringify({
|
|
1348
1542
|
type: 'error',
|
|
1349
1543
|
ok: false,
|
|
1350
1544
|
error: {
|
|
1351
|
-
code: 'UNKNOWN',
|
|
1545
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
1352
1546
|
message: error instanceof Error ? error.message : String(error),
|
|
1547
|
+
...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
|
|
1353
1548
|
},
|
|
1549
|
+
meta: meta(),
|
|
1354
1550
|
}) + '\n'));
|
|
1551
|
+
controller.close();
|
|
1355
1552
|
}
|
|
1356
|
-
controller.close();
|
|
1357
1553
|
},
|
|
1358
1554
|
});
|
|
1359
1555
|
return new Response(stream, {
|
|
@@ -1371,6 +1567,7 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
|
|
|
1371
1567
|
...(result.error.retryable !== undefined
|
|
1372
1568
|
? { retryable: result.error.retryable }
|
|
1373
1569
|
: undefined),
|
|
1570
|
+
...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
|
|
1374
1571
|
},
|
|
1375
1572
|
meta: {
|
|
1376
1573
|
command: path,
|
|
@@ -1819,6 +2016,31 @@ function resolveAlias(commands, entry) {
|
|
|
1819
2016
|
return commands.get(entry.target);
|
|
1820
2017
|
return entry;
|
|
1821
2018
|
}
|
|
2019
|
+
/** @internal Validates command options against CLI-level global options. */
|
|
2020
|
+
function assertNoGlobalOptionConflicts(path, entry, globals) {
|
|
2021
|
+
if (!globals || isFetchGateway(entry) || isAlias(entry))
|
|
2022
|
+
return;
|
|
2023
|
+
if (isGroup(entry)) {
|
|
2024
|
+
for (const [name, child] of entry.commands)
|
|
2025
|
+
assertNoGlobalOptionConflicts(`${path} ${name}`, child, globals);
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
if (entry.options) {
|
|
2029
|
+
const globalKeys = Object.keys(globals.schema.shape);
|
|
2030
|
+
const optionKeys = Object.keys(entry.options.shape);
|
|
2031
|
+
for (const key of optionKeys) {
|
|
2032
|
+
if (globalKeys.includes(key))
|
|
2033
|
+
throw new Error(`Command '${path}' option '${key}' conflicts with a global option. Choose a different name.`);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
if (globals.alias && entry.alias) {
|
|
2037
|
+
const globalAliasValues = new Set(Object.values(globals.alias));
|
|
2038
|
+
for (const [name, short] of Object.entries(entry.alias)) {
|
|
2039
|
+
if (short && globalAliasValues.has(short))
|
|
2040
|
+
throw new Error(`Command '${path}' alias '-${short}' for '${name}' conflicts with a global alias. Choose a different alias.`);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
1822
2044
|
/** @internal Maps CLI instances to their command maps. */
|
|
1823
2045
|
export const toCommands = new WeakMap();
|
|
1824
2046
|
/** @internal Maps CLI instances to their middleware arrays. */
|
|
@@ -1831,6 +2053,8 @@ export const toRootOptions = new WeakMap();
|
|
|
1831
2053
|
export const toConfigEnabled = new WeakMap();
|
|
1832
2054
|
/** @internal Maps CLI instances to their output policy. */
|
|
1833
2055
|
const toOutputPolicy = new WeakMap();
|
|
2056
|
+
/** @internal Maps CLI instances to their globals schema and alias map. */
|
|
2057
|
+
const toGlobals = new WeakMap();
|
|
1834
2058
|
/** @internal Maps root CLI instances to their command aliases. */
|
|
1835
2059
|
const toRootAliases = new WeakMap();
|
|
1836
2060
|
/** @internal Sentinel symbol for `ok()` and `error()` return values. */
|
|
@@ -1901,7 +2125,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
1901
2125
|
}
|
|
1902
2126
|
}
|
|
1903
2127
|
if (useJsonl)
|
|
1904
|
-
ctx.writeln(
|
|
2128
|
+
ctx.writeln(Json.stringify({ type: 'chunk', data: value }));
|
|
1905
2129
|
else if (ctx.renderOutput)
|
|
1906
2130
|
ctx.writeln(ctx.truncate(Formatter.format(value, ctx.format)).text);
|
|
1907
2131
|
}
|
|
@@ -1947,6 +2171,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
1947
2171
|
error: {
|
|
1948
2172
|
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
1949
2173
|
message: error instanceof Error ? error.message : String(error),
|
|
2174
|
+
...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
|
|
1950
2175
|
},
|
|
1951
2176
|
}));
|
|
1952
2177
|
else
|
|
@@ -2025,6 +2250,7 @@ async function handleStreaming(generator, ctx) {
|
|
|
2025
2250
|
error: {
|
|
2026
2251
|
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
2027
2252
|
message: error instanceof Error ? error.message : String(error),
|
|
2253
|
+
...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
|
|
2028
2254
|
},
|
|
2029
2255
|
meta: {
|
|
2030
2256
|
command: ctx.path,
|
|
@@ -2035,35 +2261,12 @@ async function handleStreaming(generator, ctx) {
|
|
|
2035
2261
|
}
|
|
2036
2262
|
}
|
|
2037
2263
|
}
|
|
2038
|
-
/** @internal Formats a CTA block into the output envelope shape. */
|
|
2039
|
-
function formatCtaBlock(name, block) {
|
|
2040
|
-
if (!block || block.commands.length === 0)
|
|
2041
|
-
return undefined;
|
|
2042
|
-
return {
|
|
2043
|
-
description: block.description ??
|
|
2044
|
-
(block.commands.length === 1 ? 'Suggested command:' : 'Suggested commands:'),
|
|
2045
|
-
commands: block.commands.map((c) => formatCta(name, c)),
|
|
2046
|
-
};
|
|
2047
|
-
}
|
|
2048
|
-
/** @internal Formats a CTA by prefixing the CLI name. Handles string and object forms. */
|
|
2049
|
-
function formatCta(name, cta) {
|
|
2050
|
-
if (typeof cta === 'string')
|
|
2051
|
-
return { command: `${name} ${cta}` };
|
|
2052
|
-
const prefix = cta.command === name || cta.command.startsWith(`${name} `) ? '' : `${name} `;
|
|
2053
|
-
let cmd = `${prefix}${cta.command}`;
|
|
2054
|
-
if (cta.args)
|
|
2055
|
-
for (const [key, value] of Object.entries(cta.args))
|
|
2056
|
-
cmd += value === true ? ` <${key}>` : ` ${value}`;
|
|
2057
|
-
if (cta.options)
|
|
2058
|
-
for (const [key, value] of Object.entries(cta.options))
|
|
2059
|
-
cmd += value === true ? ` --${key} <${key}>` : ` --${key} ${value}`;
|
|
2060
|
-
return { command: cmd, ...(cta.description ? { description: cta.description } : undefined) };
|
|
2061
|
-
}
|
|
2062
2264
|
/** @internal Builds the `--llms` index manifest (name + description only) from the command tree. */
|
|
2063
|
-
function buildIndexManifest(commands, prefix = []) {
|
|
2265
|
+
function buildIndexManifest(commands, prefix = [], globalsSchema) {
|
|
2064
2266
|
return {
|
|
2065
2267
|
version: 'incur.v1',
|
|
2066
2268
|
commands: collectIndexCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
|
|
2269
|
+
...(globalsSchema ? { globals: Schema.toJsonSchema(globalsSchema) } : undefined),
|
|
2067
2270
|
};
|
|
2068
2271
|
}
|
|
2069
2272
|
/** @internal Recursively collects leaf commands with name + description only. */
|
|
@@ -2090,10 +2293,11 @@ function collectIndexCommands(commands, prefix) {
|
|
|
2090
2293
|
return result;
|
|
2091
2294
|
}
|
|
2092
2295
|
/** @internal Builds the `--llms` manifest from the command tree. */
|
|
2093
|
-
function buildManifest(commands, prefix = []) {
|
|
2296
|
+
function buildManifest(commands, prefix = [], globalsSchema) {
|
|
2094
2297
|
return {
|
|
2095
2298
|
version: 'incur.v1',
|
|
2096
2299
|
commands: collectCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
|
|
2300
|
+
...(globalsSchema ? { globals: Schema.toJsonSchema(globalsSchema) } : undefined),
|
|
2097
2301
|
};
|
|
2098
2302
|
}
|
|
2099
2303
|
/** @internal Recursively collects leaf commands with their full paths. */
|
|
@@ -2143,7 +2347,7 @@ function collectCommands(commands, prefix) {
|
|
|
2143
2347
|
return result;
|
|
2144
2348
|
}
|
|
2145
2349
|
/** @internal Recursively collects leaf commands as `Skill.CommandInfo` for `--llms --format md`. */
|
|
2146
|
-
function collectSkillCommands(commands, prefix, groups, rootCommand) {
|
|
2350
|
+
export function collectSkillCommands(commands, prefix, groups, rootCommand) {
|
|
2147
2351
|
const result = [];
|
|
2148
2352
|
if (rootCommand) {
|
|
2149
2353
|
const cmd = {};
|
|
@@ -2155,6 +2359,8 @@ function collectSkillCommands(commands, prefix, groups, rootCommand) {
|
|
|
2155
2359
|
cmd.env = rootCommand.env;
|
|
2156
2360
|
if (rootCommand.hint)
|
|
2157
2361
|
cmd.hint = rootCommand.hint;
|
|
2362
|
+
if (isDestructive(rootCommand))
|
|
2363
|
+
cmd.hint = appendDestructiveHint(cmd.hint);
|
|
2158
2364
|
if (rootCommand.options)
|
|
2159
2365
|
cmd.options = rootCommand.options;
|
|
2160
2366
|
if (rootCommand.output)
|
|
@@ -2190,6 +2396,8 @@ function collectSkillCommands(commands, prefix, groups, rootCommand) {
|
|
|
2190
2396
|
cmd.env = entry.env;
|
|
2191
2397
|
if (entry.hint)
|
|
2192
2398
|
cmd.hint = entry.hint;
|
|
2399
|
+
if (isDestructive(entry))
|
|
2400
|
+
cmd.hint = appendDestructiveHint(cmd.hint);
|
|
2193
2401
|
if (entry.options)
|
|
2194
2402
|
cmd.options = entry.options;
|
|
2195
2403
|
if (entry.output)
|
|
@@ -2207,6 +2415,16 @@ function collectSkillCommands(commands, prefix, groups, rootCommand) {
|
|
|
2207
2415
|
}
|
|
2208
2416
|
return result.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
|
2209
2417
|
}
|
|
2418
|
+
function isDestructive(command) {
|
|
2419
|
+
return command.destructive === true || command.mcp?.annotations?.destructiveHint === true;
|
|
2420
|
+
}
|
|
2421
|
+
function appendDestructiveHint(hint) {
|
|
2422
|
+
if (!hint)
|
|
2423
|
+
return destructiveCommandHint;
|
|
2424
|
+
if (hint.includes(destructiveCommandHint))
|
|
2425
|
+
return hint;
|
|
2426
|
+
return `${hint} ${destructiveCommandHint}`;
|
|
2427
|
+
}
|
|
2210
2428
|
/** @internal Formats examples into `{ command, description }` objects. `command` is the args/options suffix only. */
|
|
2211
2429
|
export function formatExamples(examples) {
|
|
2212
2430
|
if (!examples || examples.length === 0)
|
|
@@ -2225,6 +2443,16 @@ export function formatExamples(examples) {
|
|
|
2225
2443
|
return result;
|
|
2226
2444
|
});
|
|
2227
2445
|
}
|
|
2446
|
+
/** @internal Parses YAML frontmatter from generated skill Markdown. */
|
|
2447
|
+
export function parseSkillFrontmatter(content) {
|
|
2448
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
2449
|
+
if (!match)
|
|
2450
|
+
return {};
|
|
2451
|
+
const meta = Yaml.loadSync().parse(match[1]);
|
|
2452
|
+
if (!meta || typeof meta !== 'object')
|
|
2453
|
+
return {};
|
|
2454
|
+
return meta;
|
|
2455
|
+
}
|
|
2228
2456
|
/** @internal Builds separate args, env, and options JSON Schemas. */
|
|
2229
2457
|
function buildInputSchema(args, env, options) {
|
|
2230
2458
|
if (!args && !env && !options)
|