argsbarg 3.4.2 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/biome.json +29 -6
  3. package/bun.lock +22 -0
  4. package/docs/install.md +1 -1
  5. package/docs/mcp.md +45 -1
  6. package/index.d.ts +50 -50
  7. package/justfile +27 -6
  8. package/package.json +4 -2
  9. package/scripts/release.ts +26 -9
  10. package/src/builtins/builtins.test.ts +9 -4
  11. package/src/builtins/completion-bash.ts +74 -50
  12. package/src/builtins/completion-fish.ts +3 -8
  13. package/src/builtins/completion-group.ts +1 -1
  14. package/src/builtins/completion-zsh.ts +80 -42
  15. package/src/builtins/dispatch.ts +20 -16
  16. package/src/builtins/export.ts +19 -10
  17. package/src/builtins/index.ts +9 -4
  18. package/src/builtins/install.ts +10 -10
  19. package/src/builtins/mcp.ts +1 -1
  20. package/src/builtins/presentation.ts +8 -8
  21. package/src/builtins/scopes.ts +1 -1
  22. package/src/builtins/version.ts +1 -1
  23. package/src/completion.ts +4 -4
  24. package/src/docs/api-guide.test.ts +2 -2
  25. package/src/docs/api-guide.ts +2 -2
  26. package/src/docs/builtin.ts +27 -8
  27. package/src/docs/docs.test.ts +18 -11
  28. package/src/docs/mcp-guide.ts +105 -24
  29. package/src/docs/resolve.ts +10 -3
  30. package/src/docs/save.ts +11 -3
  31. package/src/headless.test.ts +8 -16
  32. package/src/help.ts +73 -43
  33. package/src/hidden-mcpb.test.ts +7 -6
  34. package/src/hidden.ts +2 -2
  35. package/src/index.test.ts +113 -89
  36. package/src/index.ts +24 -24
  37. package/src/install/binary.ts +12 -5
  38. package/src/install/completions.ts +7 -3
  39. package/src/install/detect-installed.ts +29 -4
  40. package/src/install/gh-release-update.ts +31 -23
  41. package/src/install/index.ts +69 -19
  42. package/src/install/install.test.ts +31 -8
  43. package/src/install/mcp-codex.test.ts +57 -0
  44. package/src/install/mcp-codex.ts +125 -0
  45. package/src/install/mcp-config.ts +12 -5
  46. package/src/install/mcp-opencode.test.ts +98 -0
  47. package/src/install/mcp-opencode.ts +149 -0
  48. package/src/install/paths.ts +29 -3
  49. package/src/install/plan.ts +73 -6
  50. package/src/install/shell.ts +1 -4
  51. package/src/install/status.ts +12 -6
  52. package/src/install/uninstall.ts +38 -4
  53. package/src/install/update.test.ts +2 -2
  54. package/src/install/update.ts +3 -1
  55. package/src/invoke.ts +12 -9
  56. package/src/mcp/bundle.ts +36 -8
  57. package/src/mcp/env.ts +7 -13
  58. package/src/mcp/server.ts +12 -6
  59. package/src/mcp/tools.ts +20 -4
  60. package/src/mcp.ts +3 -3
  61. package/src/parse.ts +96 -24
  62. package/src/runtime.ts +22 -12
  63. package/src/schema.ts +11 -5
  64. package/src/skill/generate.ts +4 -4
  65. package/src/skill/install.ts +6 -2
  66. package/src/validate.ts +21 -16
@@ -1,16 +1,18 @@
1
1
  import { existsSync, rmSync } from "node:fs";
2
- import { CliProgram } from "../types.ts";
2
+ import type { CliProgram } from "../types.ts";
3
3
  import { uninstallBinary } from "./binary.ts";
4
4
  import { uninstallCompletions } from "./completions.ts";
5
5
  import { detectInstalledArtifacts } from "./detect-installed.ts";
6
+ import { removeCodexMcpConfig } from "./mcp-codex.ts";
6
7
  import { removeMcpConfig } from "./mcp-config.ts";
7
- import { InstallPaths } from "./paths.ts";
8
+ import { detectOpenCodeMcpConfigPath, removeOpenCodeMcpConfig } from "./mcp-opencode.ts";
9
+ import { type InstallPaths, userHome } from "./paths.ts";
8
10
  import {
11
+ type InstallOpts,
9
12
  wantsInstallBin,
10
13
  wantsInstallCompletions,
11
14
  wantsInstallMcp,
12
15
  wantsInstallSkill,
13
- type InstallOpts,
14
16
  } from "./plan.ts";
15
17
 
16
18
  export interface UninstallAction {
@@ -111,6 +113,38 @@ export function buildUninstallPlan(
111
113
  },
112
114
  });
113
115
  }
116
+ if (detected.opencodeMcp) {
117
+ const openCodePath =
118
+ detectOpenCodeMcpConfigPath(userHome(), paths.mcpName) ?? paths.opencodeMcpPath;
119
+ actions.push({
120
+ summary: `opencode mcp: ${openCodePath}`,
121
+ message: `Removing MCP server "${paths.mcpName}" from ${openCodePath}`,
122
+ run: () => {
123
+ removeOpenCodeMcpConfig(openCodePath, paths.mcpName, dry);
124
+ return [openCodePath];
125
+ },
126
+ });
127
+ }
128
+ if (detected.codexMcp) {
129
+ actions.push({
130
+ summary: `codex mcp: ${paths.codexConfigPath}`,
131
+ message: `Removing MCP server "${paths.mcpName}" from Codex via codex mcp remove`,
132
+ run: () => {
133
+ removeCodexMcpConfig(userHome(), paths.mcpName, dry);
134
+ return [paths.codexConfigPath];
135
+ },
136
+ });
137
+ }
138
+ if (detected.chatGptMcp) {
139
+ actions.push({
140
+ summary: `chatgpt desktop mcp: ${paths.chatGptMcpPath}`,
141
+ message: `Removing MCP server "${paths.mcpName}" from ${paths.chatGptMcpPath}`,
142
+ run: () => {
143
+ removeMcpConfig(paths.chatGptMcpPath, paths.mcpName, dry);
144
+ return [paths.chatGptMcpPath];
145
+ },
146
+ });
147
+ }
114
148
  }
115
149
 
116
150
  return actions;
@@ -120,5 +154,5 @@ export function buildUninstallPlan(
120
154
  export function uninstallSkillDir(dir: string, dry: boolean): string[] {
121
155
  if (!existsSync(dir)) return [];
122
156
  if (!dry) rmSync(dir, { recursive: true, force: true });
123
- return [dir + "/"];
157
+ return [`${dir}/`];
124
158
  }
@@ -1,7 +1,7 @@
1
- import { expect, test, beforeEach, afterEach } from "bun:test";
1
+ import { afterEach, beforeEach, expect, test } from "bun:test";
2
2
  import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { join } from "node:path";
4
3
  import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
5
  import { cliPresentationRoot } from "../builtins/presentation.ts";
6
6
  import { cliInvoke } from "../index.ts";
7
7
  import type { CliProgram, CliUpdateArtifact } from "../types.ts";
@@ -7,7 +7,9 @@ import { installErr } from "./status.ts";
7
7
  export async function cliUpdate(root: CliProgram): Promise<never> {
8
8
  const hook = root.install?.updateGetLatest;
9
9
  if (!hook) {
10
- installErr("install --update is not configured. Set install.updateGetLatest on the program root.");
10
+ installErr(
11
+ "install --update is not configured. Set install.updateGetLatest on the program root.",
12
+ );
11
13
  process.exit(1);
12
14
  }
13
15
 
package/src/invoke.ts CHANGED
@@ -4,12 +4,12 @@ It parses argv against the user schema, captures stdout/stderr, and patches
4
4
  process.exit so MCP tool calls can run handlers repeatedly.
5
5
  */
6
6
 
7
- import { CliContext } from "./context.ts";
7
+ import { format } from "node:util";
8
8
  import { builtinInterceptRoot, dispatchBuiltin } from "./builtins/dispatch.ts";
9
9
  import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
10
- import { parse, postParseValidate, ParseKind } from "./parse.ts";
10
+ import { CliContext } from "./context.ts";
11
+ import { ParseKind, parse, postParseValidate } from "./parse.ts";
11
12
  import { type CliNode, type CliProgram, type CliRouter, isCliLeaf, isCliRouter } from "./types.ts";
12
- import { format } from "node:util";
13
13
 
14
14
  /** Outcome of a non-exiting CLI invocation. */
15
15
  export type CliInvokeKind = "ok" | "help" | "error";
@@ -159,21 +159,24 @@ export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliIn
159
159
  }) as typeof process.stderr.write;
160
160
 
161
161
  console.log = (...args: unknown[]) => {
162
- stdout += format(...args) + "\n";
162
+ stdout += `${format(...args)}\n`;
163
163
  };
164
164
  console.info = (...args: unknown[]) => {
165
- stdout += format(...args) + "\n";
165
+ stdout += `${format(...args)}\n`;
166
166
  };
167
167
  console.warn = (...args: unknown[]) => {
168
- stderr += format(...args) + "\n";
168
+ stderr += `${format(...args)}\n`;
169
169
  };
170
170
  console.error = (...args: unknown[]) => {
171
- stderr += format(...args) + "\n";
171
+ stderr += `${format(...args)}\n`;
172
172
  };
173
173
 
174
174
  try {
175
175
  if (pr.kind === ParseKind.Ok) {
176
- await dispatchBuiltin(root, pr, { isLeafCompletionIntercept, parseRoot: completionParseRoot });
176
+ await dispatchBuiltin(root, pr, {
177
+ isLeafCompletionIntercept,
178
+ parseRoot: completionParseRoot,
179
+ });
177
180
  }
178
181
 
179
182
  await Promise.resolve(handler(ctx));
@@ -191,7 +194,7 @@ export async function cliInvoke(root: CliProgram, argv: string[]): Promise<CliIn
191
194
  kind: "error",
192
195
  exitCode: 1,
193
196
  stdout,
194
- stderr: err.message + "\n",
197
+ stderr: `${err.message}\n`,
195
198
  errorMsg: err.message,
196
199
  };
197
200
  }
package/src/mcp/bundle.ts CHANGED
@@ -3,11 +3,19 @@ Packs a CLI program into an MCP Bundle (`.mcpb`) for Claude Desktop.
3
3
  Expects `dist/<program.key>` as the compiled binary input.
4
4
  */
5
5
 
6
- import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
+ import {
7
+ cpSync,
8
+ existsSync,
9
+ mkdirSync,
10
+ mkdtempSync,
11
+ readFileSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from "node:fs";
7
15
  import { tmpdir } from "node:os";
8
16
  import { basename, join, resolve } from "node:path";
9
- import { collectMcpTools, mcpServerId } from "./tools.ts";
10
17
  import type { CliMcpBundleConfig, CliProgram } from "../types.ts";
18
+ import { collectMcpTools, mcpServerId } from "./tools.ts";
11
19
 
12
20
  const MANIFEST_VERSION = "0.3";
13
21
  const DIST_DIR = "dist";
@@ -51,7 +59,11 @@ function buildUserConfig(program: CliProgram): Record<string, unknown> | undefin
51
59
  }
52
60
  const out: Record<string, unknown> = {};
53
61
  for (const name of envVars) {
54
- const key = name.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "") || "env_var";
62
+ const key =
63
+ name
64
+ .toLowerCase()
65
+ .replace(/[^a-z0-9]+/g, "_")
66
+ .replace(/^_|_$/g, "") || "env_var";
55
67
  out[key] = {
56
68
  type: "string",
57
69
  title: name,
@@ -64,12 +76,19 @@ function buildUserConfig(program: CliProgram): Record<string, unknown> | undefin
64
76
  }
65
77
 
66
78
  /** Default author when `mcpServer.bundle.author` is unset. */
67
- function defaultAuthor(bundle?: CliMcpBundleConfig): { name: string; email?: string; url?: string } {
79
+ function defaultAuthor(bundle?: CliMcpBundleConfig): {
80
+ name: string;
81
+ email?: string;
82
+ url?: string;
83
+ } {
68
84
  return bundle?.author ?? { name: "Unknown" };
69
85
  }
70
86
 
71
87
  /** Generates MCPB `manifest.json` object from program schema and MCP tools. */
72
- export function generateMcpManifest(program: CliProgram, binaryName: string): Record<string, unknown> {
88
+ export function generateMcpManifest(
89
+ program: CliProgram,
90
+ binaryName: string,
91
+ ): Record<string, unknown> {
73
92
  const bundle = program.mcpServer?.bundle;
74
93
  const tools = collectMcpTools(program).map((t) => ({
75
94
  name: t.name,
@@ -119,7 +138,11 @@ export function generateMcpManifest(program: CliProgram, binaryName: string): Re
119
138
  function crc32(data: Buffer): number {
120
139
  let crc = 0xffffffff;
121
140
  for (let i = 0; i < data.length; i++) {
122
- crc ^= data[i]!;
141
+ const byte = data[i];
142
+ if (byte === undefined) {
143
+ continue;
144
+ }
145
+ crc ^= byte;
123
146
  for (let j = 0; j < 8; j++) {
124
147
  crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
125
148
  }
@@ -208,7 +231,9 @@ export function packMcpBundle(program: CliProgram, opts: PackMcpBundleOpts = {})
208
231
  const binaryName = basename(binaryPath);
209
232
 
210
233
  if (!existsSync(binaryPath)) {
211
- throw new Error(`Binary not found: ${binaryPath}. Build with compile first (expected dist/${program.key}).`);
234
+ throw new Error(
235
+ `Binary not found: ${binaryPath}. Build with compile first (expected dist/${program.key}).`,
236
+ );
212
237
  }
213
238
 
214
239
  const staging = mkdtempSync(join(tmpdir(), "mcpb-"));
@@ -218,7 +243,10 @@ export function packMcpBundle(program: CliProgram, opts: PackMcpBundleOpts = {})
218
243
 
219
244
  const manifest = generateMcpManifest(program, binaryName);
220
245
  const files: { name: string; data: Buffer }[] = [
221
- { name: "manifest.json", data: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8") },
246
+ {
247
+ name: "manifest.json",
248
+ data: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"),
249
+ },
222
250
  { name: binaryName, data: readFileSync(stagedBinary) },
223
251
  ];
224
252
 
package/src/mcp/env.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  This module bootstraps process.env for MCP servers from login shell and .env files.
3
3
  */
4
4
 
5
- import { readFileSync } from "node:fs";
6
5
  import { spawnSync } from "node:child_process";
6
+ import { readFileSync } from "node:fs";
7
7
 
8
8
  /** Parses `env` stdout from a login shell into a key/value map. */
9
9
  export function captureShellEnv(shell: string): Record<string, string> {
@@ -42,9 +42,7 @@ export function applyShellEnv(env: Record<string, string>): void {
42
42
 
43
43
  /** Loads a .env file into process.env (always overwrites). Warns on stderr if missing. */
44
44
  export function loadEnvFile(envFile: string): void {
45
- const resolved = envFile.startsWith("~")
46
- ? envFile.replace("~", process.env.HOME ?? "")
47
- : envFile;
45
+ const resolved = envFile.startsWith("~") ? envFile.replace("~", process.env.HOME ?? "") : envFile;
48
46
  let text: string;
49
47
  try {
50
48
  text = readFileSync(resolved, "utf8");
@@ -63,10 +61,7 @@ export function loadEnvFile(envFile: string): void {
63
61
  }
64
62
  const key = trimmed.slice(0, eq).trim();
65
63
  let val = trimmed.slice(eq + 1).trim();
66
- if (
67
- (val.startsWith('"') && val.endsWith('"')) ||
68
- (val.startsWith("'") && val.endsWith("'"))
69
- ) {
64
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
70
65
  val = val.slice(1, -1);
71
66
  }
72
67
  if (key) {
@@ -76,10 +71,7 @@ export function loadEnvFile(envFile: string): void {
76
71
  }
77
72
 
78
73
  /** Applies mcpServer shellEnv and envFile bootstrap in order. */
79
- export function bootstrapMcpEnv(config: {
80
- shellEnv?: boolean | string;
81
- envFile?: string;
82
- }): void {
74
+ export function bootstrapMcpEnv(config: { shellEnv?: boolean | string; envFile?: string }): void {
83
75
  const shellEnvCfg = config.shellEnv;
84
76
  if (shellEnvCfg) {
85
77
  const shell =
@@ -88,7 +80,9 @@ export function bootstrapMcpEnv(config: {
88
80
  : (process.env.SHELL ?? (process.platform === "darwin" ? "/bin/zsh" : "/bin/bash"));
89
81
  const captured = captureShellEnv(shell);
90
82
  if (Object.keys(captured).length === 0) {
91
- process.stderr.write(`[argsbarg] shellEnv: failed to capture shell environment from ${shell}\n`);
83
+ process.stderr.write(
84
+ `[argsbarg] shellEnv: failed to capture shell environment from ${shell}\n`,
85
+ );
92
86
  } else {
93
87
  applyShellEnv(captured);
94
88
  }
package/src/mcp/server.ts CHANGED
@@ -4,7 +4,7 @@ resources, and ping. Responses are newline-delimited JSON on stdout only.
4
4
  */
5
5
 
6
6
  import { cliInvoke } from "../invoke.ts";
7
- import { CliProgram } from "../types.ts";
7
+ import type { CliProgram } from "../types.ts";
8
8
  import { buildToolCallSuccess } from "./result.ts";
9
9
  import {
10
10
  allMcpResources,
@@ -25,7 +25,7 @@ interface JsonRpcRequest {
25
25
 
26
26
  /** Writes a JSON-RPC response line to stdout. */
27
27
  function writeResponse(msg: Record<string, unknown>): void {
28
- process.stdout.write(JSON.stringify(msg) + "\n");
28
+ process.stdout.write(`${JSON.stringify(msg)}\n`);
29
29
  }
30
30
 
31
31
  /** Writes a JSON-RPC error response. */
@@ -108,7 +108,10 @@ async function handleRequestLine(root: CliProgram, line: string): Promise<void>
108
108
  return;
109
109
  }
110
110
  const rawArgs = params.arguments;
111
- if (rawArgs !== undefined && (typeof rawArgs !== "object" || rawArgs === null || Array.isArray(rawArgs))) {
111
+ if (
112
+ rawArgs !== undefined &&
113
+ (typeof rawArgs !== "object" || rawArgs === null || Array.isArray(rawArgs))
114
+ ) {
112
115
  writeError(id, -32602, "Invalid params: arguments must be an object");
113
116
  return;
114
117
  }
@@ -151,7 +154,8 @@ async function handleRequestLine(root: CliProgram, line: string): Promise<void>
151
154
  });
152
155
  return;
153
156
  }
154
- const errText = invokeResult.stderr.trim() || invokeResult.errorMsg || `Exit code ${invokeResult.exitCode}`;
157
+ const errText =
158
+ invokeResult.stderr.trim() || invokeResult.errorMsg || `Exit code ${invokeResult.exitCode}`;
155
159
  writeResponse({
156
160
  jsonrpc: "2.0",
157
161
  id,
@@ -222,14 +226,16 @@ export async function mcpServeStdioLoop(root: CliProgram): Promise<void> {
222
226
  let buffer = "";
223
227
  for await (const chunk of Bun.stdin.stream()) {
224
228
  buffer += new TextDecoder().decode(chunk);
225
- let nl: number;
226
- while ((nl = buffer.indexOf("\n")) !== -1) {
229
+ let nl = buffer.indexOf("\n");
230
+ while (nl !== -1) {
227
231
  const line = buffer.slice(0, nl).trim();
228
232
  buffer = buffer.slice(nl + 1);
229
233
  if (line.length === 0) {
234
+ nl = buffer.indexOf("\n");
230
235
  continue;
231
236
  }
232
237
  await handleRequestLine(root, line);
238
+ nl = buffer.indexOf("\n");
233
239
  }
234
240
  }
235
241
  const trailing = buffer.trim();
package/src/mcp/tools.ts CHANGED
@@ -4,10 +4,19 @@ flat JSON tool arguments into argv for cliInvoke.
4
4
  */
5
5
 
6
6
  import { cliResolveNotes } from "../help.ts";
7
- import { collectOptionDefs } from "../parse.ts";
8
7
  import { visibleOptions } from "../hidden.ts";
8
+ import { collectOptionDefs } from "../parse.ts";
9
9
  import { cliSchemaJson } from "../schema.ts";
10
- import { CliProgram, CliLeaf, CliNode, CliOption, CliOptionKind, CliPositional, isCliLeaf, isCliRouter, leafOutputSchema } from "../types.ts";
10
+ import {
11
+ type CliLeaf,
12
+ type CliNode,
13
+ type CliOption,
14
+ CliOptionKind,
15
+ type CliPositional,
16
+ type CliProgram,
17
+ isCliLeaf,
18
+ leafOutputSchema,
19
+ } from "../types.ts";
11
20
 
12
21
  /** Default URI pattern for the CLI schema MCP resource (`<mcpId>://schema`). */
13
22
  export function defaultMcpSchemaUri(mcpId: string): string {
@@ -80,7 +89,11 @@ function positionalProperty(p: CliPositional): Record<string, unknown> {
80
89
  }
81
90
 
82
91
  /** Builds inputSchema for a leaf command. */
83
- function buildInputSchema(root: CliProgram, path: string[], leaf: CliLeaf): Record<string, unknown> {
92
+ function buildInputSchema(
93
+ root: CliProgram,
94
+ path: string[],
95
+ leaf: CliLeaf,
96
+ ): Record<string, unknown> {
84
97
  const properties: Record<string, unknown> = {};
85
98
  const required: string[] = [];
86
99
 
@@ -252,7 +265,10 @@ export function mcpToolCallToArgv(
252
265
  items = raw.map(String);
253
266
  } else if (typeof raw === "string") {
254
267
  items = raw.includes(",")
255
- ? raw.split(",").map((s) => s.trim()).filter(Boolean)
268
+ ? raw
269
+ .split(",")
270
+ .map((s) => s.trim())
271
+ .filter(Boolean)
256
272
  : raw.trim()
257
273
  ? [raw.trim()]
258
274
  : [];
package/src/mcp.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  This module starts the ArgsBarg MCP stdio server for opt-in program roots.
3
3
  */
4
4
 
5
- import { mcpServeStdioLoop } from "./mcp/server.ts";
6
5
  import { bootstrapMcpEnv } from "./mcp/env.ts";
7
- import { CliProgram } from "./types.ts";
6
+ import { mcpServeStdioLoop } from "./mcp/server.ts";
7
+ import type { CliProgram } from "./types.ts";
8
8
 
9
9
  /**
10
10
  * Runs the MCP JSON-RPC server on stdin/stdout until stdin closes, then exits.
@@ -19,7 +19,7 @@ export async function cliMcpServeStdio(root: CliProgram): Promise<never> {
19
19
  process.exit(0);
20
20
  } catch (err) {
21
21
  if (err instanceof Error) {
22
- process.stderr.write(err.message + "\n");
22
+ process.stderr.write(`${err.message}\n`);
23
23
  } else {
24
24
  process.stderr.write("MCP server error.\n");
25
25
  }
package/src/parse.ts CHANGED
@@ -7,12 +7,11 @@ It keeps handler dispatch and help on one parser so the CLI behavior stays consi
7
7
  across every entry path.
8
8
  */
9
9
 
10
- import { CliContext } from "./context.ts";
11
10
  import {
12
- type CliLeaf,
13
- CliNode,
14
11
  CliFallbackMode,
15
- CliOption,
12
+ type CliLeaf,
13
+ type CliNode,
14
+ type CliOption,
16
15
  CliOptionKind,
17
16
  isCliLeaf,
18
17
  isCliRouter,
@@ -191,17 +190,30 @@ function consumeOptions(
191
190
 
192
191
  if (tok === "--") {
193
192
  idx += 1;
194
- return { report: { err: null, stoppedOnUnknown: false, sawDoubleDash: true }, nextIndex: idx };
193
+ return {
194
+ report: { err: null, stoppedOnUnknown: false, sawDoubleDash: true },
195
+ nextIndex: idx,
196
+ };
195
197
  }
196
198
 
197
199
  if (tok.startsWith("--")) {
198
200
  const err = consumeLong(tok);
199
- if (err === "") return { report: { err: null, stoppedOnUnknown: true, sawDoubleDash: false }, nextIndex: idx };
200
- if (err) return { report: { err, stoppedOnUnknown: false, sawDoubleDash: false }, nextIndex: idx };
201
+ if (err === "")
202
+ return {
203
+ report: { err: null, stoppedOnUnknown: true, sawDoubleDash: false },
204
+ nextIndex: idx,
205
+ };
206
+ if (err)
207
+ return { report: { err, stoppedOnUnknown: false, sawDoubleDash: false }, nextIndex: idx };
201
208
  } else {
202
209
  const err = consumeShort(tok);
203
- if (err === "") return { report: { err: null, stoppedOnUnknown: true, sawDoubleDash: false }, nextIndex: idx };
204
- if (err) return { report: { err, stoppedOnUnknown: false, sawDoubleDash: false }, nextIndex: idx };
210
+ if (err === "")
211
+ return {
212
+ report: { err: null, stoppedOnUnknown: true, sawDoubleDash: false },
213
+ nextIndex: idx,
214
+ };
215
+ if (err)
216
+ return { report: { err, stoppedOnUnknown: false, sawDoubleDash: false }, nextIndex: idx };
205
217
  }
206
218
  }
207
219
 
@@ -212,7 +224,7 @@ function consumeOptions(
212
224
 
213
225
  /** Merges option defs from the program root along the routed command path. */
214
226
  export function collectOptionDefs(root: CliNode, path: string[]): CliOption[] {
215
- let defs = [...(root.options ?? [])];
227
+ const defs = [...(root.options ?? [])];
216
228
  let node: CliNode = root;
217
229
 
218
230
  for (const seg of path) {
@@ -343,7 +355,16 @@ function finishLeaf(
343
355
  }
344
356
  }
345
357
 
346
- return { kind: ParseKind.Ok, path, opts, args, helpExplicit: false, helpPath: [], errorMsg: "", errorHelpPath: [] };
358
+ return {
359
+ kind: ParseKind.Ok,
360
+ path,
361
+ opts,
362
+ args,
363
+ helpExplicit: false,
364
+ helpPath: [],
365
+ errorMsg: "",
366
+ errorHelpPath: [],
367
+ };
347
368
  }
348
369
 
349
370
  // ── Main Parser ───────────────────────────────────────────────────────────────
@@ -414,7 +435,16 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
414
435
  cmdName = root.fallbackCommand;
415
436
  node = findChild(root.commands, cmdName);
416
437
  if (!node) {
417
- return { kind: ParseKind.Error, path: [], opts: {}, args: [], helpExplicit: false, helpPath: [], errorMsg: `Unknown command: ${cmdName}`, errorHelpPath: path };
438
+ return {
439
+ kind: ParseKind.Error,
440
+ path: [],
441
+ opts: {},
442
+ args: [],
443
+ helpExplicit: false,
444
+ helpPath: [],
445
+ errorMsg: `Unknown command: ${cmdName}`,
446
+ errorHelpPath: path,
447
+ };
418
448
  }
419
449
  } else {
420
450
  return helpResult([], false);
@@ -428,16 +458,26 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
428
458
  i += 1;
429
459
  node = childPick;
430
460
  } else {
461
+ const fallbackCommand = root.fallbackCommand;
431
462
  const canRouteUnknown =
432
- root.fallbackCommand !== undefined &&
463
+ fallbackCommand !== undefined &&
433
464
  ((root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.MissingOrUnknown ||
434
465
  (root.fallbackMode ?? CliFallbackMode.MissingOnly) === CliFallbackMode.UnknownOnly);
435
466
 
436
467
  if (canRouteUnknown) {
437
- cmdName = root.fallbackCommand!;
468
+ cmdName = fallbackCommand;
438
469
  node = findChild(root.commands, cmdName);
439
470
  if (!node) {
440
- return { kind: ParseKind.Error, path: [], opts: {}, args: [], helpExplicit: false, helpPath: [], errorMsg: `Unknown command: ${cmdName}`, errorHelpPath: path };
471
+ return {
472
+ kind: ParseKind.Error,
473
+ path: [],
474
+ opts: {},
475
+ args: [],
476
+ helpExplicit: false,
477
+ helpPath: [],
478
+ errorMsg: `Unknown command: ${cmdName}`,
479
+ errorHelpPath: path,
480
+ };
441
481
  }
442
482
  } else {
443
483
  cmdName = peek;
@@ -451,7 +491,9 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
451
491
  args: [],
452
492
  helpExplicit: false,
453
493
  helpPath: [],
454
- errorMsg: forcePositionals ? `Expected subcommand but got positional: ${cmdName}` : `Unknown command: ${cmdName}`,
494
+ errorMsg: forcePositionals
495
+ ? `Expected subcommand but got positional: ${cmdName}`
496
+ : `Unknown command: ${cmdName}`,
455
497
  errorHelpPath: path,
456
498
  };
457
499
  }
@@ -460,7 +502,19 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
460
502
  }
461
503
 
462
504
  path.push(cmdName);
463
- let current = node!;
505
+ if (!node) {
506
+ return {
507
+ kind: ParseKind.Error,
508
+ path,
509
+ opts: {},
510
+ args: [],
511
+ helpExplicit: false,
512
+ helpPath: [],
513
+ errorMsg: `Unknown command: ${cmdName}`,
514
+ errorHelpPath: path,
515
+ };
516
+ }
517
+ let current = node;
464
518
 
465
519
  // Walk the command tree
466
520
  while (true) {
@@ -508,7 +562,15 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
508
562
  if (!isCliLeaf(current)) {
509
563
  return helpResult(path, false);
510
564
  }
511
- return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
565
+ return finishLeaf(
566
+ current,
567
+ i,
568
+ argv,
569
+ path,
570
+ opts,
571
+ collectOptionDefs(root, path),
572
+ forcePositionals,
573
+ );
512
574
  }
513
575
 
514
576
  const tok = argv[i];
@@ -542,10 +604,10 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
542
604
  fb !== undefined &&
543
605
  (fm === CliFallbackMode.MissingOrUnknown || fm === CliFallbackMode.UnknownOnly);
544
606
 
545
- if (canRouteUnknown) {
546
- const fbNode = findChild(current.commands, fb!);
607
+ if (canRouteUnknown && fb !== undefined) {
608
+ const fbNode = findChild(current.commands, fb);
547
609
  if (fbNode) {
548
- path.push(fb!);
610
+ path.push(fb);
549
611
  current = fbNode;
550
612
  continue;
551
613
  }
@@ -558,7 +620,9 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
558
620
  args: [],
559
621
  helpExplicit: false,
560
622
  helpPath: [],
561
- errorMsg: forcePositionals ? `Expected subcommand but got positional: ${tok}` : `Unknown subcommand: ${tok}`,
623
+ errorMsg: forcePositionals
624
+ ? `Expected subcommand but got positional: ${tok}`
625
+ : `Unknown subcommand: ${tok}`,
562
626
  errorHelpPath: path,
563
627
  };
564
628
  }
@@ -566,7 +630,15 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
566
630
  if (!isCliLeaf(current)) {
567
631
  return helpResult(path, false);
568
632
  }
569
- return finishLeaf(current, i, argv, path, opts, collectOptionDefs(root, path), forcePositionals);
633
+ return finishLeaf(
634
+ current,
635
+ i,
636
+ argv,
637
+ path,
638
+ opts,
639
+ collectOptionDefs(root, path),
640
+ forcePositionals,
641
+ );
570
642
  }
571
643
  }
572
644
 
@@ -578,7 +650,7 @@ export function parse(root: CliNode, argv: string[]): ParseResult {
578
650
  export function postParseValidate(root: CliNode, pr: ParseResult): ParseResult {
579
651
  if (pr.kind !== ParseKind.Ok) return pr;
580
652
 
581
- let defs = [...(root.options ?? [])];
653
+ const defs = [...(root.options ?? [])];
582
654
  let node: CliNode = root;
583
655
 
584
656
  for (const seg of pr.path) {