argsbarg 5.0.2 → 5.1.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 (46) hide show
  1. package/CHANGELOG.md +16 -2
  2. package/docs/cli-program.md +4 -4
  3. package/docs/config-schema.md +9 -9
  4. package/docs/configure.md +4 -0
  5. package/docs/mcp.md +1 -1
  6. package/examples/full-example/README.md +2 -2
  7. package/examples/full-example/src/program.ts +0 -3
  8. package/index.d.ts +12 -7
  9. package/package.json +1 -1
  10. package/src/builtins/builtins.test.ts +9 -14
  11. package/src/builtins/config.test.ts +4 -4
  12. package/src/builtins/config.ts +19 -22
  13. package/src/builtins/configure.ts +35 -4
  14. package/src/builtins/dispatch.ts +10 -15
  15. package/src/builtins/registry.ts +0 -2
  16. package/src/capabilities.ts +6 -12
  17. package/src/cli-tool/cli-smoke.test.ts +0 -3
  18. package/src/cli-tool/create.test.ts +0 -3
  19. package/src/cli-tool/full-example-capabilities.test.ts +0 -2
  20. package/src/config/context.test.ts +0 -1
  21. package/src/config/entry.ts +2 -2
  22. package/src/config/file.test.ts +0 -5
  23. package/src/config/resolve.test.ts +0 -7
  24. package/src/config/resolve.ts +2 -2
  25. package/src/config/validate.test.ts +0 -6
  26. package/src/configure/configure.test.ts +0 -11
  27. package/src/docs/api-guide.test.ts +0 -4
  28. package/src/docs/docs.test.ts +1 -21
  29. package/src/docs/mcp-resources.test.ts +0 -6
  30. package/src/formats.test.ts +0 -5
  31. package/src/headless.test.ts +0 -4
  32. package/src/headless.ts +5 -4
  33. package/src/hidden-mcpb.test.ts +0 -12
  34. package/src/install/binary-placement.test.ts +0 -5
  35. package/src/install/gh-release-update.test.ts +0 -5
  36. package/src/install/install-validate.test.ts +0 -5
  37. package/src/install/mcp-opencode.test.ts +0 -4
  38. package/src/install/targets.test.ts +0 -10
  39. package/src/invoke.test.ts +0 -7
  40. package/src/mcp/claude.test.ts +0 -4
  41. package/src/mcp/env.test.ts +0 -5
  42. package/src/mcp/zip.test.ts +0 -1
  43. package/src/mcp.integration.test.ts +0 -22
  44. package/src/parse.test.ts +0 -28
  45. package/src/schema.ts +1 -1
  46. package/src/types.ts +2 -2
@@ -59,13 +59,11 @@ describe("full-example template", () => {
59
59
  expect(programSource).toContain("appConfig:");
60
60
  });
61
61
 
62
- /** Status command defines outputSchema. */
63
62
  test("status command defines outputSchema", () => {
64
63
  const statusSource = readFileSync(join(exampleRoot, "src/commands/status/command.ts"), "utf8");
65
64
  expect(statusSource).toContain("outputSchema:");
66
65
  });
67
66
 
68
- /** ResolveCapabilities matches full sink shape. */
69
67
  test("resolveCapabilities matches full sink shape", () => {
70
68
  expect(resolveCapabilities(sinkProgram)).toEqual({
71
69
  completion: true,
@@ -72,7 +72,6 @@ describe("config/context", () => {
72
72
  expect(empty.dir).toBe(dirname(empty.path));
73
73
  });
74
74
 
75
- /** Tests that AppConfigSnapshot path uses OS default from program key. */
76
75
  test("AppConfigSnapshot path uses OS default from program key", () => {
77
76
  const ctx = createAppConfigSnapshot(program, {}, {});
78
77
  expect(ctx.path).toContain("ctx_test");
@@ -29,7 +29,7 @@ export function configEntryRequired(
29
29
  return true;
30
30
  }
31
31
 
32
- /** Whether prompts and `config get` should redact this entry. */
32
+ /** Whether prompts and `configure get` should redact this entry. */
33
33
  export function configEntrySensitive(key: string, entry: CliAppConfigEntry): boolean {
34
34
  return entry.sensitive ?? defaultConfigEntrySensitive(key);
35
35
  }
@@ -56,7 +56,7 @@ export function jsonSchemaRequiredKeys(
56
56
  return new Set(required.filter((k): k is string => typeof k === "string"));
57
57
  }
58
58
 
59
- /** Whether built-in config get/set commands are enabled. */
59
+ /** Whether built-in `configure get` / `configure set` subcommands are enabled. */
60
60
  export function configCommandsEnabled(program: CliProgram): boolean {
61
61
  if (!program.appConfig) {
62
62
  return false;
@@ -46,7 +46,6 @@ function withHome<T>(fn: (home: string) => T): T {
46
46
 
47
47
  /** Tests for config/file. */
48
48
  describe("config/file", () => {
49
- /** BuildProgramUserConfig from program.appConfig env entries. */
50
49
  test("buildProgramUserConfig from program.appConfig env entries", () => {
51
50
  const cfg = buildProgramUserConfig(program);
52
51
  expect(cfg?.api_token).toMatchObject({
@@ -57,7 +56,6 @@ describe("config/file", () => {
57
56
  expect(cfg?.port).toBeUndefined();
58
57
  });
59
58
 
60
- /** ResolveAppConfigPath uses config.json. */
61
59
  test("resolveAppConfigPath uses config.json", () => {
62
60
  withHome((home) => {
63
61
  expect(resolveAppConfigPath(program)).toBe(
@@ -100,7 +98,6 @@ describe("config/file", () => {
100
98
  }
101
99
  });
102
100
 
103
- /** Rejects unknown keys on read. */
104
101
  test("rejects unknown keys on read", () => {
105
102
  withHome(() => {
106
103
  const configPath = resolveAppConfigPath(program);
@@ -110,7 +107,6 @@ describe("config/file", () => {
110
107
  });
111
108
  });
112
109
 
113
- /** WriteAppConfigFile round-trip. */
114
110
  test("writeAppConfigFile round-trip", () => {
115
111
  withHome(() => {
116
112
  writeAppConfigFile(program, { apiToken: "saved" });
@@ -119,7 +115,6 @@ describe("config/file", () => {
119
115
  });
120
116
  });
121
117
 
122
- /** Tests that uninstallAppConfig removes config directory recursively. */
123
118
  test("uninstallAppConfig removes config directory recursively", () => {
124
119
  withHome(() => {
125
120
  writeAppConfigFile(program, { apiToken: "saved" });
@@ -31,7 +31,6 @@ const program: CliProgram = {
31
31
 
32
32
  /** Tests for config/resolve. */
33
33
  describe("config/resolve", () => {
34
- /** Tests that prefers env over file for mapped keys. */
35
34
  test("prefers env over file for mapped keys", () => {
36
35
  const prev = process.env.API_TOKEN;
37
36
  process.env.API_TOKEN = "from-env";
@@ -44,7 +43,6 @@ describe("config/resolve", () => {
44
43
  }
45
44
  });
46
45
 
47
- /** Tests that uses file when env empty. */
48
46
  test("uses file when env empty", () => {
49
47
  const prev = process.env.API_TOKEN;
50
48
  delete process.env.API_TOKEN;
@@ -56,7 +54,6 @@ describe("config/resolve", () => {
56
54
  }
57
55
  });
58
56
 
59
- /** Tests that empty string in env or file counts as missing. */
60
57
  test("empty string in env or file counts as missing", () => {
61
58
  const prev = process.env.API_TOKEN;
62
59
  process.env.API_TOKEN = "";
@@ -69,7 +66,6 @@ describe("config/resolve", () => {
69
66
  }
70
67
  });
71
68
 
72
- /** Applies jsonSchema default when file and env absent. */
73
69
  test("applies jsonSchema default when file and env absent", () => {
74
70
  const prev = process.env.API_TOKEN;
75
71
  delete process.env.API_TOKEN;
@@ -82,7 +78,6 @@ describe("config/resolve", () => {
82
78
  }
83
79
  });
84
80
 
85
- /** All-string mode uses entry.default. */
86
81
  test("all-string mode uses entry.default", () => {
87
82
  const stringProgram: CliProgram = {
88
83
  ...program,
@@ -96,7 +91,6 @@ describe("config/resolve", () => {
96
91
  expect(resolved.greeting).toBe("world");
97
92
  });
98
93
 
99
- /** Tests that prefers captured host env over file when process.env was exported from file. */
100
94
  test("prefers captured host env over file when process.env was exported from file", () => {
101
95
  const hostEnv = { API_TOKEN: "from-host" };
102
96
  process.env.API_TOKEN = "from-file-export";
@@ -108,7 +102,6 @@ describe("config/resolve", () => {
108
102
  }
109
103
  });
110
104
 
111
- /** ExportConfigToEnv does not overwrite host env. */
112
105
  test("exportConfigToEnv does not overwrite host env", () => {
113
106
  const prev = process.env.API_TOKEN;
114
107
  process.env.API_TOKEN = "from-host";
@@ -227,7 +227,7 @@ export function formatMissingConfigMessage(program: CliProgram, keys: string[]):
227
227
  return [
228
228
  `Missing required configuration: ${list}`,
229
229
  `Configure interactively: ${program.key} configure`,
230
- `Or set via: ${program.key} config set <key> <value>`,
230
+ `Or set via: ${program.key} configure set <key> <value>`,
231
231
  `Config file: ${path}`,
232
232
  `See: ${program.key} docs mcp`,
233
233
  ].join("\n");
@@ -240,7 +240,7 @@ export function formatMcpMissingConfigMessage(program: CliProgram, keys: string[
240
240
  return [
241
241
  `Missing required configuration: ${list}`,
242
242
  `Configure: ${program.key} configure`,
243
- `Or set via: ${program.key} config set`,
243
+ `Or set via: ${program.key} configure set`,
244
244
  `Config file: ${path}`,
245
245
  ].join("\n");
246
246
  }
@@ -23,7 +23,6 @@ const rootSchema = {
23
23
 
24
24
  /** Tests for config/validate. */
25
25
  describe("config/validate", () => {
26
- /** Tests that accepts valid document. */
27
26
  test("accepts valid document", () => {
28
27
  const result = validateConfigDocument(
29
28
  { apiToken: "x", maxRetries: 3, prefs: { ttl: 3600 } },
@@ -33,14 +32,12 @@ describe("config/validate", () => {
33
32
  expect(result.errors).toEqual([]);
34
33
  });
35
34
 
36
- /** Rejects missing required property. */
37
35
  test("rejects missing required property", () => {
38
36
  const result = validateConfigDocument({ apiToken: "x" }, rootSchema);
39
37
  expect(result.valid).toBe(false);
40
38
  expect(result.errors.some((e) => e.includes("maxRetries"))).toBe(true);
41
39
  });
42
40
 
43
- /** Rejects unknown property when additionalProperties is false. */
44
41
  test("rejects unknown property when additionalProperties is false", () => {
45
42
  const result = validateConfigDocument(
46
43
  { apiToken: "x", maxRetries: 1, extra: true },
@@ -50,19 +47,16 @@ describe("config/validate", () => {
50
47
  expect(result.errors.some((e) => e.includes("extra"))).toBe(true);
51
48
  });
52
49
 
53
- /** Rejects type mismatch. */
54
50
  test("rejects type mismatch", () => {
55
51
  const result = validateConfigDocument({ apiToken: "x", maxRetries: "nope" }, rootSchema);
56
52
  expect(result.valid).toBe(false);
57
53
  });
58
54
 
59
- /** ParseConfigSetValue coerces number and boolean. */
60
55
  test("parseConfigSetValue coerces number and boolean", () => {
61
56
  expect(parseConfigSetValue("5", { type: "integer" }, rootSchema, false)).toBe(5);
62
57
  expect(parseConfigSetValue("true", { type: "boolean" }, rootSchema, false)).toBe(true);
63
58
  });
64
59
 
65
- /** ParseConfigSetValue requires --json for objects. */
66
60
  test("parseConfigSetValue requires --json for objects", () => {
67
61
  expect(() => parseConfigSetValue('{"ttl":1}', { type: "object" }, rootSchema, false)).toThrow(
68
62
  /--json/,
@@ -48,19 +48,16 @@ afterEach(() => {
48
48
 
49
49
  /** Tests for configure opts. */
50
50
  describe("configure opts", () => {
51
- /** Validate rejects multiple modes. */
52
51
  test("validate rejects multiple modes", () => {
53
52
  const opts = parseConfigureOpts({ sync: "1", status: "1" });
54
53
  expect(validateConfigureOpts(opts)).toContain("only one");
55
54
  });
56
55
 
57
- /** Sync requires --yes. */
58
56
  test("sync requires --yes", () => {
59
57
  const opts = parseConfigureOpts({ sync: "1" });
60
58
  expect(validateConfigureOpts(opts)).toContain("--yes");
61
59
  });
62
60
 
63
- /** Remove-all requires --yes. */
64
61
  test("remove-all requires --yes", () => {
65
62
  const opts = parseConfigureOpts({ "remove-all": "1" });
66
63
  expect(validateConfigureOpts(opts)).toContain("--yes");
@@ -69,14 +66,12 @@ describe("configure opts", () => {
69
66
 
70
67
  /** Tests for install paths. */
71
68
  describe("install paths", () => {
72
- /** ResolveInstallPaths includes skill and mcp paths. */
73
69
  test("resolveInstallPaths includes skill and mcp paths", () => {
74
70
  const paths = resolveInstallPaths(fixture);
75
71
  expect(paths.cursorSkillDir).toContain(".cursor/skills");
76
72
  expect(paths.cursorMcpPath).toContain("mcp.json");
77
73
  });
78
74
 
79
- /** Claude desktop mcp path on darwin. */
80
75
  test("claude desktop mcp path on darwin", () => {
81
76
  const prev = process.platform;
82
77
  Object.defineProperty(process, "platform", { value: "darwin" });
@@ -110,7 +105,6 @@ describe("detect installed", () => {
110
105
 
111
106
  /** Tests for sync plan. */
112
107
  describe("sync plan", () => {
113
- /** BuildUpdatePlan greenfield includes agent targets. */
114
108
  test("buildUpdatePlan greenfield includes agent targets", () => {
115
109
  const paths = resolveInstallPaths(fixture);
116
110
  const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
@@ -118,7 +112,6 @@ describe("sync plan", () => {
118
112
  expect(plan.some((a) => a.kind === "app")).toBe(false);
119
113
  });
120
114
 
121
- /** BuildInstallPlan --all omits app self-install. */
122
115
  test("buildInstallPlan --all omits app self-install", () => {
123
116
  const paths = resolveInstallPaths(fixture);
124
117
  const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
@@ -126,7 +119,6 @@ describe("sync plan", () => {
126
119
  expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
127
120
  });
128
121
 
129
- /** BuildInstallPlan respects configure.agentIntegration skill. */
130
122
  test("buildInstallPlan respects configure.agentIntegration skill", () => {
131
123
  const skillApp: CliProgram = {
132
124
  ...fixture,
@@ -142,7 +134,6 @@ describe("sync plan", () => {
142
134
 
143
135
  /** Tests for remove plan. */
144
136
  describe("remove plan", () => {
145
- /** BuildUninstallPlan --all with nothing installed is empty. */
146
137
  test("buildUninstallPlan --all with nothing installed is empty", () => {
147
138
  const paths = resolveInstallPaths(fixture);
148
139
  const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
@@ -152,7 +143,6 @@ describe("remove plan", () => {
152
143
 
153
144
  /** Tests for app config wizard. */
154
145
  describe("app config wizard", () => {
155
- /** Tests that appConfigHasEntries when entries exist. */
156
146
  test("appConfigHasEntries when entries exist", () => {
157
147
  expect(
158
148
  appConfigHasEntries({
@@ -162,7 +152,6 @@ describe("app config wizard", () => {
162
152
  ).toBe(true);
163
153
  });
164
154
 
165
- /** Tests that appConfigHasEntries false for empty entries or missing appConfig. */
166
155
  test("appConfigHasEntries false for empty entries or missing appConfig", () => {
167
156
  expect(appConfigHasEntries({ ...fixture, appConfig: { entries: {} } })).toBe(false);
168
157
  expect(appConfigHasEntries(fixture)).toBe(false);
@@ -49,7 +49,6 @@ const nestedFixture: CliProgram = {
49
49
  ],
50
50
  };
51
51
 
52
- /** Tests that generateApiGuideBody matches command section of full API guide. */
53
52
  test("generateApiGuideBody matches command section of full API guide", () => {
54
53
  const body = generateApiGuideBody(nestedFixture);
55
54
  const full = generateApiGuide(nestedFixture);
@@ -58,7 +57,6 @@ test("generateApiGuideBody matches command section of full API guide", () => {
58
57
  expect(body).not.toContain("CLI API reference");
59
58
  });
60
59
 
61
- /** Tests that generateApiGuide covers the same command keys as cliSchemaExport. */
62
60
  test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
63
61
  const md = generateApiGuide(nestedFixture);
64
62
  const schema = cliSchemaExport(nestedFixture);
@@ -68,7 +66,6 @@ test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
68
66
  expect(schema.commands?.map((c) => c.key)).toEqual(["stat"]);
69
67
  });
70
68
 
71
- /** Tests that generateApiGuide resolves program key in install notes. */
72
69
  test("generateApiGuide resolves program key in install notes", () => {
73
70
  const fixture: CliProgram = {
74
71
  key: "myapp",
@@ -82,7 +79,6 @@ test("generateApiGuide resolves program key in install notes", () => {
82
79
  expect(md).not.toContain("Upgrade to latest release");
83
80
  });
84
81
 
85
- /** Tests that generateApiGuide mentions Homebrew upgrade. */
86
82
  test("generateApiGuide mentions Homebrew upgrade", () => {
87
83
  const fixture: CliProgram = {
88
84
  key: "myapp",
@@ -83,26 +83,22 @@ test("docs rejects reserved topic keys", () => {
83
83
  expect(() => cliValidateProgram(root)).toThrow(/reserved/);
84
84
  });
85
85
 
86
- /** DocsEffectiveDefaultTopic uses first topic key. */
87
86
  test("docsEffectiveDefaultTopic uses first topic key", () => {
88
87
  expect(docsEffectiveDefaultTopic(docsFixture().docs!)).toBe("readme");
89
88
  });
90
89
 
91
- /** Bare docs prints first topic via Cli.invoke. */
92
90
  test("bare docs prints first topic via Cli.invoke", async () => {
93
91
  const result = await new Cli(docsFixture()).invoke(["docs"]);
94
92
  expect(result.exitCode).toBe(0);
95
93
  expect(result.stdout).toContain("Hello README");
96
94
  });
97
95
 
98
- /** Docs readme prints bundled text. */
99
96
  test("docs readme prints bundled text", async () => {
100
97
  const result = await new Cli(docsFixture()).invoke(["docs", "readme"]);
101
98
  expect(result.exitCode).toBe(0);
102
99
  expect(result.stdout).toContain("Hello README");
103
100
  });
104
101
 
105
- /** Docs defaultTopic override. */
106
102
  test("docs defaultTopic override", async () => {
107
103
  const root = docsFixture();
108
104
  root.docs!.defaultTopic = "arch";
@@ -110,7 +106,6 @@ test("docs defaultTopic override", async () => {
110
106
  expect(result.stdout).toContain("Architecture");
111
107
  });
112
108
 
113
- /** Docs mcp when MCP enabled. */
114
109
  test("docs mcp when MCP enabled", async () => {
115
110
  const result = await new Cli(docsFixture(true)).invoke(["docs", "mcp"]);
116
111
  expect(result.exitCode).toBe(0);
@@ -120,13 +115,11 @@ test("docs mcp when MCP enabled", async () => {
120
115
  expect(result.stdout).toContain("configure --sync --yes");
121
116
  });
122
117
 
123
- /** Docs rejects unknown subcommand. */
124
118
  test("docs rejects unknown subcommand", async () => {
125
119
  const result = await new Cli(docsFixture()).invoke(["docs", "all"]);
126
120
  expect(result.exitCode).not.toBe(0);
127
121
  });
128
122
 
129
- /** Docs mcp absent from router when MCP disabled. */
130
123
  test("docs mcp absent from router when MCP disabled", async () => {
131
124
  const root = docsFixture(false);
132
125
  const presentation = cliPresentationRoot(root);
@@ -139,7 +132,6 @@ test("docs mcp absent from router when MCP disabled", async () => {
139
132
  expect(result.exitCode).not.toBe(0);
140
133
  });
141
134
 
142
- /** Presentation includes docs subtree. */
143
135
  test("presentation includes docs subtree", () => {
144
136
  const presentation = cliPresentationRoot(docsFixture());
145
137
  const docsNode = presentation.commands.find((c) => c.key === "docs");
@@ -149,7 +141,6 @@ test("presentation includes docs subtree", () => {
149
141
  ).toBe(true);
150
142
  });
151
143
 
152
- /** Docs schema prints JSON. */
153
144
  test("docs schema prints JSON", async () => {
154
145
  const result = await new Cli(docsFixture()).invoke(["docs", "schema"]);
155
146
  expect(result.exitCode).toBe(0);
@@ -158,7 +149,6 @@ test("docs schema prints JSON", async () => {
158
149
  expect(schema.commands.some((c: { key: string }) => c.key === "run")).toBe(true);
159
150
  });
160
151
 
161
- /** Docs api prints markdown reference. */
162
152
  test("docs api prints markdown reference", async () => {
163
153
  const result = await new Cli(docsFixture()).invoke(["docs", "api"]);
164
154
  expect(result.exitCode).toBe(0);
@@ -168,7 +158,6 @@ test("docs api prints markdown reference", async () => {
168
158
  expect(result.stdout).toContain("myapp docs schema");
169
159
  });
170
160
 
171
- /** Tests that skipsRequiredAppConfigExit includes docs and config builtins. */
172
161
  test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
173
162
  const program = {
174
163
  ...docsFixture(),
@@ -178,11 +167,10 @@ test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
178
167
  };
179
168
  const caps = resolveCapabilities(program);
180
169
  expect(skipsRequiredAppConfigExit(["docs", "api"], caps)).toBe(true);
181
- expect(skipsRequiredAppConfigExit(["config", "get"], caps)).toBe(true);
170
+ expect(skipsRequiredAppConfigExit(["configure", "get"], caps)).toBe(true);
182
171
  expect(skipsRequiredAppConfigExit(["run"], caps)).toBe(false);
183
172
  });
184
173
 
185
- /** Docs skill prints Cursor SKILL.md. */
186
174
  test("docs skill prints Cursor SKILL.md", async () => {
187
175
  const result = await new Cli(docsFixture()).invoke(["docs", "skill"]);
188
176
  expect(result.exitCode).toBe(0);
@@ -209,7 +197,6 @@ test("docs skill help recommends configure", async () => {
209
197
  }
210
198
  });
211
199
 
212
- /** Presentation includes docs schema and skill. */
213
200
  test("presentation includes docs schema and skill", () => {
214
201
  const presentation = cliPresentationRoot(docsFixture());
215
202
  const docsNode = presentation.commands.find((c) => c.key === "docs");
@@ -221,7 +208,6 @@ test("presentation includes docs schema and skill", () => {
221
208
  }
222
209
  });
223
210
 
224
- /** Completions offer docs subcommands. */
225
211
  test("completions offer docs subcommands", () => {
226
212
  const bash = completionBashScript(cliPresentationRoot(docsFixture()));
227
213
  expect(bash).toContain("docs) echo");
@@ -231,7 +217,6 @@ test("completions offer docs subcommands", () => {
231
217
  expect(bash).toContain("skill) echo");
232
218
  });
233
219
 
234
- /** Tests that generateMcpGuide includes schema URI and configure sync. */
235
220
  test("generateMcpGuide includes schema URI and configure sync", () => {
236
221
  const guide = generateMcpGuide(docsFixture(true));
237
222
  expect(guide).toContain("myapp://schema");
@@ -245,7 +230,6 @@ test("generateMcpGuide includes schema URI and configure sync", () => {
245
230
  expect(guide).toContain("ChatGPT");
246
231
  });
247
232
 
248
- /** Docs --save writes topic file. */
249
233
  test("docs --save writes topic file", async () => {
250
234
  const result = await new Cli(docsFixture()).invoke(["docs", "readme", "--save"]);
251
235
  expect(result.exitCode).toBe(0);
@@ -255,7 +239,6 @@ test("docs --save writes topic file", async () => {
255
239
  expect(text).not.toContain("Generated by");
256
240
  });
257
241
 
258
- /** Docs api --save prepends generated hint. */
259
242
  test("docs api --save prepends generated hint", async () => {
260
243
  const result = await new Cli(docsFixture()).invoke(["docs", "api", "--save"]);
261
244
  expect(result.exitCode).toBe(0);
@@ -266,7 +249,6 @@ test("docs api --save prepends generated hint", async () => {
266
249
  expect(text).toContain("CLI API reference");
267
250
  });
268
251
 
269
- /** Docs skill --save keeps frontmatter first. */
270
252
  test("docs skill --save keeps frontmatter first", async () => {
271
253
  const result = await new Cli(docsFixture()).invoke(["docs", "skill", "--save"]);
272
254
  expect(result.exitCode).toBe(0);
@@ -277,7 +259,6 @@ test("docs skill --save keeps frontmatter first", async () => {
277
259
  expect(text.indexOf(hint)).toBeGreaterThan(text.indexOf("---\n", 4));
278
260
  });
279
261
 
280
- /** Docs schema --save writes JSON file. */
281
262
  test("docs schema --save writes JSON file", async () => {
282
263
  const result = await new Cli(docsFixture()).invoke(["docs", "schema", "--save"]);
283
264
  expect(result.exitCode).toBe(0);
@@ -288,7 +269,6 @@ test("docs schema --save writes JSON file", async () => {
288
269
  expect(schema.key).toBe("myapp");
289
270
  });
290
271
 
291
- /** Tests that saveDocsTopic returns relative path. */
292
272
  test("saveDocsTopic returns relative path", () => {
293
273
  const path = saveDocsTopic(docsFixture(), "api");
294
274
  expect(path).toBe("docs/api.md");
@@ -34,17 +34,14 @@ function fixture(opts?: { docs?: boolean; mcp?: boolean }): CliProgram {
34
34
  };
35
35
  }
36
36
 
37
- /** DefaultDocsTopicResourceUri. */
38
37
  test("defaultDocsTopicResourceUri", () => {
39
38
  expect(defaultDocsTopicResourceUri("my_app", "readme")).toBe("my_app://docs/readme");
40
39
  });
41
40
 
42
- /** ResolveDocsTopicResourceUri sanitizes program key. */
43
41
  test("resolveDocsTopicResourceUri sanitizes program key", () => {
44
42
  expect(resolveDocsTopicResourceUri(fixture(), "readme")).toBe("my_app://docs/readme");
45
43
  });
46
44
 
47
- /** DocsMcpResources when docs and MCP enabled. */
48
45
  test("docsMcpResources when docs and MCP enabled", () => {
49
46
  const resources = docsMcpResources(fixture());
50
47
  expect(resources.map((r) => r.uri)).toEqual(["my_app://docs/readme", "my_app://docs/arch"]);
@@ -54,17 +51,14 @@ test("docsMcpResources when docs and MCP enabled", () => {
54
51
  expect(resources[0]?.load()).toBe("# Readme\n");
55
52
  });
56
53
 
57
- /** DocsMcpResources empty when docs disabled. */
58
54
  test("docsMcpResources empty when docs disabled", () => {
59
55
  expect(docsMcpResources(fixture({ docs: false }))).toEqual([]);
60
56
  });
61
57
 
62
- /** DocsMcpResources empty when MCP disabled. */
63
58
  test("docsMcpResources empty when MCP disabled", () => {
64
59
  expect(docsMcpResources(fixture({ mcp: false }))).toEqual([]);
65
60
  });
66
61
 
67
- /** Tests that reservedDocsTopicResourceUris matches docsMcpResources URIs. */
68
62
  test("reservedDocsTopicResourceUris matches docsMcpResources URIs", () => {
69
63
  const program = fixture();
70
64
  expect(reservedDocsTopicResourceUris(program)).toEqual(
@@ -12,7 +12,6 @@ import {
12
12
  } from "./formats.ts";
13
13
  import { CliValueFormat } from "./types.ts";
14
14
 
15
- /** ParseDurationMs parses minutes and hours. */
16
15
  test("parseDurationMs parses minutes and hours", () => {
17
16
  expect(parseDurationMs("30s")).toBe(30_000);
18
17
  expect(parseDurationMs("5m")).toBe(5 * 60 * 1000);
@@ -20,25 +19,21 @@ test("parseDurationMs parses minutes and hours", () => {
20
19
  expect(parseDurationMs("1d")).toBe(24 * 60 * 60 * 1000);
21
20
  });
22
21
 
23
- /** ParseCommaList splits and trims. */
24
22
  test("parseCommaList splits and trims", () => {
25
23
  expect(parseCommaList("a,b")).toEqual(["a", "b"]);
26
24
  expect(parseCommaList(" a , b , ")).toEqual(["a", "b"]);
27
25
  });
28
26
 
29
- /** ParseDate validates calendar dates. */
30
27
  test("parseDate validates calendar dates", () => {
31
28
  expect(parseDate("2026-06-22")).toBe("2026-06-22");
32
29
  expect(() => parseDate("2026-02-30")).toThrow();
33
30
  });
34
31
 
35
- /** ParseDateTime normalizes to UTC ISO. */
36
32
  test("parseDateTime normalizes to UTC ISO", () => {
37
33
  expect(parseDateTime("2026-06-22T15:00:00Z")).toBe("2026-06-22T15:00:00.000Z");
38
34
  expect(() => parseDateTime("2026-06-22")).toThrow();
39
35
  });
40
36
 
41
- /** ValidateFormatValue rejects invalid duration. */
42
37
  test("validateFormatValue rejects invalid duration", () => {
43
38
  expect(() => validateFormatValue("nope", CliValueFormat.Duration)).toThrow();
44
39
  });
@@ -12,14 +12,12 @@ import {
12
12
  wantsExplicitJson,
13
13
  } from "./headless.ts";
14
14
 
15
- /** WantsExplicitJson includes MCP invocation. */
16
15
  test("wantsExplicitJson includes MCP invocation", () => {
17
16
  expect(wantsExplicitJson({ invocation: "cli" }, false)).toBe(false);
18
17
  expect(wantsExplicitJson({ invocation: "mcp" }, false)).toBe(true);
19
18
  expect(wantsExplicitJson({ invocation: "cli" }, true)).toBe(true);
20
19
  });
21
20
 
22
- /** Tests that shouldRunHeadless is true for MCP and json. */
23
21
  test("shouldRunHeadless is true for MCP and json", () => {
24
22
  expect(shouldRunHeadless({ invocation: "mcp" }, false)).toBe(true);
25
23
  expect(shouldRunHeadless({ invocation: "cli" }, true)).toBe(true);
@@ -27,7 +25,6 @@ test("shouldRunHeadless is true for MCP and json", () => {
27
25
  expect(shouldRunHeadless({ invocation: "cli" }, false, false, false)).toBe(true);
28
26
  });
29
27
 
30
- /** Tests that shouldRunHeadlessWithPositionals requires positionals in non-tty. */
31
28
  test("shouldRunHeadlessWithPositionals requires positionals in non-tty", () => {
32
29
  expect(shouldRunHeadlessWithPositionals({ invocation: "cli" }, false, [], false, false)).toBe(
33
30
  false,
@@ -54,7 +51,6 @@ test("shouldRunHeadlessWithYes requires yes in non-tty", () => {
54
51
  ).toBe(true);
55
52
  });
56
53
 
57
- /** Tests that formatDryRunMessage prefixes dry-run output. */
58
54
  test("formatDryRunMessage prefixes dry-run output", () => {
59
55
  expect(formatDryRunMessage("hello", false)).toBe("hello");
60
56
  expect(formatDryRunMessage("hello", true)).toBe("[DRY RUN] hello");
package/src/headless.ts CHANGED
@@ -57,14 +57,15 @@ export function shouldRunHeadlessWithYes(
57
57
  return opts.yes && opts.hasRequiredArgs;
58
58
  }
59
59
 
60
- /**
61
- * Exits when non-interactive mode is used without `--yes`.
62
- * @param hint - Command-specific guidance appended to the error
63
- */
60
+ /** Exits when non-interactive mode is used without `--yes`. */
64
61
  export function requireYesInNonTty(
62
+ /** True when `--yes` was passed on the command line. */
65
63
  yes: boolean,
64
+ /** Command-specific guidance appended to the error message. */
66
65
  hint: string,
66
+ /** When true, skip the check (dry-run preview). */
67
67
  dryRun = false,
68
+ /** Injectable TTY probe for tests. */
68
69
  interactive: boolean = isInteractiveTty,
69
70
  ): void {
70
71
  if (dryRun) return;