incur 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Cli.js CHANGED
@@ -5,6 +5,8 @@ import * as Fetch from './Fetch.js';
5
5
  import * as Filter from './Filter.js';
6
6
  import * as Formatter from './Formatter.js';
7
7
  import * as Help from './Help.js';
8
+ import { builtinCommands, shells } from './internal/command.js';
9
+ import * as Command from './internal/command.js';
8
10
  import { detectRunner } from './internal/pm.js';
9
11
  import * as Mcp from './Mcp.js';
10
12
  import * as Openapi from './Openapi.js';
@@ -76,10 +78,13 @@ export function create(nameOrDefinition, definition) {
76
78
  if (pending.length > 0)
77
79
  await Promise.all(pending);
78
80
  return fetchImpl(name, commands, req, {
81
+ envSchema: def.env,
79
82
  mcpHandler,
80
83
  middlewares,
84
+ name,
81
85
  rootCommand: rootDef,
82
86
  vars: def.vars,
87
+ version: def.version,
83
88
  });
84
89
  },
85
90
  async serve(argv = process.argv.slice(2), serveOptions = {}) {
@@ -122,7 +127,12 @@ async function serveImpl(name, commands, argv, options = {}) {
122
127
  const { verbose, format: formatFlag, formatExplicit, filterOutput, tokenLimit, tokenOffset, tokenCount, llms, llmsFull, mcp: mcpFlag, help, version, schema, rest: filtered, } = extractBuiltinFlags(argv);
123
128
  // --mcp: start as MCP stdio server
124
129
  if (mcpFlag) {
125
- await Mcp.serve(name, options.version ?? '0.0.0', commands);
130
+ await Mcp.serve(name, options.version ?? '0.0.0', commands, {
131
+ middlewares: options.middlewares,
132
+ env: options.envSchema,
133
+ vars: options.vars,
134
+ version: options.version,
135
+ });
126
136
  return;
127
137
  }
128
138
  // COMPLETE: dynamic shell completions (called by shell hook at tab-press)
@@ -139,6 +149,27 @@ async function serveImpl(name, commands, argv, options = {}) {
139
149
  else {
140
150
  const index = Number(process.env._COMPLETE_INDEX ?? words.length - 1);
141
151
  const candidates = Completions.complete(commands, options.rootCommand, words, index);
152
+ // Add built-in commands (completions, mcp, skills) to completions
153
+ const current = words[index] ?? '';
154
+ const nonFlags = words.slice(0, index).filter((w) => !w.startsWith('-'));
155
+ if (nonFlags.length <= 1) {
156
+ for (const b of builtinCommands) {
157
+ if (b.name.startsWith(current) && !candidates.some((c) => c.value === b.name))
158
+ candidates.push({
159
+ value: b.name,
160
+ description: b.description,
161
+ ...(b.subcommands ? { noSpace: true } : undefined),
162
+ });
163
+ }
164
+ }
165
+ else if (nonFlags.length === 2) {
166
+ const parent = nonFlags[nonFlags.length - 1];
167
+ const builtin = builtinCommands.find((b) => b.name === parent && b.subcommands);
168
+ if (builtin?.subcommands)
169
+ for (const sub of builtin.subcommands)
170
+ if (sub.name.startsWith(current))
171
+ candidates.push({ value: sub.name, description: sub.description });
172
+ }
142
173
  const out = Completions.format(completeShell, candidates);
143
174
  if (out)
144
175
  stdout(out);
@@ -219,41 +250,23 @@ async function serveImpl(name, commands, argv, options = {}) {
219
250
  // not a completions invocation
220
251
  return -1;
221
252
  })();
253
+ // TODO: refactor built-in command handlers (completions, skills, mcp) into a generic dispatch loop on `builtinCommands`
222
254
  if (completionsIdx !== -1 && filtered[completionsIdx] === 'completions') {
223
- if (help) {
224
- writeln([
225
- `${name} completions Generate shell completion script`,
226
- '',
227
- `Usage: ${name} completions <shell>`,
228
- '',
229
- 'Shells:',
230
- ' bash',
231
- ' fish',
232
- ' nushell',
233
- ' zsh',
234
- '',
235
- 'Setup:',
236
- ...(() => {
237
- const rows = [
238
- ['bash', `eval "$(${name} completions bash)"`, '# add to ~/.bashrc'],
239
- ['zsh', `eval "$(${name} completions zsh)"`, '# add to ~/.zshrc'],
240
- ['fish', `${name} completions fish | source`, '# add to ~/.config/fish/config.fish'],
241
- ['nushell', `see \`${name} completions nushell\``, '# add to config.nu'],
242
- ];
243
- const shellW = Math.max(...rows.map((r) => r[0].length));
244
- const cmdW = Math.max(...rows.map((r) => r[1].length));
245
- return rows.map(([shell, cmd, comment]) => ` ${shell.padEnd(shellW)} ${cmd.padEnd(cmdW)} ${comment}`);
246
- })(),
247
- ].join('\n'));
255
+ const shell = filtered[completionsIdx + 1];
256
+ if (help || !shell) {
257
+ const b = builtinCommands.find((c) => c.name === 'completions');
258
+ writeln(Help.formatCommand(`${name} completions`, {
259
+ args: b.args,
260
+ description: b.description,
261
+ hideGlobalOptions: true,
262
+ hint: b.hint?.(name),
263
+ }));
248
264
  return;
249
265
  }
250
- const shell = filtered[completionsIdx + 1];
251
- if (!shell || !['bash', 'fish', 'nushell', 'zsh'].includes(shell)) {
266
+ if (!shells.includes(shell)) {
252
267
  writeln(formatHumanError({
253
268
  code: 'INVALID_SHELL',
254
- message: shell
255
- ? `Unknown shell '${shell}'. Supported: bash, fish, nushell, zsh`
256
- : `Missing shell argument. Usage: ${name} completions <bash|fish|nushell|zsh>`,
269
+ message: `Unknown shell '${shell}'. Supported: ${shells.join(', ')}`,
257
270
  }));
258
271
  exit(1);
259
272
  return;
@@ -264,17 +277,15 @@ async function serveImpl(name, commands, argv, options = {}) {
264
277
  }
265
278
  // skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
266
279
  const skillsIdx = filtered[0] === 'skills' ? 0 : filtered[0] === name && filtered[1] === 'skills' ? 1 : -1;
267
- if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills' && filtered[skillsIdx + 1] === 'add') {
280
+ if (skillsIdx !== -1 && filtered[skillsIdx] === 'skills') {
281
+ if (filtered[skillsIdx + 1] !== 'add') {
282
+ const b = builtinCommands.find((c) => c.name === 'skills');
283
+ writeln(formatBuiltinHelp(name, b));
284
+ return;
285
+ }
268
286
  if (help) {
269
- writeln([
270
- `${name} skills add — Sync skill files to your agent`,
271
- '',
272
- `Usage: ${name} skills add [options]`,
273
- '',
274
- 'Options:',
275
- ' --depth <number> Grouping depth for skill files (default: 1)',
276
- ' --no-global Install to project instead of globally',
277
- ].join('\n'));
287
+ const b = builtinCommands.find((c) => c.name === 'skills');
288
+ writeln(formatBuiltinSubcommandHelp(name, b, 'add'));
278
289
  return;
279
290
  }
280
291
  const rest = filtered.slice(skillsIdx + 2);
@@ -333,18 +344,15 @@ async function serveImpl(name, commands, argv, options = {}) {
333
344
  }
334
345
  // mcp add: register CLI as MCP server via `npx add-mcp`
335
346
  const mcpIdx = filtered[0] === 'mcp' ? 0 : filtered[0] === name && filtered[1] === 'mcp' ? 1 : -1;
336
- if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp' && filtered[mcpIdx + 1] === 'add') {
347
+ if (mcpIdx !== -1 && filtered[mcpIdx] === 'mcp') {
348
+ if (filtered[mcpIdx + 1] !== 'add') {
349
+ const b = builtinCommands.find((c) => c.name === 'mcp');
350
+ writeln(formatBuiltinHelp(name, b));
351
+ return;
352
+ }
337
353
  if (help) {
338
- writeln([
339
- `${name} mcp add — Register as an MCP server for your agent`,
340
- '',
341
- `Usage: ${name} mcp add [options]`,
342
- '',
343
- 'Options:',
344
- ' -c, --command <cmd> Override the command agents will run (e.g. "pnpm my-cli --mcp")',
345
- ' --no-global Install to project instead of globally',
346
- ' --agent <agent> Target a specific agent (e.g. claude-code, cursor)',
347
- ].join('\n'));
354
+ const b = builtinCommands.find((c) => c.name === 'mcp');
355
+ writeln(formatBuiltinSubcommandHelp(name, b, 'add'));
348
356
  return;
349
357
  }
350
358
  const rest = filtered.slice(mcpIdx + 2);
@@ -807,178 +815,86 @@ async function serveImpl(name, commands, argv, options = {}) {
807
815
  : []),
808
816
  ...(command.middleware ?? []),
809
817
  ];
810
- // Initialize vars from schema defaults
811
- const varsMap = options.vars ? options.vars.parse({}) : {};
812
- const envSource = options.env ?? process.env;
813
- const runCommand = async () => {
814
- const { args, options: parsedOptions } = Parser.parse(rest, {
815
- alias: command.alias,
816
- args: command.args,
817
- options: command.options,
818
- });
819
- if (human)
820
- emitDeprecationWarnings(rest, command.options, command.alias);
821
- const env = command.env ? Parser.parseEnv(command.env, envSource) : {};
822
- const okFn = (data, meta = {}) => {
823
- return { [sentinel]: 'ok', data, cta: meta.cta };
824
- };
825
- const errorFn = (opts) => {
826
- return { [sentinel]: 'error', ...opts };
827
- };
828
- const result = command.run({
829
- agent: !human,
830
- args,
831
- env,
832
- error: errorFn,
818
+ if (human)
819
+ emitDeprecationWarnings(rest, command.options, command.alias);
820
+ const result = await Command.execute(command, {
821
+ agent: !human,
822
+ argv: rest,
823
+ env: options.envSchema,
824
+ envSource: options.env,
825
+ format,
826
+ formatExplicit,
827
+ inputOptions: {},
828
+ middlewares: allMiddleware,
829
+ name,
830
+ path,
831
+ vars: options.vars,
832
+ version: options.version,
833
+ });
834
+ const duration = `${Math.round(performance.now() - start)}ms`;
835
+ // Streaming path — async generator
836
+ if ('stream' in result) {
837
+ await handleStreaming(result.stream, {
838
+ name,
839
+ path,
840
+ start,
833
841
  format,
834
842
  formatExplicit,
835
- name,
836
- ok: okFn,
837
- options: parsedOptions,
838
- var: varsMap,
839
- version: options.version,
843
+ human,
844
+ renderOutput,
845
+ verbose,
846
+ truncate,
847
+ write,
848
+ writeln,
849
+ exit,
840
850
  });
841
- // Streaming path — async generator
842
- if (isAsyncGenerator(result)) {
843
- await handleStreaming(result, {
844
- name,
845
- path,
846
- start,
847
- format,
848
- formatExplicit,
849
- human,
850
- renderOutput,
851
- verbose,
852
- truncate,
853
- write,
854
- writeln,
855
- exit,
856
- });
857
- return;
858
- }
859
- const awaited = await result;
860
- if (isSentinel(awaited)) {
861
- const cta = formatCtaBlock(name, awaited.cta);
862
- if (awaited[sentinel] === 'ok') {
863
- write({
864
- ok: true,
865
- data: awaited.data,
866
- meta: {
867
- command: path,
868
- duration: `${Math.round(performance.now() - start)}ms`,
869
- ...(cta ? { cta } : undefined),
870
- },
871
- });
872
- }
873
- else {
874
- const err = awaited;
875
- write({
876
- ok: false,
877
- error: {
878
- code: err.code,
879
- message: err.message,
880
- ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
881
- },
882
- meta: {
883
- command: path,
884
- duration: `${Math.round(performance.now() - start)}ms`,
885
- ...(cta ? { cta } : undefined),
886
- },
887
- });
888
- exit(err.exitCode ?? 1);
889
- }
890
- }
891
- else {
892
- write({
893
- ok: true,
894
- data: awaited,
895
- meta: {
896
- command: path,
897
- duration: `${Math.round(performance.now() - start)}ms`,
898
- },
899
- });
900
- }
901
- };
902
- try {
903
- const cliEnv = options.envSchema ? Parser.parseEnv(options.envSchema, envSource) : {};
904
- if (allMiddleware.length > 0) {
905
- const errorFn = (opts) => {
906
- return { [sentinel]: 'error', ...opts };
907
- };
908
- const mwCtx = {
909
- agent: !human,
851
+ return;
852
+ }
853
+ if (result.ok) {
854
+ const cta = formatCtaBlock(name, result.cta);
855
+ write({
856
+ ok: true,
857
+ data: result.data,
858
+ meta: {
910
859
  command: path,
911
- env: cliEnv,
912
- error: errorFn,
913
- format,
914
- formatExplicit,
915
- name,
916
- set(key, value) {
917
- varsMap[key] = value;
918
- },
919
- var: varsMap,
920
- version: options.version,
921
- };
922
- const handleMwSentinel = (result) => {
923
- if (!isSentinel(result) || result[sentinel] !== 'error')
924
- return;
925
- const err = result;
926
- const cta = formatCtaBlock(name, err.cta);
927
- write({
928
- ok: false,
929
- error: {
930
- code: err.code,
931
- message: err.message,
932
- ...(err.retryable !== undefined ? { retryable: err.retryable } : undefined),
933
- },
934
- meta: {
935
- command: path,
936
- duration: `${Math.round(performance.now() - start)}ms`,
937
- ...(cta ? { cta } : undefined),
938
- },
939
- });
940
- exit(err.exitCode ?? 1);
941
- };
942
- const composed = allMiddleware.reduceRight((next, mw) => async () => {
943
- handleMwSentinel(await mw(mwCtx, next));
944
- }, runCommand);
945
- await composed();
946
- }
947
- else {
948
- await runCommand();
949
- }
860
+ duration,
861
+ ...(cta ? { cta } : undefined),
862
+ },
863
+ });
950
864
  }
951
- catch (error) {
952
- const errorOutput = {
865
+ else {
866
+ const cta = formatCtaBlock(name, result.cta);
867
+ if (human && !formatExplicit && result.error.fieldErrors) {
868
+ writeln(formatHumanValidationError(name, path, command, new ValidationError({
869
+ message: result.error.message,
870
+ fieldErrors: result.error.fieldErrors,
871
+ }), options.env));
872
+ exit(1);
873
+ return;
874
+ }
875
+ write({
953
876
  ok: false,
954
877
  error: {
955
- code: error instanceof IncurError
956
- ? error.code
957
- : error instanceof ValidationError
958
- ? 'VALIDATION_ERROR'
959
- : 'UNKNOWN',
960
- message: error instanceof Error ? error.message : String(error),
961
- ...(error instanceof IncurError ? { retryable: error.retryable } : undefined),
962
- ...(error instanceof ValidationError ? { fieldErrors: error.fieldErrors } : undefined),
878
+ code: result.error.code,
879
+ message: result.error.message,
880
+ ...(result.error.retryable !== undefined
881
+ ? { retryable: result.error.retryable }
882
+ : undefined),
883
+ ...(result.error.fieldErrors ? { fieldErrors: result.error.fieldErrors } : undefined),
963
884
  },
964
885
  meta: {
965
886
  command: path,
966
- duration: `${Math.round(performance.now() - start)}ms`,
887
+ duration,
888
+ ...(cta ? { cta } : undefined),
967
889
  },
968
- };
969
- if (human && !formatExplicit && error instanceof ValidationError) {
970
- writeln(formatHumanValidationError(name, path, command, error, options.env));
971
- exit(1);
972
- return;
973
- }
974
- write(errorOutput);
975
- exit(error instanceof IncurError ? (error.exitCode ?? 1) : 1);
890
+ });
891
+ exit(result.exitCode ?? 1);
976
892
  }
977
893
  }
978
894
  /** @internal Creates a lazy MCP HTTP handler scoped to a CLI instance. */
979
895
  function createMcpHttpHandler(name, version) {
980
896
  let transport;
981
- return async (req, commands) => {
897
+ return async (req, commands, mcpOptions) => {
982
898
  if (!transport) {
983
899
  const { McpServer } = await import('@modelcontextprotocol/sdk/server/mcp.js');
984
900
  const { WebStandardStreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js');
@@ -994,7 +910,13 @@ function createMcpHttpHandler(name, version) {
994
910
  ...(hasInput ? { inputSchema: mergedShape } : undefined),
995
911
  }, async (...callArgs) => {
996
912
  const params = hasInput ? callArgs[0] : {};
997
- return Mcp.callTool(tool, params);
913
+ return Mcp.callTool(tool, params, {
914
+ name,
915
+ version,
916
+ middlewares: mcpOptions?.middlewares,
917
+ env: mcpOptions?.env,
918
+ vars: mcpOptions?.vars,
919
+ });
998
920
  });
999
921
  }
1000
922
  transport = new WebStandardStreamableHTTPServerTransport({
@@ -1013,7 +935,11 @@ async function fetchImpl(name, commands, req, options = {}) {
1013
935
  const segments = url.pathname.split('/').filter(Boolean);
1014
936
  // MCP over HTTP: route /mcp to the MCP transport
1015
937
  if (segments[0] === 'mcp' && segments.length === 1 && options.mcpHandler)
1016
- return options.mcpHandler(req, commands);
938
+ return options.mcpHandler(req, commands, {
939
+ middlewares: options.middlewares,
940
+ env: options.envSchema,
941
+ vars: options.vars,
942
+ });
1017
943
  // .well-known/skills/ — Agent Skills Discovery (RFC)
1018
944
  if (segments[0] === '.well-known' &&
1019
945
  segments[1] === 'skills' &&
@@ -1103,7 +1029,11 @@ async function fetchImpl(name, commands, req, options = {}) {
1103
1029
  if ('fetchGateway' in resolved)
1104
1030
  return resolved.fetchGateway.fetch(req);
1105
1031
  const { command, path, rest } = resolved;
1106
- return executeCommand(path, command, rest, inputOptions, start, options);
1032
+ const groupMiddlewares = 'middlewares' in resolved ? resolved.middlewares : [];
1033
+ return executeCommand(path, command, rest, inputOptions, start, {
1034
+ ...options,
1035
+ groupMiddlewares,
1036
+ });
1107
1037
  }
1108
1038
  /** @internal Executes a resolved command for the fetch handler and returns a JSON Response. */
1109
1039
  async function executeCommand(path, command, rest, inputOptions, start, options) {
@@ -1113,153 +1043,87 @@ async function executeCommand(path, command, rest, inputOptions, start, options)
1113
1043
  headers: { 'content-type': 'application/json' },
1114
1044
  });
1115
1045
  }
1116
- const sentinel_ = Symbol.for('incur.sentinel');
1117
- const varsMap = options.vars ? options.vars.parse({}) : {};
1118
- let response;
1119
- const runCommand = async () => {
1120
- const { args } = Parser.parse(rest, { args: command.args });
1121
- const parsedOptions = command.options ? command.options.parse(inputOptions) : {};
1122
- const okFn = (data) => ({ [sentinel_]: 'ok', data });
1123
- const errorFn = (opts) => ({ [sentinel_]: 'error', ...opts });
1124
- const result = command.run({
1125
- agent: true,
1126
- args,
1127
- env: {},
1128
- error: errorFn,
1129
- format: 'json',
1130
- formatExplicit: true,
1131
- name: path,
1132
- ok: okFn,
1133
- options: parsedOptions,
1134
- var: varsMap,
1135
- version: undefined,
1136
- });
1137
- // Streaming path async generator → NDJSON response
1138
- if (isAsyncGenerator(result)) {
1139
- const stream = new ReadableStream({
1140
- async start(controller) {
1141
- const encoder = new TextEncoder();
1142
- try {
1143
- let returnValue;
1144
- while (true) {
1145
- const { value, done } = await result.next();
1146
- if (done) {
1147
- returnValue = value;
1148
- break;
1149
- }
1150
- if (isSentinel(value) && value[sentinel] === 'error') {
1151
- const tagged = value;
1152
- controller.enqueue(encoder.encode(JSON.stringify({
1153
- type: 'error',
1154
- ok: false,
1155
- error: { code: tagged.code, message: tagged.message },
1156
- }) + '\n'));
1157
- controller.close();
1158
- return;
1159
- }
1160
- controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'));
1161
- }
1162
- const meta = { command: path };
1163
- if (isSentinel(returnValue) && returnValue[sentinel] === 'error') {
1164
- const tagged = returnValue;
1165
- controller.enqueue(encoder.encode(JSON.stringify({
1166
- type: 'error',
1167
- ok: false,
1168
- error: { code: tagged.code, message: tagged.message },
1169
- }) + '\n'));
1170
- }
1171
- else {
1172
- controller.enqueue(encoder.encode(JSON.stringify({ type: 'done', ok: true, meta }) + '\n'));
1173
- }
1174
- }
1175
- catch (error) {
1176
- controller.enqueue(encoder.encode(JSON.stringify({
1177
- type: 'error',
1178
- ok: false,
1179
- error: {
1180
- code: 'UNKNOWN',
1181
- message: error instanceof Error ? error.message : String(error),
1182
- },
1183
- }) + '\n'));
1046
+ const allMiddleware = [
1047
+ ...(options.middlewares ?? []),
1048
+ ...(options.groupMiddlewares ?? []),
1049
+ ...(command.middleware ?? []),
1050
+ ];
1051
+ const result = await Command.execute(command, {
1052
+ agent: true,
1053
+ argv: rest,
1054
+ env: options.envSchema,
1055
+ format: 'json',
1056
+ formatExplicit: true,
1057
+ inputOptions,
1058
+ middlewares: allMiddleware,
1059
+ name: options.name ?? path,
1060
+ parseMode: 'split',
1061
+ path,
1062
+ vars: options.vars,
1063
+ version: options.version,
1064
+ });
1065
+ const duration = `${Math.round(performance.now() - start)}ms`;
1066
+ // Streaming path — async generator → NDJSON response
1067
+ if ('stream' in result) {
1068
+ const stream = new ReadableStream({
1069
+ async start(controller) {
1070
+ const encoder = new TextEncoder();
1071
+ try {
1072
+ for await (const value of result.stream) {
1073
+ controller.enqueue(encoder.encode(JSON.stringify({ type: 'chunk', data: value }) + '\n'));
1184
1074
  }
1185
- controller.close();
1186
- },
1187
- });
1188
- response = new Response(stream, {
1189
- status: 200,
1190
- headers: { 'content-type': 'application/x-ndjson' },
1191
- });
1192
- return;
1193
- }
1194
- const awaited = await result;
1195
- const duration = `${Math.round(performance.now() - start)}ms`;
1196
- if (typeof awaited === 'object' && awaited !== null && sentinel_ in awaited) {
1197
- const tagged = awaited;
1198
- if (tagged[sentinel_] === 'error')
1199
- response = jsonResponse({
1200
- ok: false,
1201
- error: { code: tagged.code, message: tagged.message },
1202
- meta: { command: path, duration },
1203
- }, 500);
1204
- else
1205
- response = jsonResponse({ ok: true, data: tagged.data, meta: { command: path, duration } }, 200);
1206
- return;
1207
- }
1208
- response = jsonResponse({ ok: true, data: awaited, meta: { command: path, duration } }, 200);
1209
- };
1210
- try {
1211
- const allMiddleware = options.middlewares ?? [];
1212
- if (allMiddleware.length > 0) {
1213
- const errorFn = (opts) => {
1214
- const duration = `${Math.round(performance.now() - start)}ms`;
1215
- response = jsonResponse({
1216
- ok: false,
1217
- error: { code: opts.code, message: opts.message },
1218
- meta: { command: path, duration },
1219
- }, 500);
1220
- return undefined;
1221
- };
1222
- const mwCtx = {
1223
- agent: true,
1224
- command: path,
1225
- env: {},
1226
- error: errorFn,
1227
- format: 'json',
1228
- formatExplicit: true,
1229
- name: path,
1230
- set(key, value) {
1231
- varsMap[key] = value;
1232
- },
1233
- var: varsMap,
1234
- version: undefined,
1235
- };
1236
- const composed = allMiddleware.reduceRight((next, mw) => async () => {
1237
- await mw(mwCtx, next);
1238
- }, runCommand);
1239
- await composed();
1240
- }
1241
- else {
1242
- await runCommand();
1243
- }
1075
+ controller.enqueue(encoder.encode(JSON.stringify({
1076
+ type: 'done',
1077
+ ok: true,
1078
+ meta: { command: path },
1079
+ }) + '\n'));
1080
+ }
1081
+ catch (error) {
1082
+ controller.enqueue(encoder.encode(JSON.stringify({
1083
+ type: 'error',
1084
+ ok: false,
1085
+ error: {
1086
+ code: 'UNKNOWN',
1087
+ message: error instanceof Error ? error.message : String(error),
1088
+ },
1089
+ }) + '\n'));
1090
+ }
1091
+ controller.close();
1092
+ },
1093
+ });
1094
+ return new Response(stream, {
1095
+ status: 200,
1096
+ headers: { 'content-type': 'application/x-ndjson' },
1097
+ });
1244
1098
  }
1245
- catch (error) {
1246
- const duration = `${Math.round(performance.now() - start)}ms`;
1247
- if (error instanceof ValidationError)
1248
- return jsonResponse({
1249
- ok: false,
1250
- error: { code: 'VALIDATION_ERROR', message: error.message },
1251
- meta: { command: path, duration },
1252
- }, 400);
1099
+ if (!result.ok) {
1100
+ const cta = formatCtaBlock(options.name ?? path, result.cta);
1253
1101
  return jsonResponse({
1254
1102
  ok: false,
1255
1103
  error: {
1256
- code: error instanceof IncurError ? error.code : 'UNKNOWN',
1257
- message: error instanceof Error ? error.message : String(error),
1104
+ code: result.error.code,
1105
+ message: result.error.message,
1106
+ ...(result.error.retryable !== undefined
1107
+ ? { retryable: result.error.retryable }
1108
+ : undefined),
1258
1109
  },
1259
- meta: { command: path, duration },
1260
- }, 500);
1110
+ meta: {
1111
+ command: path,
1112
+ duration,
1113
+ ...(cta ? { cta } : undefined),
1114
+ },
1115
+ }, result.error.code === 'VALIDATION_ERROR' ? 400 : 500);
1261
1116
  }
1262
- return response;
1117
+ const cta = formatCtaBlock(options.name ?? path, result.cta);
1118
+ return jsonResponse({
1119
+ ok: true,
1120
+ data: result.data,
1121
+ meta: {
1122
+ command: path,
1123
+ duration,
1124
+ ...(cta ? { cta } : undefined),
1125
+ },
1126
+ }, 200);
1263
1127
  }
1264
1128
  /** @internal Formats a validation error for TTY with usage hint. */
1265
1129
  function formatHumanValidationError(cli, path, command, error, envSource) {
@@ -1425,6 +1289,23 @@ function collectHelpCommands(commands) {
1425
1289
  }
1426
1290
  return result.sort((a, b) => a.name.localeCompare(b.name));
1427
1291
  }
1292
+ /** @internal Formats group-level help for a built-in command (e.g. `cli skills`). */
1293
+ function formatBuiltinHelp(cli, builtin) {
1294
+ return Help.formatRoot(`${cli} ${builtin.name}`, {
1295
+ description: builtin.description,
1296
+ commands: builtin.subcommands?.map((s) => ({ name: s.name, description: s.description })),
1297
+ });
1298
+ }
1299
+ /** @internal Formats subcommand-level help for a built-in command (e.g. `cli skills add --help`). */
1300
+ function formatBuiltinSubcommandHelp(cli, builtin, subName) {
1301
+ const sub = builtin.subcommands?.find((s) => s.name === subName);
1302
+ return Help.formatCommand(`${cli} ${builtin.name} ${subName}`, {
1303
+ alias: sub?.alias,
1304
+ description: sub?.description,
1305
+ hideGlobalOptions: true,
1306
+ options: sub?.options,
1307
+ });
1308
+ }
1428
1309
  /** @internal Formats help text for a fetch gateway command. */
1429
1310
  function formatFetchHelp(name, description) {
1430
1311
  const lines = [];
@@ -1477,8 +1358,9 @@ function formatHumanError(error) {
1477
1358
  /** @internal Formats a CTA block for human-readable TTY output. */
1478
1359
  function formatHumanCta(cta) {
1479
1360
  const lines = ['', cta.description];
1361
+ const maxLen = Math.max(...cta.commands.map((c) => c.command.length));
1480
1362
  for (const c of cta.commands) {
1481
- const desc = c.description ? ` # ${c.description}` : '';
1363
+ const desc = c.description ? ` ${''.padEnd(maxLen - c.command.length)}# ${c.description}` : '';
1482
1364
  lines.push(` ${c.command}${desc}`);
1483
1365
  }
1484
1366
  return lines.join('\n');
@@ -1490,13 +1372,6 @@ function hasRequiredArgs(args) {
1490
1372
  function isSentinel(value) {
1491
1373
  return typeof value === 'object' && value !== null && sentinel in value;
1492
1374
  }
1493
- /** @internal Type guard for async generators returned by streaming `run` handlers. */
1494
- function isAsyncGenerator(value) {
1495
- return (typeof value === 'object' &&
1496
- value !== null &&
1497
- Symbol.asyncIterator in value &&
1498
- typeof value.next === 'function');
1499
- }
1500
1375
  /** @internal Handles streaming output from an async generator `run` handler. */
1501
1376
  async function handleStreaming(generator, ctx) {
1502
1377
  // Incremental: no explicit format (default toon), or explicit jsonl