argsbarg 4.1.1 → 5.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.
Files changed (82) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +5 -5
  3. package/docs/README.md +3 -3
  4. package/docs/ai-skills.md +9 -9
  5. package/docs/bundled-docs.md +4 -4
  6. package/docs/cli-program.md +8 -8
  7. package/docs/config-schema.md +2 -2
  8. package/docs/configure.md +177 -0
  9. package/docs/developing.md +1 -1
  10. package/docs/distribution-homebrew.md +12 -9
  11. package/docs/mcp.md +9 -9
  12. package/examples/full-example/README.md +3 -3
  13. package/examples/full-example/justfile +6 -6
  14. package/examples/full-example/scripts/formula-shared.ts +3 -2
  15. package/examples/full-example/src/program.ts +3 -3
  16. package/examples/nested.ts +1 -1
  17. package/index.d.ts +23 -16
  18. package/package.json +1 -1
  19. package/src/builtins/builtins.test.ts +105 -61
  20. package/src/builtins/completion-group.ts +4 -6
  21. package/src/builtins/config.test.ts +8 -0
  22. package/src/builtins/configure-copy.ts +86 -0
  23. package/src/builtins/configure.ts +70 -0
  24. package/src/builtins/dispatch.ts +13 -33
  25. package/src/builtins/index.ts +1 -1
  26. package/src/builtins/mcp.ts +2 -2
  27. package/src/builtins/registry.ts +6 -6
  28. package/src/capabilities.ts +22 -13
  29. package/src/cli-tool/cli-smoke.test.ts +21 -3
  30. package/src/cli-tool/create.test.ts +36 -1
  31. package/src/cli-tool/create.ts +26 -4
  32. package/src/cli-tool/full-example-capabilities.test.ts +10 -2
  33. package/src/cli-tool/main.ts +0 -0
  34. package/src/cli-tool/program.ts +20 -5
  35. package/src/cli-tool/run-create.ts +8 -19
  36. package/src/config/bootstrap.ts +11 -9
  37. package/src/config/context.test.ts +8 -0
  38. package/src/config/file.test.ts +13 -1
  39. package/src/config/resolve.test.ts +18 -0
  40. package/src/config/resolve.ts +2 -2
  41. package/src/config/validate.test.ts +11 -0
  42. package/src/config.integration.test.ts +5 -0
  43. package/src/configure/configure.test.ts +170 -0
  44. package/src/configure/index.ts +298 -0
  45. package/src/configure/prompt.ts +47 -0
  46. package/src/docs/api-guide.test.ts +10 -0
  47. package/src/docs/builtin.ts +3 -5
  48. package/src/docs/docs.test.ts +32 -5
  49. package/src/docs/mcp-guide.ts +4 -4
  50. package/src/docs/mcp-resources.test.ts +10 -0
  51. package/src/formats.test.ts +9 -0
  52. package/src/headless.test.ts +10 -0
  53. package/src/hidden-mcpb.test.ts +22 -0
  54. package/src/index.ts +2 -2
  55. package/src/install/binary-placement.test.ts +14 -0
  56. package/src/install/gh-release-update.test.ts +9 -0
  57. package/src/install/install-validate.test.ts +15 -5
  58. package/src/install/mcp-codex.test.ts +7 -0
  59. package/src/install/mcp-openclaw.test.ts +6 -0
  60. package/src/install/mcp-opencode.test.ts +10 -0
  61. package/src/install/opts.ts +17 -0
  62. package/src/install/status.test.ts +9 -0
  63. package/src/install/target-effective.ts +8 -8
  64. package/src/install/target-scope.ts +11 -8
  65. package/src/install/targets/configure.ts +1 -1
  66. package/src/install/targets.test.ts +23 -4
  67. package/src/invoke.test.ts +15 -1
  68. package/src/mcp/claude.test.ts +10 -0
  69. package/src/mcp/env.test.ts +12 -0
  70. package/src/mcp/tools.ts +1 -1
  71. package/src/mcp/zip.test.ts +5 -0
  72. package/src/mcp.integration.test.ts +43 -4
  73. package/src/parse.test.ts +82 -13
  74. package/src/schema.ts +1 -9
  75. package/src/skill/hint.ts +2 -2
  76. package/src/types.ts +22 -14
  77. package/src/validate.ts +17 -17
  78. package/docs/install.md +0 -206
  79. package/src/builtins/install.ts +0 -106
  80. package/src/builtins/uninstall.ts +0 -80
  81. package/src/install/index.ts +0 -409
  82. package/src/install/install.test.ts +0 -317
@@ -0,0 +1,298 @@
1
+ /*
2
+ Interactive and automated `configure` command orchestration (agent artifacts and app config).
3
+ */
4
+
5
+ import { resolveCapabilities } from "../capabilities.ts";
6
+ import { displayAppConfigPath, runConfigure } from "../config/bootstrap.ts";
7
+ import { resolveInstallPaths } from "../install/paths.ts";
8
+ import { buildInstallPlan, buildUpdatePlan } from "../install/plan.ts";
9
+ import {
10
+ installErr,
11
+ installInfo,
12
+ installOut,
13
+ printInstallStatus,
14
+ writeInteractiveInstallIntro,
15
+ } from "../install/status.ts";
16
+ import { resolveEffectiveInstallTargets } from "../install/target-effective.ts";
17
+ import { runTargetPreflight } from "../install/target-plan-build.ts";
18
+ import { INSTALL_TARGETS, installTargetForKey } from "../install/target-registry.ts";
19
+ import { buildDetectedSnapshot, buildTargetPlanContext } from "../install/target-scope.ts";
20
+ import type {
21
+ CliInstallArtifactKey,
22
+ InstallAction,
23
+ InstallActionKind,
24
+ InstallOpts,
25
+ TargetPlanContext,
26
+ UninstallAction,
27
+ } from "../install/target-types.ts";
28
+ import {
29
+ buildUninstallPlan,
30
+ skillDirFromUninstallSummary,
31
+ uninstallSkillDir,
32
+ } from "../install/uninstall.ts";
33
+ import { cliSkillInstall, skillTargetFromActionKind } from "../skill/install.ts";
34
+ import type { CliProgram } from "../types.ts";
35
+ import { artifactPromptLabel, promptTargetAction } from "./prompt.ts";
36
+
37
+ /** True when interactive `configure` should auto-run the app config wizard. */
38
+ export function appConfigHasEntries(program: CliProgram): boolean {
39
+ const entries = program.appConfig?.entries;
40
+ return !!entries && Object.keys(entries).length > 0;
41
+ }
42
+
43
+ /** Parsed flags for the top-level `configure` built-in. */
44
+ export interface ConfigureOpts {
45
+ sync?: boolean;
46
+ removeAll?: boolean;
47
+ removeConfig?: boolean;
48
+ status?: boolean;
49
+ yes?: boolean;
50
+ dry?: boolean;
51
+ json?: boolean;
52
+ }
53
+
54
+ /** Maps raw argv flags into {@link ConfigureOpts}. */
55
+ export function parseConfigureOpts(raw: Record<string, string>): ConfigureOpts {
56
+ const flag = (name: string) => raw[name] === "1";
57
+ return {
58
+ sync: flag("sync"),
59
+ removeAll: flag("remove-all"),
60
+ removeConfig: flag("remove-config"),
61
+ status: flag("status"),
62
+ yes: flag("yes"),
63
+ dry: flag("dry"),
64
+ json: flag("json"),
65
+ };
66
+ }
67
+
68
+ /** Returns an error message when configure flags are inconsistent; otherwise null. */
69
+ export function validateConfigureOpts(opts: ConfigureOpts): string | null {
70
+ const flags = [opts.sync, opts.removeAll, opts.removeConfig, opts.status].filter(Boolean);
71
+ if (flags.length > 1) {
72
+ return "Use only one of --sync, --remove-all, --remove-config, or --status.";
73
+ }
74
+ if (opts.json) {
75
+ opts.yes = true;
76
+ }
77
+ if ((opts.sync || opts.removeAll || opts.removeConfig) && !opts.yes) {
78
+ return "--yes is required with --sync, --remove-all, or --remove-config.";
79
+ }
80
+ return null;
81
+ }
82
+
83
+ /** Adapts configure flags to internal install-plan option shape. */
84
+ function configureToInstallOpts(opts: ConfigureOpts): InstallOpts {
85
+ if (opts.status) {
86
+ return { status: true, yes: opts.yes, dry: opts.dry, json: opts.json };
87
+ }
88
+ if (opts.sync) {
89
+ return { reinstall: true, yes: true, dry: opts.dry, json: opts.json };
90
+ }
91
+ if (opts.removeAll) {
92
+ return { uninstall: true, all: true, yes: true, dry: opts.dry, json: opts.json };
93
+ }
94
+ if (opts.removeConfig) {
95
+ return { uninstall: true, configure: true, yes: true, dry: opts.dry, json: opts.json };
96
+ }
97
+ return { dry: opts.dry, json: opts.json };
98
+ }
99
+
100
+ /** Installs a skill target and returns changed paths. */
101
+ function runSkillAction(root: CliProgram, kind: InstallActionKind, opts: InstallOpts): string[] {
102
+ const target = skillTargetFromActionKind(kind);
103
+ if (!target) return [];
104
+ return cliSkillInstall(root, target, {
105
+ global: true,
106
+ rimraf: true,
107
+ dry: opts.dry,
108
+ });
109
+ }
110
+
111
+ /** Runs install or uninstall actions and collects changed paths. */
112
+ function executePlan(
113
+ root: CliProgram,
114
+ actions: Array<InstallAction | UninstallAction>,
115
+ opts: InstallOpts,
116
+ showProgress: boolean,
117
+ ): string[] {
118
+ const changed: string[] = [];
119
+ const paths = resolveInstallPaths(root);
120
+ for (const action of actions) {
121
+ if (showProgress) {
122
+ installInfo(action.message, opts);
123
+ }
124
+ if ("kind" in action && action.kind) {
125
+ const skillTarget = skillTargetFromActionKind(action.kind);
126
+ if (skillTarget) {
127
+ changed.push(...runSkillAction(root, action.kind as InstallActionKind, opts));
128
+ continue;
129
+ }
130
+ }
131
+ if (!("kind" in action) || !action.kind) {
132
+ const skillDir = skillDirFromUninstallSummary(action.summary, paths);
133
+ if (skillDir) {
134
+ changed.push(...uninstallSkillDir(skillDir, !!opts.dry));
135
+ continue;
136
+ }
137
+ }
138
+ changed.push(...action.run());
139
+ }
140
+ return changed;
141
+ }
142
+
143
+ /** Builds plan context limited to a single artifact key. */
144
+ function buildSingleTargetContext(
145
+ root: CliProgram,
146
+ paths: ReturnType<typeof resolveInstallPaths>,
147
+ detected: ReturnType<typeof buildDetectedSnapshot>,
148
+ key: CliInstallArtifactKey,
149
+ mode: "install" | "uninstall",
150
+ opts: InstallOpts,
151
+ ): TargetPlanContext {
152
+ const effective = resolveEffectiveInstallTargets(root.configure, root);
153
+ const base = buildTargetPlanContext(root, paths, opts, detected);
154
+ return {
155
+ ...base,
156
+ mode: mode === "install" ? "install-scoped" : "uninstall-scoped",
157
+ include: (k) => k === key && effective[k].enabled,
158
+ };
159
+ }
160
+
161
+ /** Resolves install or uninstall actions for one artifact target. */
162
+ function actionsForTarget(
163
+ root: CliProgram,
164
+ paths: ReturnType<typeof resolveInstallPaths>,
165
+ key: CliInstallArtifactKey,
166
+ action: "install" | "uninstall",
167
+ opts: InstallOpts,
168
+ ): Array<InstallAction | UninstallAction> {
169
+ const detected = buildDetectedSnapshot(root, paths);
170
+ const target = installTargetForKey(key);
171
+ if (!target) return [];
172
+ const ctx = buildSingleTargetContext(root, paths, detected, key, action, opts);
173
+ if (action === "install") {
174
+ return target.planInstall(ctx);
175
+ }
176
+ return target.planUninstall(ctx);
177
+ }
178
+
179
+ /** Walks enabled targets with per-target prompts (TTY required). */
180
+ async function runInteractiveConfigure(root: CliProgram, opts: ConfigureOpts): Promise<string[]> {
181
+ if (!process.stdin.isTTY) {
182
+ throw new Error("Interactive configure requires a TTY. Use flags such as --sync --yes.");
183
+ }
184
+
185
+ writeInteractiveInstallIntro(root);
186
+ const paths = resolveInstallPaths(root);
187
+ const detected = buildDetectedSnapshot(root, paths);
188
+ const effective = resolveEffectiveInstallTargets(root.configure, root);
189
+ const mutationOpts: InstallOpts = { dry: opts.dry, json: opts.json };
190
+ const changed: string[] = [];
191
+
192
+ for (const target of INSTALL_TARGETS) {
193
+ if (target.key === "app") continue;
194
+ if (!effective[target.key].enabled) continue;
195
+ if (target.key !== "configure" && !target.isAvailable(root, paths)) continue;
196
+ if (target.key === "configure" && !root.appConfig) continue;
197
+
198
+ if (target.key === "configure") {
199
+ if (!appConfigHasEntries(root)) continue;
200
+
201
+ const result = runConfigure(root, { context: "standalone", showHeading: false });
202
+ if (result.changed) {
203
+ installOut(`Wrote config: ${displayAppConfigPath(root)}`, mutationOpts);
204
+ changed.push(displayAppConfigPath(root));
205
+ }
206
+ continue;
207
+ }
208
+
209
+ const label = artifactPromptLabel(target.key);
210
+ const installed = target.detectedForSnapshot(detected);
211
+
212
+ const statusHint = installed ? "installed" : "not installed";
213
+ process.stderr.write(`\n${label} (${statusHint})\n`);
214
+ const choice = promptTargetAction(label, installed);
215
+ if (!choice || choice === "skip") continue;
216
+
217
+ const actions = actionsForTarget(root, paths, target.key, choice, mutationOpts);
218
+ if (actions.length === 0) continue;
219
+
220
+ const installActions = actions.filter(
221
+ (a): a is InstallAction =>
222
+ "kind" in a && typeof a.kind === "string" && a.kind.includes("-mcp"),
223
+ );
224
+ if (choice === "install" && resolveCapabilities(root).mcp && installActions.length > 0) {
225
+ runTargetPreflight(root, paths, mutationOpts, installActions);
226
+ }
227
+
228
+ changed.push(...executePlan(root, actions, mutationOpts, true));
229
+ }
230
+
231
+ return changed;
232
+ }
233
+
234
+ /** Runs sync, remove, or status modes without per-target prompts. */
235
+ async function runAutomatedConfigure(root: CliProgram, opts: ConfigureOpts): Promise<string[]> {
236
+ const installOpts = configureToInstallOpts(opts);
237
+ const paths = resolveInstallPaths(root);
238
+
239
+ if (installOpts.status) {
240
+ printInstallStatus(root, installOpts);
241
+ return [];
242
+ }
243
+
244
+ let actions: Array<InstallAction | UninstallAction>;
245
+ if (installOpts.uninstall) {
246
+ actions = buildUninstallPlan(root, paths, installOpts);
247
+ } else if (installOpts.reinstall) {
248
+ actions = buildUpdatePlan(root, paths, installOpts);
249
+ } else {
250
+ actions = buildInstallPlan(root, paths, installOpts);
251
+ }
252
+
253
+ const installActions = actions.filter(
254
+ (a): a is InstallAction => "kind" in a && typeof a.kind === "string" && a.kind.includes("-mcp"),
255
+ );
256
+ if (!installOpts.uninstall && resolveCapabilities(root).mcp && installActions.length > 0) {
257
+ runTargetPreflight(root, paths, installOpts, installActions);
258
+ }
259
+
260
+ return executePlan(root, actions, installOpts, true);
261
+ }
262
+
263
+ /** Main configure command orchestrator. */
264
+ export async function cliConfigure(
265
+ root: CliProgram,
266
+ rawOpts: Record<string, string>,
267
+ ): Promise<never> {
268
+ const opts = parseConfigureOpts(rawOpts);
269
+ const err = validateConfigureOpts(opts);
270
+ if (err) {
271
+ installErr(err);
272
+ process.exit(1);
273
+ }
274
+
275
+ const isInteractive = !opts.sync && !opts.removeAll && !opts.removeConfig && !opts.status;
276
+
277
+ let changed: string[] = [];
278
+ try {
279
+ changed = isInteractive
280
+ ? await runInteractiveConfigure(root, opts)
281
+ : await runAutomatedConfigure(root, opts);
282
+ } catch (mutationErr) {
283
+ installErr(mutationErr instanceof Error ? mutationErr.message : String(mutationErr));
284
+ process.exit(1);
285
+ }
286
+
287
+ if (opts.json && !opts.status) {
288
+ process.stdout.write(`${JSON.stringify(changed, null, 2)}\n`);
289
+ process.exit(0);
290
+ }
291
+
292
+ if (!opts.status && changed.length > 0) {
293
+ const verb = opts.removeAll || opts.removeConfig ? "Removed" : opts.sync ? "Synced" : "Updated";
294
+ installOut(`${verb} ${changed.length} file(s).`, configureToInstallOpts(opts));
295
+ }
296
+
297
+ process.exit(0);
298
+ }
@@ -0,0 +1,47 @@
1
+ /*
2
+ TTY prompts for per-target install, skip, or uninstall during interactive `configure`.
3
+ */
4
+
5
+ import { readSync } from "node:fs";
6
+ import type { CliInstallArtifactKey } from "../install/target-types.ts";
7
+
8
+ /** Human-readable labels for each install artifact key in interactive prompts. */
9
+ const LABELS: Record<CliInstallArtifactKey, string> = {
10
+ app: "App binary",
11
+ cursorSkill: "Cursor skill",
12
+ claudeSkill: "Claude skill",
13
+ codexSkill: "Codex skill",
14
+ opencodeSkill: "OpenCode skill",
15
+ openclawSkill: "OpenClaw skill",
16
+ cursorMcp: "Cursor MCP",
17
+ claudeCodeMcp: "Claude Code MCP",
18
+ claudeDesktopMcp: "Claude Desktop MCP",
19
+ opencodeMcp: "OpenCode MCP",
20
+ codexMcp: "Codex MCP",
21
+ openclawMcp: "OpenClaw MCP",
22
+ chatgptMcp: "ChatGPT desktop MCP",
23
+ configure: "App config",
24
+ };
25
+
26
+ /** Returns the prompt label for an install artifact key. */
27
+ export function artifactPromptLabel(key: CliInstallArtifactKey): string {
28
+ return LABELS[key];
29
+ }
30
+
31
+ /** User choice from a per-target Y/n or y/N prompt. */
32
+ export type TargetPromptAction = "install" | "skip" | "uninstall";
33
+
34
+ /** Prompt per target: Y/n when not installed, y/N when installed. */
35
+ export function promptTargetAction(label: string, installed: boolean): TargetPromptAction | null {
36
+ const hint = installed ? `${label} [y/N]: ` : `${label} [Y/n]: `;
37
+ process.stderr.write(hint);
38
+ const buf = Buffer.alloc(256);
39
+ const n = readSync(0, buf, { length: 256 });
40
+ const ans = buf.toString("utf8", 0, n).trim().toLowerCase();
41
+ if (installed) {
42
+ if (ans === "n") return "uninstall";
43
+ return "skip";
44
+ }
45
+ if (ans === "n") return "skip";
46
+ return "install";
47
+ }
@@ -1,3 +1,7 @@
1
+ /*
2
+ Tests for docs/api-guide module behavior.
3
+ */
4
+
1
5
  import { expect, test } from "bun:test";
2
6
  import { cliSchemaExport } from "../schema.ts";
3
7
  import type { CliProgram } from "../types.ts";
@@ -45,6 +49,7 @@ const nestedFixture: CliProgram = {
45
49
  ],
46
50
  };
47
51
 
52
+ /** Tests that generateApiGuideBody matches command section of full API guide. */
48
53
  test("generateApiGuideBody matches command section of full API guide", () => {
49
54
  const body = generateApiGuideBody(nestedFixture);
50
55
  const full = generateApiGuide(nestedFixture);
@@ -53,6 +58,7 @@ test("generateApiGuideBody matches command section of full API guide", () => {
53
58
  expect(body).not.toContain("CLI API reference");
54
59
  });
55
60
 
61
+ /** Tests that generateApiGuide covers the same command keys as cliSchemaExport. */
56
62
  test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
57
63
  const md = generateApiGuide(nestedFixture);
58
64
  const schema = cliSchemaExport(nestedFixture);
@@ -62,6 +68,7 @@ test("generateApiGuide covers the same command keys as cliSchemaExport", () => {
62
68
  expect(schema.commands?.map((c) => c.key)).toEqual(["stat"]);
63
69
  });
64
70
 
71
+ /** Tests that generateApiGuide resolves program key in install notes. */
65
72
  test("generateApiGuide resolves program key in install notes", () => {
66
73
  const fixture: CliProgram = {
67
74
  key: "myapp",
@@ -75,6 +82,7 @@ test("generateApiGuide resolves program key in install notes", () => {
75
82
  expect(md).not.toContain("Upgrade to latest release");
76
83
  });
77
84
 
85
+ /** Tests that generateApiGuide mentions Homebrew upgrade. */
78
86
  test("generateApiGuide mentions Homebrew upgrade", () => {
79
87
  const fixture: CliProgram = {
80
88
  key: "myapp",
@@ -87,6 +95,7 @@ test("generateApiGuide mentions Homebrew upgrade", () => {
87
95
  expect(md).not.toContain("install --update");
88
96
  });
89
97
 
98
+ /** Tests that generateApiGuide resolves {argsbarg:program} in consumer notes. */
90
99
  test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
91
100
  const fixture: CliProgram = {
92
101
  key: "myapp",
@@ -105,6 +114,7 @@ test("generateApiGuide resolves {argsbarg:program} in consumer notes", () => {
105
114
  expect(md).toContain("Invoke `myapp run`.");
106
115
  });
107
116
 
117
+ /** Tests that generateApiGuide and cliSchemaExport include leaf outputSchema. */
108
118
  test("generateApiGuide and cliSchemaExport include leaf outputSchema", () => {
109
119
  const fixture: CliProgram = {
110
120
  key: "myapp",
@@ -1,3 +1,5 @@
1
+ import { docsSkillTopicDescription } from "../builtins/configure-copy.ts";
2
+ import { resolveCapabilities } from "../capabilities.ts";
1
3
  import {
2
4
  CliFallbackMode,
3
5
  type CliLeaf,
@@ -75,11 +77,7 @@ export function cliBuiltinDocsGroup(program: CliProgram): CliRouter {
75
77
  leaves.push(
76
78
  docsLeaf(program, "schema", "Print the full command tree as JSON."),
77
79
  docsLeaf(program, "api", "Print the full command reference as markdown."),
78
- docsLeaf(
79
- program,
80
- "skill",
81
- "Print a reference agent SKILL, use `install --skill` for optimized.",
82
- ),
80
+ docsLeaf(program, "skill", docsSkillTopicDescription(program, resolveCapabilities(program))),
83
81
  );
84
82
 
85
83
  return {
@@ -1,3 +1,7 @@
1
+ /*
2
+ Tests for docs/docs module behavior.
3
+ */
4
+
1
5
  import { afterEach, beforeEach, expect, test } from "bun:test";
2
6
  import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
7
  import { tmpdir } from "node:os";
@@ -49,6 +53,7 @@ function docsFixture(mcp = true): CliProgram {
49
53
  };
50
54
  }
51
55
 
56
+ /** Docs reserved when enabled. */
52
57
  test("docs reserved when enabled", () => {
53
58
  const root: CliProgram = {
54
59
  ...docsFixture(),
@@ -63,6 +68,7 @@ test("docs reserved when enabled", () => {
63
68
  expect(() => cliValidateProgram(root)).toThrow(/Reserved command name: docs/);
64
69
  });
65
70
 
71
+ /** Docs rejects reserved topic keys. */
66
72
  test("docs rejects reserved topic keys", () => {
67
73
  const root = docsFixture();
68
74
  const docs = root.docs;
@@ -77,22 +83,26 @@ test("docs rejects reserved topic keys", () => {
77
83
  expect(() => cliValidateProgram(root)).toThrow(/reserved/);
78
84
  });
79
85
 
86
+ /** DocsEffectiveDefaultTopic uses first topic key. */
80
87
  test("docsEffectiveDefaultTopic uses first topic key", () => {
81
88
  expect(docsEffectiveDefaultTopic(docsFixture().docs!)).toBe("readme");
82
89
  });
83
90
 
91
+ /** Bare docs prints first topic via Cli.invoke. */
84
92
  test("bare docs prints first topic via Cli.invoke", async () => {
85
93
  const result = await new Cli(docsFixture()).invoke(["docs"]);
86
94
  expect(result.exitCode).toBe(0);
87
95
  expect(result.stdout).toContain("Hello README");
88
96
  });
89
97
 
98
+ /** Docs readme prints bundled text. */
90
99
  test("docs readme prints bundled text", async () => {
91
100
  const result = await new Cli(docsFixture()).invoke(["docs", "readme"]);
92
101
  expect(result.exitCode).toBe(0);
93
102
  expect(result.stdout).toContain("Hello README");
94
103
  });
95
104
 
105
+ /** Docs defaultTopic override. */
96
106
  test("docs defaultTopic override", async () => {
97
107
  const root = docsFixture();
98
108
  root.docs!.defaultTopic = "arch";
@@ -100,20 +110,23 @@ test("docs defaultTopic override", async () => {
100
110
  expect(result.stdout).toContain("Architecture");
101
111
  });
102
112
 
113
+ /** Docs mcp when MCP enabled. */
103
114
  test("docs mcp when MCP enabled", async () => {
104
115
  const result = await new Cli(docsFixture(true)).invoke(["docs", "mcp"]);
105
116
  expect(result.exitCode).toBe(0);
106
117
  expect(result.stdout).toContain("MCP server (myapp)");
107
118
  expect(result.stdout).toContain("myapp mcp");
108
119
  expect(result.stdout).toContain("claude_desktop_config.json");
109
- expect(result.stdout).toContain("install --mcp --yes");
120
+ expect(result.stdout).toContain("configure --sync --yes");
110
121
  });
111
122
 
123
+ /** Docs rejects unknown subcommand. */
112
124
  test("docs rejects unknown subcommand", async () => {
113
125
  const result = await new Cli(docsFixture()).invoke(["docs", "all"]);
114
126
  expect(result.exitCode).not.toBe(0);
115
127
  });
116
128
 
129
+ /** Docs mcp absent from router when MCP disabled. */
117
130
  test("docs mcp absent from router when MCP disabled", async () => {
118
131
  const root = docsFixture(false);
119
132
  const presentation = cliPresentationRoot(root);
@@ -126,6 +139,7 @@ test("docs mcp absent from router when MCP disabled", async () => {
126
139
  expect(result.exitCode).not.toBe(0);
127
140
  });
128
141
 
142
+ /** Presentation includes docs subtree. */
129
143
  test("presentation includes docs subtree", () => {
130
144
  const presentation = cliPresentationRoot(docsFixture());
131
145
  const docsNode = presentation.commands.find((c) => c.key === "docs");
@@ -135,6 +149,7 @@ test("presentation includes docs subtree", () => {
135
149
  ).toBe(true);
136
150
  });
137
151
 
152
+ /** Docs schema prints JSON. */
138
153
  test("docs schema prints JSON", async () => {
139
154
  const result = await new Cli(docsFixture()).invoke(["docs", "schema"]);
140
155
  expect(result.exitCode).toBe(0);
@@ -143,6 +158,7 @@ test("docs schema prints JSON", async () => {
143
158
  expect(schema.commands.some((c: { key: string }) => c.key === "run")).toBe(true);
144
159
  });
145
160
 
161
+ /** Docs api prints markdown reference. */
146
162
  test("docs api prints markdown reference", async () => {
147
163
  const result = await new Cli(docsFixture()).invoke(["docs", "api"]);
148
164
  expect(result.exitCode).toBe(0);
@@ -152,6 +168,7 @@ test("docs api prints markdown reference", async () => {
152
168
  expect(result.stdout).toContain("myapp docs schema");
153
169
  });
154
170
 
171
+ /** Tests that skipsRequiredAppConfigExit includes docs and config builtins. */
155
172
  test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
156
173
  const program = {
157
174
  ...docsFixture(),
@@ -165,6 +182,7 @@ test("skipsRequiredAppConfigExit includes docs and config builtins", () => {
165
182
  expect(skipsRequiredAppConfigExit(["run"], caps)).toBe(false);
166
183
  });
167
184
 
185
+ /** Docs skill prints Cursor SKILL.md. */
168
186
  test("docs skill prints Cursor SKILL.md", async () => {
169
187
  const result = await new Cli(docsFixture()).invoke(["docs", "skill"]);
170
188
  expect(result.exitCode).toBe(0);
@@ -176,20 +194,22 @@ test("docs skill prints Cursor SKILL.md", async () => {
176
194
  expect(result.stdout).not.toContain("mcp.json");
177
195
  });
178
196
 
179
- test("docs skill help recommends install --skill", async () => {
197
+ /** Docs skill help recommends configure. */
198
+ test("docs skill help recommends configure", async () => {
180
199
  const presentation = cliPresentationRoot(docsFixture());
181
200
  const docsNode = presentation.commands.find((c) => c.key === "docs");
182
201
  expect(docsNode && "commands" in docsNode).toBe(true);
183
202
  if (docsNode && "commands" in docsNode) {
184
203
  const skill = docsNode.commands.find((c) => c.key === "skill");
185
204
  expect(skill?.description).toContain("reference agent SKILL");
186
- expect(skill?.description).toContain("install --skill");
205
+ expect(skill?.description).toContain("configure");
187
206
  expect(skill?.notes).toBeUndefined();
188
207
  expect(docsNode.notes).toContain("--save");
189
208
  expect(docsNode.notes).not.toContain("install --skill");
190
209
  }
191
210
  });
192
211
 
212
+ /** Presentation includes docs schema and skill. */
193
213
  test("presentation includes docs schema and skill", () => {
194
214
  const presentation = cliPresentationRoot(docsFixture());
195
215
  const docsNode = presentation.commands.find((c) => c.key === "docs");
@@ -201,6 +221,7 @@ test("presentation includes docs schema and skill", () => {
201
221
  }
202
222
  });
203
223
 
224
+ /** Completions offer docs subcommands. */
204
225
  test("completions offer docs subcommands", () => {
205
226
  const bash = completionBashScript(cliPresentationRoot(docsFixture()));
206
227
  expect(bash).toContain("docs) echo");
@@ -210,19 +231,21 @@ test("completions offer docs subcommands", () => {
210
231
  expect(bash).toContain("skill) echo");
211
232
  });
212
233
 
213
- test("generateMcpGuide includes schema URI and install targets", () => {
234
+ /** Tests that generateMcpGuide includes schema URI and configure sync. */
235
+ test("generateMcpGuide includes schema URI and configure sync", () => {
214
236
  const guide = generateMcpGuide(docsFixture(true));
215
237
  expect(guide).toContain("myapp://schema");
216
238
  expect(guide).toContain("~/.cursor/mcp.json");
217
239
  expect(guide).toContain("claude_desktop_config.json");
218
240
  expect(guide).toContain("## Installation");
219
241
  expect(guide).toContain("## Running directly");
220
- expect(guide).toContain("install --mcp");
242
+ expect(guide).toContain("configure --sync");
221
243
  expect(guide).toContain("brew install myapp");
222
244
  expect(guide).toContain("OpenAI Codex");
223
245
  expect(guide).toContain("ChatGPT");
224
246
  });
225
247
 
248
+ /** Docs --save writes topic file. */
226
249
  test("docs --save writes topic file", async () => {
227
250
  const result = await new Cli(docsFixture()).invoke(["docs", "readme", "--save"]);
228
251
  expect(result.exitCode).toBe(0);
@@ -232,6 +255,7 @@ test("docs --save writes topic file", async () => {
232
255
  expect(text).not.toContain("Generated by");
233
256
  });
234
257
 
258
+ /** Docs api --save prepends generated hint. */
235
259
  test("docs api --save prepends generated hint", async () => {
236
260
  const result = await new Cli(docsFixture()).invoke(["docs", "api", "--save"]);
237
261
  expect(result.exitCode).toBe(0);
@@ -242,6 +266,7 @@ test("docs api --save prepends generated hint", async () => {
242
266
  expect(text).toContain("CLI API reference");
243
267
  });
244
268
 
269
+ /** Docs skill --save keeps frontmatter first. */
245
270
  test("docs skill --save keeps frontmatter first", async () => {
246
271
  const result = await new Cli(docsFixture()).invoke(["docs", "skill", "--save"]);
247
272
  expect(result.exitCode).toBe(0);
@@ -252,6 +277,7 @@ test("docs skill --save keeps frontmatter first", async () => {
252
277
  expect(text.indexOf(hint)).toBeGreaterThan(text.indexOf("---\n", 4));
253
278
  });
254
279
 
280
+ /** Docs schema --save writes JSON file. */
255
281
  test("docs schema --save writes JSON file", async () => {
256
282
  const result = await new Cli(docsFixture()).invoke(["docs", "schema", "--save"]);
257
283
  expect(result.exitCode).toBe(0);
@@ -262,6 +288,7 @@ test("docs schema --save writes JSON file", async () => {
262
288
  expect(schema.key).toBe("myapp");
263
289
  });
264
290
 
291
+ /** Tests that saveDocsTopic returns relative path. */
265
292
  test("saveDocsTopic returns relative path", () => {
266
293
  const path = saveDocsTopic(docsFixture(), "api");
267
294
  expect(path).toBe("docs/api.md");
@@ -105,11 +105,11 @@ export function generateMcpGuide(root: CliProgram): string {
105
105
  "",
106
106
  "## Installation",
107
107
  "",
108
- "### `install --mcp`",
108
+ "### `configure`",
109
109
  "",
110
110
  ];
111
111
 
112
- if (caps.install) {
112
+ if (caps.configure) {
113
113
  lines.push(
114
114
  `Install the CLI first so \`${root.key}\` is on your PATH (e.g. \`brew install ${root.key}\`). Host configs reference the app by name.`,
115
115
  "",
@@ -123,7 +123,7 @@ export function generateMcpGuide(root: CliProgram): string {
123
123
 
124
124
  lines.push(
125
125
  "```bash",
126
- `${root.key} install --mcp --yes`,
126
+ `${root.key} configure --sync --yes`,
127
127
  "```",
128
128
  "",
129
129
  "Merges the server entry below into host config when each host is present:",
@@ -177,7 +177,7 @@ export function generateMcpGuide(root: CliProgram): string {
177
177
  if (root.appConfig?.entries && Object.keys(root.appConfig.entries).length > 0) {
178
178
  lines.push("## Configuration", "");
179
179
  lines.push(
180
- `Configure before first use in Cursor or Claude Desktop (MCP hosts are non-interactive): \`${root.key} install --configure\`.`,
180
+ `Configure before first use in Cursor or Claude Desktop (MCP hosts are non-interactive): \`${root.key} configure\`.`,
181
181
  "",
182
182
  `Default config file: \`${displayAppConfigPath(root)}\` (flat JSON keys).`,
183
183
  "",