incur 0.0.0 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/SKILL.md +664 -0
- package/dist/Cli.d.ts +255 -0
- package/dist/Cli.d.ts.map +1 -0
- package/dist/Cli.js +900 -0
- package/dist/Cli.js.map +1 -0
- package/dist/Errors.d.ts +92 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Errors.js +75 -0
- package/dist/Errors.js.map +1 -0
- package/dist/Formatter.d.ts +5 -0
- package/dist/Formatter.d.ts.map +1 -0
- package/dist/Formatter.js +91 -0
- package/dist/Formatter.js.map +1 -0
- package/dist/Help.d.ts +53 -0
- package/dist/Help.d.ts.map +1 -0
- package/dist/Help.js +231 -0
- package/dist/Help.js.map +1 -0
- package/dist/Mcp.d.ts +13 -0
- package/dist/Mcp.d.ts.map +1 -0
- package/dist/Mcp.js +140 -0
- package/dist/Mcp.js.map +1 -0
- package/dist/Parser.d.ts +24 -0
- package/dist/Parser.d.ts.map +1 -0
- package/dist/Parser.js +215 -0
- package/dist/Parser.js.map +1 -0
- package/dist/Register.d.ts +19 -0
- package/dist/Register.d.ts.map +1 -0
- package/dist/Register.js +2 -0
- package/dist/Register.js.map +1 -0
- package/dist/Schema.d.ts +4 -0
- package/dist/Schema.d.ts.map +1 -0
- package/dist/Schema.js +8 -0
- package/dist/Schema.js.map +1 -0
- package/dist/Skill.d.ts +29 -0
- package/dist/Skill.d.ts.map +1 -0
- package/dist/Skill.js +196 -0
- package/dist/Skill.js.map +1 -0
- package/dist/Skillgen.d.ts +3 -0
- package/dist/Skillgen.d.ts.map +1 -0
- package/dist/Skillgen.js +67 -0
- package/dist/Skillgen.js.map +1 -0
- package/dist/SyncMcp.d.ts +23 -0
- package/dist/SyncMcp.d.ts.map +1 -0
- package/dist/SyncMcp.js +100 -0
- package/dist/SyncMcp.js.map +1 -0
- package/dist/SyncSkills.d.ts +38 -0
- package/dist/SyncSkills.d.ts.map +1 -0
- package/dist/SyncSkills.js +163 -0
- package/dist/SyncSkills.js.map +1 -0
- package/dist/Typegen.d.ts +6 -0
- package/dist/Typegen.d.ts.map +1 -0
- package/dist/Typegen.js +92 -0
- package/dist/Typegen.js.map +1 -0
- package/dist/bin.d.ts +14 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +30 -0
- package/dist/bin.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/pm.d.ts +3 -0
- package/dist/internal/pm.d.ts.map +1 -0
- package/dist/internal/pm.js +11 -0
- package/dist/internal/pm.js.map +1 -0
- package/dist/internal/types.d.ts +11 -0
- package/dist/internal/types.d.ts.map +1 -0
- package/dist/internal/types.js +2 -0
- package/dist/internal/types.js.map +1 -0
- package/dist/internal/utils.d.ts +8 -0
- package/dist/internal/utils.d.ts.map +1 -0
- package/dist/internal/utils.js +51 -0
- package/dist/internal/utils.js.map +1 -0
- package/examples/npm/cli.ts +180 -0
- package/examples/npm/node_modules/.bin/incur.src +21 -0
- package/examples/npm/node_modules/.bin/tsx +21 -0
- package/examples/npm/package.json +14 -0
- package/examples/npm/tsconfig.json +9 -0
- package/examples/presto/cli.ts +246 -0
- package/examples/presto/node_modules/.bin/incur.src +21 -0
- package/examples/presto/node_modules/.bin/tsx +21 -0
- package/examples/presto/package.json +14 -0
- package/examples/presto/tsconfig.json +9 -0
- package/package.json +53 -2
- package/src/Cli.test-d.ts +135 -0
- package/src/Cli.test.ts +1373 -0
- package/src/Cli.ts +1470 -0
- package/src/Errors.test.ts +96 -0
- package/src/Errors.ts +139 -0
- package/src/Formatter.test.ts +245 -0
- package/src/Formatter.ts +106 -0
- package/src/Help.test.ts +124 -0
- package/src/Help.ts +302 -0
- package/src/Mcp.test.ts +254 -0
- package/src/Mcp.ts +195 -0
- package/src/Parser.test-d.ts +45 -0
- package/src/Parser.test.ts +118 -0
- package/src/Parser.ts +247 -0
- package/src/Register.ts +18 -0
- package/src/Schema.test.ts +125 -0
- package/src/Schema.ts +8 -0
- package/src/Skill.test.ts +293 -0
- package/src/Skill.ts +253 -0
- package/src/Skillgen.ts +66 -0
- package/src/SyncMcp.test.ts +75 -0
- package/src/SyncMcp.ts +132 -0
- package/src/SyncSkills.test.ts +92 -0
- package/src/SyncSkills.ts +205 -0
- package/src/Typegen.test.ts +150 -0
- package/src/Typegen.ts +107 -0
- package/src/bin.ts +33 -0
- package/src/e2e.test.ts +1710 -0
- package/src/index.ts +14 -0
- package/src/internal/pm.test.ts +38 -0
- package/src/internal/pm.ts +8 -0
- package/src/internal/types.ts +22 -0
- package/src/internal/utils.ts +50 -0
- package/src/tsconfig.json +8 -0
package/dist/Cli.js
ADDED
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
import { IncurError, ValidationError } from './Errors.js';
|
|
2
|
+
import * as Formatter from './Formatter.js';
|
|
3
|
+
import * as Help from './Help.js';
|
|
4
|
+
import { detectRunner } from './internal/pm.js';
|
|
5
|
+
import * as Mcp from './Mcp.js';
|
|
6
|
+
import * as Parser from './Parser.js';
|
|
7
|
+
import * as Schema from './Schema.js';
|
|
8
|
+
import * as Skill from './Skill.js';
|
|
9
|
+
import * as SyncMcp from './SyncMcp.js';
|
|
10
|
+
import * as SyncSkills from './SyncSkills.js';
|
|
11
|
+
export function create(nameOrDefinition, definition) {
|
|
12
|
+
const name = typeof nameOrDefinition === 'string' ? nameOrDefinition : nameOrDefinition.name;
|
|
13
|
+
const def = typeof nameOrDefinition === 'string' ? (definition ?? {}) : nameOrDefinition;
|
|
14
|
+
if ('run' in def) {
|
|
15
|
+
const rootDef = def;
|
|
16
|
+
const leafCommands = new Map();
|
|
17
|
+
leafCommands.set(name, rootDef);
|
|
18
|
+
const leaf = {
|
|
19
|
+
name,
|
|
20
|
+
description: def.description,
|
|
21
|
+
async serve(argv = process.argv.slice(2), options = {}) {
|
|
22
|
+
return serveImpl(name, leafCommands, [name, ...argv], {
|
|
23
|
+
...options,
|
|
24
|
+
description: def.description,
|
|
25
|
+
format: def.format,
|
|
26
|
+
mcp: def.mcp,
|
|
27
|
+
sync: def.sync,
|
|
28
|
+
version: def.version,
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
toRootDefinition.set(leaf, rootDef);
|
|
33
|
+
toCommands.set(leaf, leafCommands);
|
|
34
|
+
return leaf;
|
|
35
|
+
}
|
|
36
|
+
const commands = new Map();
|
|
37
|
+
const cli = {
|
|
38
|
+
name,
|
|
39
|
+
description: def.description,
|
|
40
|
+
command(nameOrCli, def) {
|
|
41
|
+
if (typeof nameOrCli === 'string') {
|
|
42
|
+
commands.set(nameOrCli, def);
|
|
43
|
+
return cli;
|
|
44
|
+
}
|
|
45
|
+
const rootDef = toRootDefinition.get(nameOrCli);
|
|
46
|
+
if (rootDef) {
|
|
47
|
+
commands.set(nameOrCli.name, rootDef);
|
|
48
|
+
return cli;
|
|
49
|
+
}
|
|
50
|
+
const sub = nameOrCli;
|
|
51
|
+
const subCommands = toCommands.get(sub);
|
|
52
|
+
commands.set(sub.name, { _group: true, description: sub.description, commands: subCommands });
|
|
53
|
+
return cli;
|
|
54
|
+
},
|
|
55
|
+
async serve(argv = process.argv.slice(2), serveOptions = {}) {
|
|
56
|
+
return serveImpl(name, commands, argv, {
|
|
57
|
+
...serveOptions,
|
|
58
|
+
description: def.description,
|
|
59
|
+
format: def.format,
|
|
60
|
+
mcp: def.mcp,
|
|
61
|
+
sync: def.sync,
|
|
62
|
+
version: def.version,
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
toCommands.set(cli, commands);
|
|
67
|
+
return cli;
|
|
68
|
+
}
|
|
69
|
+
/** @internal Shared serve implementation for both router and leaf CLIs. */
|
|
70
|
+
// biome-ignore lint/correctness/noUnusedVariables: _
|
|
71
|
+
async function serveImpl(name, commands, argv, options = {}) {
|
|
72
|
+
const stdout = options.stdout ?? ((s) => process.stdout.write(s));
|
|
73
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
74
|
+
const { verbose, format: formatFlag, formatExplicit, llms, mcp: mcpFlag, help, version, rest: filtered, } = extractBuiltinFlags(argv);
|
|
75
|
+
// --mcp: start as MCP stdio server
|
|
76
|
+
if (mcpFlag) {
|
|
77
|
+
await Mcp.serve(name, options.version ?? '0.0.0', commands);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Human mode: default unless --verbose or explicit --format/--json override
|
|
81
|
+
const human = !formatExplicit && !verbose;
|
|
82
|
+
function writeln(s) {
|
|
83
|
+
stdout(s.endsWith('\n') ? s : `${s}\n`);
|
|
84
|
+
}
|
|
85
|
+
// Skills staleness check (skip for built-in commands)
|
|
86
|
+
if (!llms && !help && !version) {
|
|
87
|
+
const isSkillsAdd = filtered[0] === 'skills' || (filtered[0] === name && filtered[1] === 'skills');
|
|
88
|
+
const isMcpAdd = filtered[0] === 'mcp' || (filtered[0] === name && filtered[1] === 'mcp');
|
|
89
|
+
if (!isSkillsAdd && !isMcpAdd) {
|
|
90
|
+
const stored = SyncSkills.readHash(name);
|
|
91
|
+
if (stored) {
|
|
92
|
+
const groups = new Map();
|
|
93
|
+
const entries = collectSkillCommands(commands, [], groups);
|
|
94
|
+
if (Skill.hash(entries) !== stored) {
|
|
95
|
+
const runner = detectRunner();
|
|
96
|
+
const spec = SyncMcp.detectPackageSpecifier(name);
|
|
97
|
+
process.stderr.write(`⚠ Skills are out of date. Run '${runner} ${spec} skills add' to update.\n\n`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (llms) {
|
|
103
|
+
// Scope to a subtree if command tokens are provided
|
|
104
|
+
let scopedCommands = commands;
|
|
105
|
+
const prefix = [];
|
|
106
|
+
for (const token of filtered) {
|
|
107
|
+
const entry = scopedCommands.get(token);
|
|
108
|
+
if (!entry)
|
|
109
|
+
break;
|
|
110
|
+
if (isGroup(entry)) {
|
|
111
|
+
scopedCommands = entry.commands;
|
|
112
|
+
prefix.push(token);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Leaf command — scope to just this command
|
|
116
|
+
scopedCommands = new Map([[token, entry]]);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (!formatExplicit || formatFlag === 'md') {
|
|
121
|
+
const groups = new Map();
|
|
122
|
+
const cmds = collectSkillCommands(scopedCommands, prefix, groups);
|
|
123
|
+
const scopedName = prefix.length > 0 ? `${name} ${prefix.join(' ')}` : name;
|
|
124
|
+
writeln(Skill.generate(scopedName, cmds, groups));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
writeln(Formatter.format(buildManifest(scopedCommands, prefix), formatFlag));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
|
|
131
|
+
const skillsIdx = filtered[0] === 'skills' ? 0 : filtered[0] === name && filtered[1] === 'skills' ? 1 : -1;
|
|
132
|
+
if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills' && filtered[skillsIdx + 1] === 'add') {
|
|
133
|
+
if (help) {
|
|
134
|
+
writeln([
|
|
135
|
+
`${name} skills add — Sync skill files to your agent`,
|
|
136
|
+
'',
|
|
137
|
+
`Usage: ${name} skills add [options]`,
|
|
138
|
+
'',
|
|
139
|
+
'Options:',
|
|
140
|
+
' --depth <number> Grouping depth for skill files (default: 1)',
|
|
141
|
+
' --no-global Install to project instead of globally',
|
|
142
|
+
].join('\n'));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const rest = filtered.slice(skillsIdx + 2);
|
|
146
|
+
const depthArg = rest.indexOf('--depth');
|
|
147
|
+
const depth = depthArg !== -1 ? Number(rest[depthArg + 1]) : (options.sync?.depth ?? 1);
|
|
148
|
+
const global = rest.includes('--no-global') ? false : undefined;
|
|
149
|
+
try {
|
|
150
|
+
if (human)
|
|
151
|
+
stdout('Syncing...');
|
|
152
|
+
const result = await SyncSkills.sync(name, commands, {
|
|
153
|
+
cwd: options.sync?.cwd,
|
|
154
|
+
depth,
|
|
155
|
+
description: options.description,
|
|
156
|
+
global,
|
|
157
|
+
include: options.sync?.include,
|
|
158
|
+
});
|
|
159
|
+
if (human) {
|
|
160
|
+
stdout('\r\x1b[K');
|
|
161
|
+
const lines = [];
|
|
162
|
+
const skillLabel = (s) => s.external || s.name === name ? s.name : `${name}-${s.name}`;
|
|
163
|
+
const maxLen = Math.max(...result.skills.map((s) => skillLabel(s).length));
|
|
164
|
+
for (const s of result.skills) {
|
|
165
|
+
const label = skillLabel(s);
|
|
166
|
+
const padding = s.description
|
|
167
|
+
? `${' '.repeat(maxLen - label.length)} ${s.description}`
|
|
168
|
+
: '';
|
|
169
|
+
lines.push(` ✓ ${label}${padding}`);
|
|
170
|
+
}
|
|
171
|
+
lines.push('');
|
|
172
|
+
lines.push(`${result.skills.length} skill${result.skills.length === 1 ? '' : 's'} synced`);
|
|
173
|
+
const suggestions = options.sync?.suggestions;
|
|
174
|
+
if (suggestions && suggestions.length > 0) {
|
|
175
|
+
lines.push('');
|
|
176
|
+
lines.push(`Your agent can now use ${name}. Try asking:`);
|
|
177
|
+
for (const s of suggestions)
|
|
178
|
+
lines.push(` "${s}"`);
|
|
179
|
+
}
|
|
180
|
+
lines.push('');
|
|
181
|
+
lines.push(`Run \`${name} --help\` to see the full command reference.`);
|
|
182
|
+
writeln(lines.join('\n'));
|
|
183
|
+
}
|
|
184
|
+
else
|
|
185
|
+
writeln(Formatter.format({ skills: result.paths }, formatExplicit ? formatFlag : 'toon'));
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
writeln(Formatter.format({ code: 'SYNC_SKILLS_FAILED', message: err instanceof Error ? err.message : String(err) }, formatExplicit ? formatFlag : 'toon'));
|
|
189
|
+
exit(1);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
// mcp add: register CLI as MCP server via `npx add-mcp`
|
|
194
|
+
const mcpIdx = filtered[0] === 'mcp' ? 0 : filtered[0] === name && filtered[1] === 'mcp' ? 1 : -1;
|
|
195
|
+
if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp' && filtered[mcpIdx + 1] === 'add') {
|
|
196
|
+
if (help) {
|
|
197
|
+
writeln([
|
|
198
|
+
`${name} mcp add — Register as an MCP server for your agent`,
|
|
199
|
+
'',
|
|
200
|
+
`Usage: ${name} mcp add [options]`,
|
|
201
|
+
'',
|
|
202
|
+
'Options:',
|
|
203
|
+
' -c, --command <cmd> Override the command agents will run (e.g. "pnpm my-cli --mcp")',
|
|
204
|
+
' --no-global Install to project instead of globally',
|
|
205
|
+
' --agent <agent> Target a specific agent (e.g. claude-code, cursor)',
|
|
206
|
+
].join('\n'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const rest = filtered.slice(mcpIdx + 2);
|
|
210
|
+
const global = rest.includes('--no-global') ? false : true;
|
|
211
|
+
// Parse --command / -c and --agent flags from argv
|
|
212
|
+
let command = options.mcp?.command;
|
|
213
|
+
const agents = [...(options.mcp?.agents ?? [])];
|
|
214
|
+
for (let i = 0; i < rest.length; i++) {
|
|
215
|
+
if ((rest[i] === '--command' || rest[i] === '-c') && rest[i + 1])
|
|
216
|
+
command = rest[++i];
|
|
217
|
+
else if (rest[i] === '--agent' && rest[i + 1])
|
|
218
|
+
agents.push(rest[++i]);
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
if (human)
|
|
222
|
+
stdout('Registering MCP server...');
|
|
223
|
+
const result = await SyncMcp.register(name, {
|
|
224
|
+
command,
|
|
225
|
+
global,
|
|
226
|
+
agents,
|
|
227
|
+
});
|
|
228
|
+
if (human) {
|
|
229
|
+
stdout('\r\x1b[K');
|
|
230
|
+
const lines = [];
|
|
231
|
+
lines.push(`✓ Registered ${name} as MCP server`);
|
|
232
|
+
if (result.agents.length > 0)
|
|
233
|
+
lines.push(` Agents: ${result.agents.join(', ')}`);
|
|
234
|
+
lines.push('');
|
|
235
|
+
lines.push(`Agents can now use ${name} tools.`);
|
|
236
|
+
const suggestions = options.sync?.suggestions;
|
|
237
|
+
if (suggestions && suggestions.length > 0) {
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push('Try asking:');
|
|
240
|
+
for (const s of suggestions)
|
|
241
|
+
lines.push(` "${s}"`);
|
|
242
|
+
}
|
|
243
|
+
writeln(lines.join('\n'));
|
|
244
|
+
}
|
|
245
|
+
else
|
|
246
|
+
writeln(Formatter.format({ name, command: result.command, agents: result.agents }, formatExplicit ? formatFlag : 'toon'));
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
writeln(Formatter.format({ code: 'MCP_ADD_FAILED', message: err instanceof Error ? err.message : String(err) }, formatExplicit ? formatFlag : 'toon'));
|
|
250
|
+
exit(1);
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
// --help takes precedence over --version
|
|
255
|
+
if (version && !help && options.version) {
|
|
256
|
+
writeln(options.version);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (filtered.length === 0) {
|
|
260
|
+
writeln(Help.formatRoot(name, {
|
|
261
|
+
description: options.description,
|
|
262
|
+
version: options.version,
|
|
263
|
+
commands: collectHelpCommands(commands),
|
|
264
|
+
root: true,
|
|
265
|
+
}));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const resolved = resolveCommand(commands, filtered);
|
|
269
|
+
// --help after a command → show help for that command
|
|
270
|
+
if (help) {
|
|
271
|
+
if ('help' in resolved || 'error' in resolved) {
|
|
272
|
+
// group or unknown → show root help for that path
|
|
273
|
+
const helpName = 'help' in resolved ? `${name} ${resolved.path}` : name;
|
|
274
|
+
const helpDesc = 'help' in resolved ? resolved.description : options.description;
|
|
275
|
+
const helpCmds = 'help' in resolved ? resolved.commands : commands;
|
|
276
|
+
const isRoot = helpName === name;
|
|
277
|
+
writeln(Help.formatRoot(helpName, {
|
|
278
|
+
description: helpDesc,
|
|
279
|
+
version: isRoot ? options.version : undefined,
|
|
280
|
+
commands: collectHelpCommands(helpCmds),
|
|
281
|
+
root: isRoot,
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const isRootCmd = resolved.path === name;
|
|
286
|
+
const commandName = isRootCmd ? name : `${name} ${resolved.path}`;
|
|
287
|
+
writeln(Help.formatCommand(commandName, {
|
|
288
|
+
alias: resolved.command.alias,
|
|
289
|
+
description: resolved.command.description,
|
|
290
|
+
version: isRootCmd ? options.version : undefined,
|
|
291
|
+
args: resolved.command.args,
|
|
292
|
+
env: resolved.command.env,
|
|
293
|
+
hint: resolved.command.hint,
|
|
294
|
+
options: resolved.command.options,
|
|
295
|
+
examples: formatExamples(resolved.command.examples),
|
|
296
|
+
usage: resolved.command.usage,
|
|
297
|
+
root: isRootCmd,
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if ('help' in resolved) {
|
|
303
|
+
writeln(Help.formatRoot(`${name} ${resolved.path}`, {
|
|
304
|
+
description: resolved.description,
|
|
305
|
+
commands: collectHelpCommands(resolved.commands),
|
|
306
|
+
}));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const start = performance.now();
|
|
310
|
+
// Resolve effective format: explicit --format/--json → command default → CLI default → toon
|
|
311
|
+
const resolvedFormat = 'command' in resolved && resolved.command.format;
|
|
312
|
+
const format = formatExplicit ? formatFlag : resolvedFormat || options.format || 'toon';
|
|
313
|
+
function write(output) {
|
|
314
|
+
const cta = output.meta.cta;
|
|
315
|
+
if (human) {
|
|
316
|
+
if (output.ok)
|
|
317
|
+
writeln(Formatter.format(output.data, format));
|
|
318
|
+
else
|
|
319
|
+
writeln(formatHumanError(output.error));
|
|
320
|
+
if (cta)
|
|
321
|
+
writeln(formatHumanCta(cta));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (verbose)
|
|
325
|
+
return writeln(Formatter.format(output, format));
|
|
326
|
+
const base = output.ok ? output.data : output.error;
|
|
327
|
+
if (!cta)
|
|
328
|
+
return writeln(Formatter.format(base, format));
|
|
329
|
+
const payload = typeof base === 'object' && base !== null ? { ...base, cta } : { data: base, cta };
|
|
330
|
+
writeln(Formatter.format(payload, format));
|
|
331
|
+
}
|
|
332
|
+
if ('error' in resolved) {
|
|
333
|
+
const helpCmd = resolved.path ? `${name} ${resolved.path} --help` : `${name} --help`;
|
|
334
|
+
const message = `'${resolved.error}' is not a command. See '${helpCmd}' for a list of available commands.`;
|
|
335
|
+
if (human) {
|
|
336
|
+
writeln(formatHumanError({ code: 'COMMAND_NOT_FOUND', message }));
|
|
337
|
+
exit(1);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
write({
|
|
341
|
+
ok: false,
|
|
342
|
+
error: { code: 'COMMAND_NOT_FOUND', message },
|
|
343
|
+
meta: {
|
|
344
|
+
command: resolved.error,
|
|
345
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
exit(1);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const { command, path, rest } = resolved;
|
|
352
|
+
try {
|
|
353
|
+
const { args, options: parsedOptions } = Parser.parse(rest, {
|
|
354
|
+
alias: command.alias,
|
|
355
|
+
args: command.args,
|
|
356
|
+
options: command.options,
|
|
357
|
+
});
|
|
358
|
+
const envSource = options.env ?? process.env;
|
|
359
|
+
const env = command.env ? Parser.parseEnv(command.env, envSource) : {};
|
|
360
|
+
const okFn = (data, meta = {}) => {
|
|
361
|
+
return { [sentinel]: 'ok', data, cta: meta.cta };
|
|
362
|
+
};
|
|
363
|
+
const errorFn = (opts) => {
|
|
364
|
+
return { [sentinel]: 'error', ...opts };
|
|
365
|
+
};
|
|
366
|
+
const result = command.run({
|
|
367
|
+
args,
|
|
368
|
+
env,
|
|
369
|
+
options: parsedOptions,
|
|
370
|
+
ok: okFn,
|
|
371
|
+
error: errorFn,
|
|
372
|
+
});
|
|
373
|
+
// Streaming path — async generator
|
|
374
|
+
if (isAsyncGenerator(result)) {
|
|
375
|
+
await handleStreaming(result, {
|
|
376
|
+
name,
|
|
377
|
+
path,
|
|
378
|
+
start,
|
|
379
|
+
format,
|
|
380
|
+
formatExplicit,
|
|
381
|
+
human,
|
|
382
|
+
verbose,
|
|
383
|
+
write,
|
|
384
|
+
writeln,
|
|
385
|
+
exit,
|
|
386
|
+
});
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const awaited = await result;
|
|
390
|
+
if (isSentinel(awaited)) {
|
|
391
|
+
const cta = formatCtaBlock(name, awaited.cta);
|
|
392
|
+
if (awaited[sentinel] === 'ok') {
|
|
393
|
+
write({
|
|
394
|
+
ok: true,
|
|
395
|
+
data: awaited.data,
|
|
396
|
+
meta: {
|
|
397
|
+
command: path,
|
|
398
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
399
|
+
...(cta ? { cta } : undefined),
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
write({
|
|
405
|
+
ok: false,
|
|
406
|
+
error: {
|
|
407
|
+
code: awaited.code,
|
|
408
|
+
message: awaited.message,
|
|
409
|
+
...(awaited.retryable !== undefined ? { retryable: awaited.retryable } : undefined),
|
|
410
|
+
},
|
|
411
|
+
meta: {
|
|
412
|
+
command: path,
|
|
413
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
414
|
+
...(cta ? { cta } : undefined),
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
exit(1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
write({
|
|
422
|
+
ok: true,
|
|
423
|
+
data: awaited,
|
|
424
|
+
meta: {
|
|
425
|
+
command: path,
|
|
426
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
const errorOutput = {
|
|
433
|
+
ok: false,
|
|
434
|
+
error: {
|
|
435
|
+
code: error instanceof IncurError
|
|
436
|
+
? error.code
|
|
437
|
+
: error instanceof ValidationError
|
|
438
|
+
? 'VALIDATION_ERROR'
|
|
439
|
+
: 'UNKNOWN',
|
|
440
|
+
message: error instanceof Error ? error.message : String(error),
|
|
441
|
+
...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
|
|
442
|
+
...(error instanceof ValidationError ? { fieldErrors: error.fieldErrors } : undefined),
|
|
443
|
+
},
|
|
444
|
+
meta: {
|
|
445
|
+
command: path,
|
|
446
|
+
duration: `${Math.round(performance.now() - start)}ms`,
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
if (human && error instanceof ValidationError) {
|
|
450
|
+
writeln(formatHumanValidationError(name, path, command, error));
|
|
451
|
+
exit(1);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
write(errorOutput);
|
|
455
|
+
exit(1);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/** @internal Formats a validation error for TTY with usage hint. */
|
|
459
|
+
function formatHumanValidationError(cli, path, command, error) {
|
|
460
|
+
const lines = [];
|
|
461
|
+
for (const fe of error.fieldErrors)
|
|
462
|
+
lines.push(`Error: missing required argument <${fe.path}>`);
|
|
463
|
+
lines.push('See below for usage.');
|
|
464
|
+
lines.push('');
|
|
465
|
+
lines.push(Help.formatCommand(path === cli ? cli : `${cli} ${path}`, {
|
|
466
|
+
alias: command.alias,
|
|
467
|
+
description: command.description,
|
|
468
|
+
args: command.args,
|
|
469
|
+
env: command.env,
|
|
470
|
+
hint: command.hint,
|
|
471
|
+
options: command.options,
|
|
472
|
+
examples: formatExamples(command.examples),
|
|
473
|
+
usage: command.usage,
|
|
474
|
+
}));
|
|
475
|
+
return lines.join('\n');
|
|
476
|
+
}
|
|
477
|
+
/** @internal Resolves a command from the tree by walking tokens until a leaf is found. */
|
|
478
|
+
function resolveCommand(commands, tokens) {
|
|
479
|
+
const [first, ...rest] = tokens;
|
|
480
|
+
if (!first || !commands.has(first))
|
|
481
|
+
return { error: first ?? '(none)', path: '' };
|
|
482
|
+
let entry = commands.get(first);
|
|
483
|
+
const path = [first];
|
|
484
|
+
let remaining = rest;
|
|
485
|
+
while (isGroup(entry)) {
|
|
486
|
+
const next = remaining[0];
|
|
487
|
+
if (!next)
|
|
488
|
+
return {
|
|
489
|
+
help: true,
|
|
490
|
+
path: path.join(' '),
|
|
491
|
+
description: entry.description,
|
|
492
|
+
commands: entry.commands,
|
|
493
|
+
};
|
|
494
|
+
const child = entry.commands.get(next);
|
|
495
|
+
if (!child) {
|
|
496
|
+
return { error: next, path: path.join(' ') };
|
|
497
|
+
}
|
|
498
|
+
path.push(next);
|
|
499
|
+
remaining = remaining.slice(1);
|
|
500
|
+
entry = child;
|
|
501
|
+
}
|
|
502
|
+
return { command: entry, path: path.join(' '), rest: remaining };
|
|
503
|
+
}
|
|
504
|
+
/** @internal Extracts built-in flags (--verbose, --format, --json, --llms, --help, --version) from argv. */
|
|
505
|
+
function extractBuiltinFlags(argv) {
|
|
506
|
+
let verbose = false;
|
|
507
|
+
let llms = false;
|
|
508
|
+
let mcp = false;
|
|
509
|
+
let help = false;
|
|
510
|
+
let version = false;
|
|
511
|
+
let format = 'toon';
|
|
512
|
+
let formatExplicit = false;
|
|
513
|
+
const rest = [];
|
|
514
|
+
for (let i = 0; i < argv.length; i++) {
|
|
515
|
+
const token = argv[i];
|
|
516
|
+
if (token === '--verbose')
|
|
517
|
+
verbose = true;
|
|
518
|
+
else if (token === '--llms')
|
|
519
|
+
llms = true;
|
|
520
|
+
else if (token === '--mcp')
|
|
521
|
+
mcp = true;
|
|
522
|
+
else if (token === '--help' || token === '-h')
|
|
523
|
+
help = true;
|
|
524
|
+
else if (token === '--version')
|
|
525
|
+
version = true;
|
|
526
|
+
else if (token === '--json') {
|
|
527
|
+
format = 'json';
|
|
528
|
+
formatExplicit = true;
|
|
529
|
+
}
|
|
530
|
+
else if (token === '--format' && argv[i + 1]) {
|
|
531
|
+
format = argv[i + 1];
|
|
532
|
+
formatExplicit = true;
|
|
533
|
+
i++;
|
|
534
|
+
}
|
|
535
|
+
else
|
|
536
|
+
rest.push(token);
|
|
537
|
+
}
|
|
538
|
+
return { verbose, format, formatExplicit, llms, mcp, help, version, rest };
|
|
539
|
+
}
|
|
540
|
+
/** @internal Collects immediate child commands/groups for help output. */
|
|
541
|
+
function collectHelpCommands(commands) {
|
|
542
|
+
const result = [];
|
|
543
|
+
for (const [name, entry] of commands) {
|
|
544
|
+
if (isGroup(entry))
|
|
545
|
+
result.push({ name, description: entry.description });
|
|
546
|
+
else
|
|
547
|
+
result.push({ name, description: entry.description });
|
|
548
|
+
}
|
|
549
|
+
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
550
|
+
}
|
|
551
|
+
/** @internal Type guard for command groups. */
|
|
552
|
+
function isGroup(entry) {
|
|
553
|
+
return '_group' in entry;
|
|
554
|
+
}
|
|
555
|
+
/** @internal Maps CLI instances to their command maps. */
|
|
556
|
+
export const toCommands = new WeakMap();
|
|
557
|
+
/** @internal Maps root CLI instances to their command definitions. */
|
|
558
|
+
const toRootDefinition = new WeakMap();
|
|
559
|
+
/** @internal Sentinel symbol for `ok()` and `error()` return values. */
|
|
560
|
+
const sentinel = Symbol.for('incur.sentinel');
|
|
561
|
+
/** @internal Formats an error for human-readable TTY output. */
|
|
562
|
+
function formatHumanError(error) {
|
|
563
|
+
const prefix = error.code === 'UNKNOWN' || error.code === 'COMMAND_NOT_FOUND'
|
|
564
|
+
? 'Error'
|
|
565
|
+
: `Error (${error.code})`;
|
|
566
|
+
let out = `${prefix}: ${error.message}`;
|
|
567
|
+
if (error.fieldErrors)
|
|
568
|
+
for (const fe of error.fieldErrors)
|
|
569
|
+
out += `\n ${fe.path}: ${fe.message}`;
|
|
570
|
+
return out;
|
|
571
|
+
}
|
|
572
|
+
/** @internal Formats a CTA block for human-readable TTY output. */
|
|
573
|
+
function formatHumanCta(cta) {
|
|
574
|
+
const lines = ['', cta.description];
|
|
575
|
+
for (const c of cta.commands) {
|
|
576
|
+
const desc = c.description ? ` ${c.description}` : '';
|
|
577
|
+
lines.push(` ${c.command}${desc}`);
|
|
578
|
+
}
|
|
579
|
+
return lines.join('\n');
|
|
580
|
+
}
|
|
581
|
+
/** @internal Type guard for sentinel results. */
|
|
582
|
+
function isSentinel(value) {
|
|
583
|
+
return typeof value === 'object' && value !== null && sentinel in value;
|
|
584
|
+
}
|
|
585
|
+
/** @internal Type guard for async generators returned by streaming `run` handlers. */
|
|
586
|
+
function isAsyncGenerator(value) {
|
|
587
|
+
return (typeof value === 'object' &&
|
|
588
|
+
value !== null &&
|
|
589
|
+
Symbol.asyncIterator in value &&
|
|
590
|
+
typeof value.next === 'function');
|
|
591
|
+
}
|
|
592
|
+
/** @internal Handles streaming output from an async generator `run` handler. */
|
|
593
|
+
async function handleStreaming(generator, ctx) {
|
|
594
|
+
// Incremental: human, no explicit format (default toon), or explicit jsonl
|
|
595
|
+
// Buffered: explicit json/yaml/toon/md
|
|
596
|
+
const useJsonl = !ctx.human && ctx.formatExplicit && ctx.format === 'jsonl';
|
|
597
|
+
const incremental = ctx.human || useJsonl || !ctx.formatExplicit;
|
|
598
|
+
if (incremental) {
|
|
599
|
+
// Incremental output: write each chunk as it arrives
|
|
600
|
+
try {
|
|
601
|
+
let returnValue;
|
|
602
|
+
while (true) {
|
|
603
|
+
const { value, done } = await generator.next();
|
|
604
|
+
if (done) {
|
|
605
|
+
returnValue = value;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
if (isSentinel(value)) {
|
|
609
|
+
const tagged = value;
|
|
610
|
+
if (tagged[sentinel] === 'error') {
|
|
611
|
+
if (useJsonl)
|
|
612
|
+
ctx.writeln(JSON.stringify({
|
|
613
|
+
type: 'error',
|
|
614
|
+
ok: false,
|
|
615
|
+
error: {
|
|
616
|
+
code: tagged.code,
|
|
617
|
+
message: tagged.message,
|
|
618
|
+
...(tagged.retryable !== undefined
|
|
619
|
+
? { retryable: tagged.retryable }
|
|
620
|
+
: undefined),
|
|
621
|
+
},
|
|
622
|
+
}));
|
|
623
|
+
else
|
|
624
|
+
ctx.writeln(formatHumanError({ code: tagged.code, message: tagged.message }));
|
|
625
|
+
ctx.exit(1);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (useJsonl)
|
|
630
|
+
ctx.writeln(JSON.stringify({ type: 'chunk', data: value }));
|
|
631
|
+
else
|
|
632
|
+
ctx.writeln(Formatter.format(value, 'toon'));
|
|
633
|
+
}
|
|
634
|
+
// Handle return value — error() or ok() sentinel
|
|
635
|
+
if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
|
|
636
|
+
const err = returnValue;
|
|
637
|
+
if (useJsonl)
|
|
638
|
+
ctx.writeln(JSON.stringify({
|
|
639
|
+
type: 'error',
|
|
640
|
+
ok: false,
|
|
641
|
+
error: {
|
|
642
|
+
code: err.code,
|
|
643
|
+
message: err.message,
|
|
644
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
645
|
+
},
|
|
646
|
+
}));
|
|
647
|
+
else
|
|
648
|
+
ctx.writeln(formatHumanError({ code: err.code, message: err.message }));
|
|
649
|
+
ctx.exit(1);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const cta = isSentinel(returnValue) && returnValue[sentinel] === 'ok'
|
|
653
|
+
? formatCtaBlock(ctx.name, returnValue.cta)
|
|
654
|
+
: undefined;
|
|
655
|
+
if (useJsonl)
|
|
656
|
+
ctx.writeln(JSON.stringify({
|
|
657
|
+
type: 'done',
|
|
658
|
+
ok: true,
|
|
659
|
+
meta: {
|
|
660
|
+
command: ctx.path,
|
|
661
|
+
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
662
|
+
...(cta ? { cta } : undefined),
|
|
663
|
+
},
|
|
664
|
+
}));
|
|
665
|
+
else if (cta)
|
|
666
|
+
ctx.writeln(formatHumanCta(cta));
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
if (useJsonl)
|
|
670
|
+
ctx.writeln(JSON.stringify({
|
|
671
|
+
type: 'error',
|
|
672
|
+
ok: false,
|
|
673
|
+
error: {
|
|
674
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
675
|
+
message: error instanceof Error ? error.message : String(error),
|
|
676
|
+
},
|
|
677
|
+
}));
|
|
678
|
+
else
|
|
679
|
+
ctx.writeln(formatHumanError({
|
|
680
|
+
code: 'UNKNOWN',
|
|
681
|
+
message: error instanceof Error ? error.message : String(error),
|
|
682
|
+
}));
|
|
683
|
+
ctx.exit(1);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else {
|
|
687
|
+
// Buffered output: collect all chunks, write as single value
|
|
688
|
+
const chunks = [];
|
|
689
|
+
try {
|
|
690
|
+
let returnValue;
|
|
691
|
+
while (true) {
|
|
692
|
+
const { value, done } = await generator.next();
|
|
693
|
+
if (done) {
|
|
694
|
+
returnValue = value;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
if (isSentinel(value)) {
|
|
698
|
+
const tagged = value;
|
|
699
|
+
if (tagged[sentinel] === 'error') {
|
|
700
|
+
ctx.write({
|
|
701
|
+
ok: false,
|
|
702
|
+
error: {
|
|
703
|
+
code: tagged.code,
|
|
704
|
+
message: tagged.message,
|
|
705
|
+
...(tagged.retryable !== undefined ? { retryable: tagged.retryable } : undefined),
|
|
706
|
+
},
|
|
707
|
+
meta: {
|
|
708
|
+
command: ctx.path,
|
|
709
|
+
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
710
|
+
},
|
|
711
|
+
});
|
|
712
|
+
ctx.exit(1);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
chunks.push(value);
|
|
717
|
+
}
|
|
718
|
+
if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
|
|
719
|
+
const err = returnValue;
|
|
720
|
+
ctx.write({
|
|
721
|
+
ok: false,
|
|
722
|
+
error: {
|
|
723
|
+
code: err.code,
|
|
724
|
+
message: err.message,
|
|
725
|
+
...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
|
|
726
|
+
},
|
|
727
|
+
meta: {
|
|
728
|
+
command: ctx.path,
|
|
729
|
+
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
730
|
+
},
|
|
731
|
+
});
|
|
732
|
+
ctx.exit(1);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const cta = isSentinel(returnValue) && returnValue[sentinel] === 'ok'
|
|
736
|
+
? formatCtaBlock(ctx.name, returnValue.cta)
|
|
737
|
+
: undefined;
|
|
738
|
+
ctx.write({
|
|
739
|
+
ok: true,
|
|
740
|
+
data: chunks,
|
|
741
|
+
meta: {
|
|
742
|
+
command: ctx.path,
|
|
743
|
+
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
744
|
+
...(cta ? { cta } : undefined),
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
ctx.write({
|
|
750
|
+
ok: false,
|
|
751
|
+
error: {
|
|
752
|
+
code: error instanceof IncurError ? error.code : 'UNKNOWN',
|
|
753
|
+
message: error instanceof Error ? error.message : String(error),
|
|
754
|
+
},
|
|
755
|
+
meta: {
|
|
756
|
+
command: ctx.path,
|
|
757
|
+
duration: `${Math.round(performance.now() - ctx.start)}ms`,
|
|
758
|
+
},
|
|
759
|
+
});
|
|
760
|
+
ctx.exit(1);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/** @internal Formats a CTA block into the output envelope shape. */
|
|
765
|
+
function formatCtaBlock(name, block) {
|
|
766
|
+
if (!block || block.commands.length === 0)
|
|
767
|
+
return undefined;
|
|
768
|
+
return {
|
|
769
|
+
description: block.description ?? 'Suggested commands:',
|
|
770
|
+
commands: block.commands.map((c) => formatCta(name, c)),
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
/** @internal Formats a CTA by prefixing the CLI name. Handles string and object forms. */
|
|
774
|
+
function formatCta(name, cta) {
|
|
775
|
+
if (typeof cta === 'string')
|
|
776
|
+
return { command: `${name} ${cta}` };
|
|
777
|
+
const prefix = cta.command === name || cta.command.startsWith(`${name} `) ? '' : `${name} `;
|
|
778
|
+
let cmd = `${prefix}${cta.command}`;
|
|
779
|
+
if (cta.args)
|
|
780
|
+
for (const [key, value] of Object.entries(cta.args))
|
|
781
|
+
cmd += value === true ? ` <${key}>` : ` ${value}`;
|
|
782
|
+
if (cta.options)
|
|
783
|
+
for (const [key, value] of Object.entries(cta.options))
|
|
784
|
+
cmd += value === true ? ` --${key} <${key}>` : ` --${key} ${value}`;
|
|
785
|
+
return { command: cmd, ...(cta.description ? { description: cta.description } : undefined) };
|
|
786
|
+
}
|
|
787
|
+
/** @internal Builds the `--llms` manifest from the command tree. */
|
|
788
|
+
function buildManifest(commands, prefix = []) {
|
|
789
|
+
return {
|
|
790
|
+
version: 'incur.v1',
|
|
791
|
+
commands: collectCommands(commands, prefix).sort((a, b) => a.name.localeCompare(b.name)),
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/** @internal Recursively collects leaf commands with their full paths. */
|
|
795
|
+
function collectCommands(commands, prefix) {
|
|
796
|
+
const result = [];
|
|
797
|
+
for (const [name, entry] of commands) {
|
|
798
|
+
const path = [...prefix, name];
|
|
799
|
+
if (isGroup(entry)) {
|
|
800
|
+
result.push(...collectCommands(entry.commands, path));
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
const cmd = { name: path.join(' ') };
|
|
804
|
+
if (entry.description)
|
|
805
|
+
cmd.description = entry.description;
|
|
806
|
+
const inputSchema = buildInputSchema(entry.args, entry.env, entry.options);
|
|
807
|
+
const outputSchema = entry.output ? Schema.toJsonSchema(entry.output) : undefined;
|
|
808
|
+
if (inputSchema || outputSchema) {
|
|
809
|
+
cmd.schema = {};
|
|
810
|
+
if (inputSchema?.args)
|
|
811
|
+
cmd.schema.args = inputSchema.args;
|
|
812
|
+
if (inputSchema?.env)
|
|
813
|
+
cmd.schema.env = inputSchema.env;
|
|
814
|
+
if (inputSchema?.options)
|
|
815
|
+
cmd.schema.options = inputSchema.options;
|
|
816
|
+
if (outputSchema)
|
|
817
|
+
cmd.schema.output = outputSchema;
|
|
818
|
+
}
|
|
819
|
+
const examples = formatExamples(entry.examples);
|
|
820
|
+
if (examples) {
|
|
821
|
+
const cmdName = path.join(' ');
|
|
822
|
+
cmd.examples = examples.map((e) => ({
|
|
823
|
+
...e,
|
|
824
|
+
command: e.command ? `${cmdName} ${e.command}` : cmdName,
|
|
825
|
+
}));
|
|
826
|
+
}
|
|
827
|
+
result.push(cmd);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return result;
|
|
831
|
+
}
|
|
832
|
+
/** @internal Recursively collects leaf commands as `Skill.CommandInfo` for `--llms --format md`. */
|
|
833
|
+
function collectSkillCommands(commands, prefix, groups) {
|
|
834
|
+
const result = [];
|
|
835
|
+
for (const [name, entry] of commands) {
|
|
836
|
+
const path = [...prefix, name];
|
|
837
|
+
if (isGroup(entry)) {
|
|
838
|
+
if (entry.description)
|
|
839
|
+
groups.set(path.join(' '), entry.description);
|
|
840
|
+
result.push(...collectSkillCommands(entry.commands, path, groups));
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
const cmd = { name: path.join(' ') };
|
|
844
|
+
if (entry.description)
|
|
845
|
+
cmd.description = entry.description;
|
|
846
|
+
if (entry.args)
|
|
847
|
+
cmd.args = entry.args;
|
|
848
|
+
if (entry.env)
|
|
849
|
+
cmd.env = entry.env;
|
|
850
|
+
if (entry.hint)
|
|
851
|
+
cmd.hint = entry.hint;
|
|
852
|
+
if (entry.options)
|
|
853
|
+
cmd.options = entry.options;
|
|
854
|
+
if (entry.output)
|
|
855
|
+
cmd.output = entry.output;
|
|
856
|
+
const examples = formatExamples(entry.examples);
|
|
857
|
+
if (examples) {
|
|
858
|
+
const cmdName = path.join(' ');
|
|
859
|
+
cmd.examples = examples.map((e) => ({
|
|
860
|
+
...e,
|
|
861
|
+
command: e.command ? `${cmdName} ${e.command}` : cmdName,
|
|
862
|
+
}));
|
|
863
|
+
}
|
|
864
|
+
result.push(cmd);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return result.sort((a, b) => a.name.localeCompare(b.name));
|
|
868
|
+
}
|
|
869
|
+
/** @internal Formats examples into `{ command, description }` objects. `command` is the args/options suffix only. */
|
|
870
|
+
export function formatExamples(examples) {
|
|
871
|
+
if (!examples || examples.length === 0)
|
|
872
|
+
return undefined;
|
|
873
|
+
return examples.map((ex) => {
|
|
874
|
+
const parts = [];
|
|
875
|
+
if (ex.args)
|
|
876
|
+
for (const value of Object.values(ex.args))
|
|
877
|
+
parts.push(String(value));
|
|
878
|
+
if (ex.options)
|
|
879
|
+
for (const [key, value] of Object.entries(ex.options))
|
|
880
|
+
parts.push(`--${key} ${value}`);
|
|
881
|
+
const result = { command: parts.join(' ') };
|
|
882
|
+
if (ex.description)
|
|
883
|
+
result.description = ex.description;
|
|
884
|
+
return result;
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
/** @internal Builds separate args, env, and options JSON Schemas. */
|
|
888
|
+
function buildInputSchema(args, env, options) {
|
|
889
|
+
if (!args && !env && !options)
|
|
890
|
+
return undefined;
|
|
891
|
+
const result = {};
|
|
892
|
+
if (args)
|
|
893
|
+
result.args = Schema.toJsonSchema(args);
|
|
894
|
+
if (env)
|
|
895
|
+
result.env = Schema.toJsonSchema(env);
|
|
896
|
+
if (options)
|
|
897
|
+
result.options = Schema.toJsonSchema(options);
|
|
898
|
+
return result;
|
|
899
|
+
}
|
|
900
|
+
//# sourceMappingURL=Cli.js.map
|