argsbarg 4.1.0 → 4.1.1

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 (103) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/README.md +90 -84
  3. package/docs/README.md +6 -6
  4. package/docs/bundled-docs.md +1 -1
  5. package/docs/cli-program.md +5 -3
  6. package/docs/config-schema.md +36 -9
  7. package/docs/developing.md +7 -7
  8. package/docs/distribution-homebrew.md +103 -0
  9. package/docs/install.md +105 -189
  10. package/docs/mcp.md +2 -3
  11. package/docs/output-schema.md +1 -1
  12. package/examples/full-example/Formula/.gitkeep +0 -0
  13. package/examples/full-example/README.md +98 -0
  14. package/examples/full-example/biome.json +22 -0
  15. package/examples/{consumer-app → full-example}/bun.lock +2 -0
  16. package/examples/full-example/justfile +134 -0
  17. package/examples/{consumer-app → full-example}/package.json +10 -3
  18. package/examples/{consumer-app → full-example}/schemas/generated/app-config.json +1 -1
  19. package/examples/{consumer-app → full-example}/schemas/generated/status.json +1 -1
  20. package/examples/full-example/scripts/create-identity.ts +11 -0
  21. package/examples/full-example/scripts/formula-shared.ts +73 -0
  22. package/examples/full-example/scripts/gen-dev-formula.ts +26 -0
  23. package/examples/full-example/scripts/print-identity.ts +27 -0
  24. package/examples/full-example/src/commands/echo/command.ts +21 -0
  25. package/examples/full-example/src/commands/status/command.test.ts +10 -0
  26. package/examples/full-example/src/commands/status/command.ts +36 -0
  27. package/examples/{consumer-app → full-example}/src/commands/status/types.ts +1 -1
  28. package/examples/full-example/src/index.ts +10 -0
  29. package/examples/full-example/src/program.ts +57 -0
  30. package/examples/{consumer-app → full-example}/src/types.ts +1 -1
  31. package/examples/nested.ts +1 -3
  32. package/index.d.ts +27 -66
  33. package/package.json +2 -2
  34. package/src/builtins/builtins.test.ts +22 -23
  35. package/src/builtins/completion-group.ts +17 -15
  36. package/src/builtins/dispatch.ts +25 -1
  37. package/src/builtins/install.ts +22 -52
  38. package/src/builtins/registry.ts +2 -0
  39. package/src/builtins/uninstall.ts +80 -0
  40. package/src/capabilities.ts +1 -3
  41. package/src/cli-tool/cli-smoke.test.ts +19 -0
  42. package/src/cli-tool/create.test.ts +119 -0
  43. package/src/cli-tool/create.ts +380 -0
  44. package/{examples/consumer-app/capabilities.test.ts → src/cli-tool/full-example-capabilities.test.ts} +15 -14
  45. package/src/cli-tool/main.ts +8 -0
  46. package/src/cli-tool/post-create.ts +111 -0
  47. package/src/cli-tool/program.ts +82 -0
  48. package/src/cli-tool/prompt.ts +28 -0
  49. package/src/cli-tool/run-create.ts +149 -0
  50. package/src/cli.ts +0 -2
  51. package/src/config/bootstrap.ts +16 -9
  52. package/src/config/resolve.test.ts +167 -0
  53. package/src/config/resolve.ts +50 -6
  54. package/src/docs/api-guide.test.ts +4 -5
  55. package/src/docs/docs.test.ts +2 -1
  56. package/src/docs/mcp-guide.ts +7 -8
  57. package/src/index.ts +3 -10
  58. package/src/install/binary-placement.test.ts +101 -0
  59. package/src/install/binary-placement.ts +47 -0
  60. package/src/install/index.ts +117 -123
  61. package/src/install/install.test.ts +89 -105
  62. package/src/install/normalize-uninstall.ts +11 -0
  63. package/src/install/normalize.ts +4 -19
  64. package/src/install/paths.ts +0 -22
  65. package/src/install/plan.ts +14 -6
  66. package/src/install/shell.ts +0 -14
  67. package/src/install/status.test.ts +6 -6
  68. package/src/install/status.ts +0 -6
  69. package/src/install/target-effective.ts +0 -2
  70. package/src/install/target-scope.ts +15 -28
  71. package/src/install/target-types.ts +0 -16
  72. package/src/install/targets/app.ts +19 -28
  73. package/src/install/targets/configure.ts +5 -1
  74. package/src/install/targets/index.ts +0 -3
  75. package/src/install/targets.test.ts +24 -42
  76. package/src/mcp/env.test.ts +92 -0
  77. package/src/mcp/env.ts +15 -14
  78. package/src/parse.test.ts +3 -2
  79. package/src/prompt.ts +10 -0
  80. package/src/schema.ts +9 -1
  81. package/src/types.ts +27 -9
  82. package/src/validate.ts +5 -11
  83. package/docs/templates/cursor/rules/cli-program.mdc +0 -31
  84. package/examples/config-app/main.ts +0 -20
  85. package/examples/config-app/program.ts +0 -78
  86. package/examples/config-app/schema.ts +0 -37
  87. package/examples/config-app/types.ts +0 -19
  88. package/examples/consumer-app/README.md +0 -56
  89. package/examples/consumer-app/src/main.ts +0 -15
  90. package/examples/consumer-app/src/program.ts +0 -108
  91. package/src/install/app.ts +0 -94
  92. package/src/install/bootstrap.ts +0 -22
  93. package/src/install/completions.ts +0 -56
  94. package/src/install/targets/completions.ts +0 -133
  95. package/src/install/update.test.ts +0 -123
  96. package/src/install/update.ts +0 -54
  97. /package/examples/{consumer-app → full-example}/schemas/configSchemas.ts +0 -0
  98. /package/examples/{consumer-app → full-example}/schemas/outputSchemas.ts +0 -0
  99. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.test.ts +0 -0
  100. /package/examples/{consumer-app → full-example}/scripts/schemagen/discover-schema-roots.ts +0 -0
  101. /package/examples/{consumer-app → full-example}/scripts/schemagen/naming.ts +0 -0
  102. /package/examples/{consumer-app → full-example}/scripts/schemagen.ts +0 -0
  103. /package/examples/{consumer-app → full-example}/tsconfig.json +0 -0
@@ -1,20 +1,24 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { writeAppConfigFile } from "../config/file.ts";
6
6
  import type { CliProgram } from "../types.ts";
7
7
  import { detectInstalledArtifacts } from "./detect-installed.ts";
8
8
  import {
9
+ cliInstall,
9
10
  interactiveSelectionAssumesApp,
10
11
  mergeInteractiveSelection,
11
12
  parseInstallOpts,
12
13
  runInstallMutation,
14
+ runUninstallMutation,
13
15
  validateInstallOpts,
16
+ validateUninstallOpts,
14
17
  } from "./index.ts";
15
18
  import { opencodeConfigDir } from "./mcp-opencode.ts";
19
+ import { normalizeUninstallRawOpts } from "./normalize-uninstall.ts";
16
20
  import { resolveClaudeDesktopMcpPath, resolveInstallPaths } from "./paths.ts";
17
- import { buildInstallPlan } from "./plan.ts";
21
+ import { buildInstallPlan, buildUpdatePlan } from "./plan.ts";
18
22
  import { printInstallStatus, writeInteractiveInstallIntro } from "./status.ts";
19
23
  import { buildUninstallPlan } from "./uninstall.ts";
20
24
 
@@ -28,7 +32,6 @@ const fixture: CliProgram = {
28
32
 
29
33
  let home: string;
30
34
  let prevHome: string | undefined;
31
-
32
35
  let prevXdg: string | undefined;
33
36
 
34
37
  beforeEach(() => {
@@ -48,16 +51,10 @@ afterEach(() => {
48
51
  });
49
52
 
50
53
  describe("install paths", () => {
51
- test("resolveInstallPaths uses ~/.local/bin for app", () => {
54
+ test("resolveInstallPaths includes skill and mcp paths", () => {
52
55
  const paths = resolveInstallPaths(fixture);
53
- expect(paths.appPath).toBe(join(home, ".local", "bin", "testapp"));
54
- });
55
-
56
- test("fish completion uses XDG_CONFIG_HOME", () => {
57
- const xdg = join(home, "xdg");
58
- process.env.XDG_CONFIG_HOME = xdg;
59
- const paths = resolveInstallPaths(fixture);
60
- expect(paths.fishCompletion).toBe(join(xdg, "fish", "completions", "testapp.fish"));
56
+ expect(paths.cursorSkillDir).toContain(".cursor/skills");
57
+ expect(paths.cursorMcpPath).toContain("mcp.json");
61
58
  });
62
59
 
63
60
  test("claude desktop mcp path on darwin", () => {
@@ -74,38 +71,39 @@ describe("install paths", () => {
74
71
  });
75
72
 
76
73
  describe("detect installed", () => {
77
- test("detects app and completions", () => {
74
+ test("detects cursor mcp when configured", () => {
78
75
  const paths = resolveInstallPaths(fixture);
79
- mkdirSync(paths.appDir, { recursive: true });
80
- writeFileSync(paths.appPath, "fake", "utf8");
81
- mkdirSync(join(home, ".bash_completion.d"), { recursive: true });
82
- writeFileSync(paths.bashCompletion, "# bash", "utf8");
76
+ mkdirSync(join(home, ".cursor"), { recursive: true });
77
+ writeFileSync(
78
+ paths.cursorMcpPath,
79
+ JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
80
+ "utf8",
81
+ );
83
82
 
84
83
  const detected = detectInstalledArtifacts(paths, fixture);
85
- expect(detected.app).toBe(true);
86
- expect(detected.bashCompletion).toBe(true);
87
- expect(detected.zshCompletion).toBe(false);
84
+ expect(detected.cursorMcp).toBe(true);
85
+ expect(detected.app).toBe(false);
88
86
  });
89
87
  });
90
88
 
91
89
  describe("install plan", () => {
92
- test("buildInstallPlan --all includes app", () => {
90
+ test("buildInstallPlan --all omits app self-install", () => {
93
91
  const paths = resolveInstallPaths(fixture);
94
92
  const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
95
- expect(plan.some((a) => a.kind === "app")).toBe(true);
93
+ expect(plan.some((a) => a.kind === "app")).toBe(false);
94
+ expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
96
95
  });
97
96
 
98
- test("buildInstallPlan --mcp --yes also includes app", () => {
97
+ test("buildInstallPlan --mcp without yes omits implicit app", () => {
99
98
  const paths = resolveInstallPaths(fixture);
100
- const opts = parseInstallOpts({ mcp: "1", yes: "1" });
101
- validateInstallOpts(opts);
102
- const plan = buildInstallPlan(fixture, paths, opts);
103
- expect(plan.some((a) => a.kind === "app")).toBe(true);
99
+ const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
100
+ expect(plan.some((a) => a.kind === "app")).toBe(false);
104
101
  });
105
102
 
106
- test("buildInstallPlan --mcp without yes omits app", () => {
103
+ test("buildUpdatePlan greenfield falls back to install-all", () => {
107
104
  const paths = resolveInstallPaths(fixture);
108
- const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
105
+ const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
106
+ expect(plan.length).toBeGreaterThan(0);
109
107
  expect(plan.some((a) => a.kind === "app")).toBe(false);
110
108
  });
111
109
 
@@ -130,12 +128,6 @@ describe("install plan", () => {
130
128
  expect(plan.some((a) => a.kind === "claude-mcp")).toBe(true);
131
129
  });
132
130
 
133
- test("buildInstallPlan --mcp skips claude desktop without app data", () => {
134
- const paths = resolveInstallPaths(fixture);
135
- const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
136
- expect(plan.some((a) => a.kind === "claude-desktop-mcp")).toBe(false);
137
- });
138
-
139
131
  test("buildInstallPlan --mcp includes opencode when config dir exists", () => {
140
132
  const paths = resolveInstallPaths(fixture);
141
133
  mkdirSync(opencodeConfigDir(home), { recursive: true });
@@ -143,21 +135,37 @@ describe("install plan", () => {
143
135
  expect(plan.some((a) => a.kind === "opencode-mcp")).toBe(true);
144
136
  });
145
137
 
146
- test("buildInstallPlan --mcp includes chatgpt when app data exists", () => {
147
- const paths = resolveInstallPaths(fixture);
148
- if (process.platform === "darwin") {
149
- mkdirSync(join(home, "Library", "Application Support", "ChatGPT"), { recursive: true });
150
- const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
151
- expect(plan.some((a) => a.kind === "chatgpt-desktop-mcp")).toBe(true);
152
- }
138
+ test("buildInstallPlan --mcp --skill --configure is not empty when agents exist", () => {
139
+ const program: CliProgram = {
140
+ ...fixture,
141
+ install: { agentIntegration: "both" },
142
+ appConfig: {
143
+ entries: { token: { description: "API token." } },
144
+ },
145
+ };
146
+ const paths = resolveInstallPaths(program);
147
+ mkdirSync(join(home, ".cursor"), { recursive: true });
148
+ mkdirSync(join(home, ".claude"), { recursive: true });
149
+ const plan = buildInstallPlan(
150
+ program,
151
+ paths,
152
+ parseInstallOpts({ mcp: "1", skill: "1", configure: "1" }),
153
+ );
154
+ expect(plan.length).toBeGreaterThan(0);
155
+ expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
156
+ expect(plan.some((a) => a.kind.endsWith("-skill"))).toBe(true);
153
157
  });
154
158
  });
155
159
 
156
160
  describe("install status", () => {
157
- test("printInstallStatus human output", () => {
161
+ test("printInstallStatus human output with MCP", () => {
158
162
  const paths = resolveInstallPaths(fixture);
159
- mkdirSync(paths.appDir, { recursive: true });
160
- writeFileSync(paths.appPath, "fake", "utf8");
163
+ mkdirSync(join(home, ".cursor"), { recursive: true });
164
+ writeFileSync(
165
+ paths.cursorMcpPath,
166
+ JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
167
+ "utf8",
168
+ );
161
169
 
162
170
  const chunks: string[] = [];
163
171
  const orig = process.stdout.write;
@@ -169,7 +177,7 @@ describe("install status", () => {
169
177
  printInstallStatus(fixture, {});
170
178
  const out = chunks.join("");
171
179
  expect(out).toContain("Installed artifacts for testapp");
172
- expect(out).toContain("~/.local/bin/testapp");
180
+ expect(out).toContain("cursor mcp");
173
181
  } finally {
174
182
  process.stdout.write = orig;
175
183
  }
@@ -207,17 +215,9 @@ describe("interactive selection", () => {
207
215
  expect(mergeInteractiveSelection([2, 3], 5, false)).toEqual([2, 3]);
208
216
  });
209
217
 
210
- test("interactiveSelectionAssumesApp when install plan starts with app", () => {
218
+ test("interactiveSelectionAssumesApp is always false", () => {
211
219
  const paths = resolveInstallPaths(fixture);
212
220
  const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
213
- expect(interactiveSelectionAssumesApp(plan, false)).toBe(true);
214
- expect(interactiveSelectionAssumesApp(plan, true)).toBe(false);
215
- });
216
-
217
- test("interactiveSelectionAssumesApp false when app not in plan", () => {
218
- const paths = resolveInstallPaths(fixture);
219
- const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ completions: "1" }));
220
- expect(plan.some((a) => a.kind === "app")).toBe(false);
221
221
  expect(interactiveSelectionAssumesApp(plan, false)).toBe(false);
222
222
  });
223
223
  });
@@ -239,34 +239,34 @@ describe("install output", () => {
239
239
  });
240
240
  });
241
241
 
242
- describe("parseInstallOpts", () => {
243
- test("json implies yes in validate path via cliInstall", () => {
244
- const opts = parseInstallOpts({ json: "1", all: "1" });
245
- expect(opts.json).toBe(true);
242
+ describe("validateUninstallOpts", () => {
243
+ test("bare uninstall normalizes to --all", () => {
244
+ const opts = parseInstallOpts(normalizeUninstallRawOpts({ yes: "1" }));
245
+ expect(validateUninstallOpts(opts)).toBeNull();
246
246
  expect(opts.all).toBe(true);
247
247
  });
248
- });
249
-
250
- describe("validateInstallOpts", () => {
251
- test("uninstall requires a target flag", () => {
252
- const opts = parseInstallOpts({ uninstall: "1", yes: "1" });
253
- expect(validateInstallOpts(opts)).toContain("Specify at least one target");
254
- });
255
248
 
256
249
  test("uninstall allows --all", () => {
257
250
  const opts = parseInstallOpts({ uninstall: "1", all: "1", yes: "1" });
258
- expect(validateInstallOpts(opts)).toBeNull();
251
+ expect(validateUninstallOpts(opts)).toBeNull();
259
252
  });
260
253
 
261
254
  test("uninstall rejects --reinstall", () => {
262
255
  const opts = parseInstallOpts({ uninstall: "1", reinstall: "1", all: "1" });
263
- expect(validateInstallOpts(opts)).toContain("--reinstall");
256
+ expect(validateUninstallOpts(opts)).toContain("--reinstall");
257
+ });
258
+ });
259
+
260
+ describe("validateInstallOpts", () => {
261
+ test("install --uninstall is rejected", () => {
262
+ const opts = parseInstallOpts({ uninstall: "1", all: "1", yes: "1" });
263
+ expect(validateInstallOpts(opts)).toContain("uninstall");
264
264
  });
265
265
  });
266
266
 
267
267
  describe("install mutation", () => {
268
268
  test("uninstall --all with nothing installed succeeds", async () => {
269
- const result = await runInstallMutation(fixture, { uninstall: "1", all: "1", yes: "1" });
269
+ const result = await runUninstallMutation(fixture, { all: "1", yes: "1" });
270
270
  expect(result.changed).toEqual([]);
271
271
  });
272
272
 
@@ -277,29 +277,30 @@ describe("install mutation", () => {
277
277
  ).toBe(true);
278
278
  });
279
279
 
280
- test("uninstall --all removes detected app", async () => {
281
- const paths = resolveInstallPaths(fixture);
282
- mkdirSync(paths.appDir, { recursive: true });
283
- writeFileSync(paths.appPath, "fake", "utf8");
284
-
285
- const result = await runInstallMutation(fixture, { uninstall: "1", all: "1", yes: "1" });
286
- expect(result.changed).toContain(paths.appPath);
287
- expect(existsSync(paths.appPath)).toBe(false);
288
- expect(existsSync(paths.appDir)).toBe(true);
280
+ test("install --all --yes dry-run succeeds when stdin is not a TTY", async () => {
281
+ const program: CliProgram = {
282
+ ...fixture,
283
+ appConfig: {
284
+ entries: { token: { description: "API token." } },
285
+ },
286
+ };
287
+ const origIsTTY = process.stdin.isTTY;
288
+ Object.defineProperty(process.stdin, "isTTY", { value: false, configurable: true });
289
+ const exitCode = await new Promise<number>((resolve) => {
290
+ const origExit = process.exit;
291
+ process.exit = ((code?: number) => {
292
+ process.exit = origExit;
293
+ resolve(code ?? 0);
294
+ }) as typeof process.exit;
295
+ void cliInstall(program, { all: "1", yes: "1", dry: "1" });
296
+ });
297
+ Object.defineProperty(process.stdin, "isTTY", { value: origIsTTY, configurable: true });
298
+ expect(exitCode).toBe(0);
289
299
  });
290
300
  });
291
301
 
292
302
  describe("uninstall plan", () => {
293
- test("buildUninstallPlan --all scopes like install --all", () => {
294
- const paths = resolveInstallPaths(fixture);
295
- mkdirSync(paths.appDir, { recursive: true });
296
- writeFileSync(paths.appPath, "fake", "utf8");
297
-
298
- const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
299
- expect(plan.some((a) => a.summary.startsWith("app:"))).toBe(true);
300
- });
301
-
302
- test("buildUninstallPlan --all runs configure last", () => {
303
+ test("buildUninstallPlan --all runs configure last when config exists", () => {
303
304
  const program: CliProgram = {
304
305
  ...fixture,
305
306
  appConfig: {
@@ -307,27 +308,10 @@ describe("uninstall plan", () => {
307
308
  },
308
309
  };
309
310
  const paths = resolveInstallPaths(program);
310
- mkdirSync(paths.appDir, { recursive: true });
311
- writeFileSync(paths.appPath, "fake", "utf8");
312
311
  writeAppConfigFile(program, { token: "secret" });
313
312
 
314
313
  const plan = buildUninstallPlan(program, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
315
- const appIdx = plan.findIndex((a) => a.summary.startsWith("app:"));
316
314
  const configureIdx = plan.findIndex((a) => a.summary.startsWith("app config:"));
317
- expect(appIdx).toBeGreaterThanOrEqual(0);
318
- expect(configureIdx).toBeGreaterThan(appIdx);
319
315
  expect(configureIdx).toBe(plan.length - 1);
320
316
  });
321
-
322
- test("buildUninstallPlan --app ignores completions", () => {
323
- const paths = resolveInstallPaths(fixture);
324
- mkdirSync(paths.appDir, { recursive: true });
325
- writeFileSync(paths.appPath, "fake", "utf8");
326
- mkdirSync(join(home, ".bash_completion.d"), { recursive: true });
327
- writeFileSync(paths.bashCompletion, "# bash", "utf8");
328
-
329
- const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", app: "1" }));
330
- expect(plan).toHaveLength(1);
331
- expect(plan[0]?.summary.startsWith("app:")).toBe(true);
332
- });
333
317
  });
@@ -0,0 +1,11 @@
1
+ /** Normalizes bare `uninstall` to `--all`. */
2
+ export function normalizeUninstallRawOpts(raw: Record<string, string>): Record<string, string> {
3
+ const flag = (name: string) => raw[name] === "1";
4
+ const out: Record<string, string> = { ...raw, uninstall: "1" };
5
+
6
+ const hasUninstallTarget = flag("all") || flag("skill") || flag("mcp") || flag("configure");
7
+ if (!hasUninstallTarget) {
8
+ out.all = "1";
9
+ }
10
+ return out;
11
+ }
@@ -1,30 +1,15 @@
1
- /** Normalizes bare `install` / `install --uninstall` to `--all`. */
1
+ /** Normalizes bare `install` to `--all`. */
2
2
  export function normalizeInstallRawOpts(raw: Record<string, string>): Record<string, string> {
3
3
  const flag = (name: string) => raw[name] === "1";
4
4
  const out = { ...raw };
5
5
 
6
- if (flag("status") || flag("reinstall") || flag("update")) {
6
+ if (flag("status") || flag("reinstall")) {
7
7
  return out;
8
8
  }
9
9
 
10
- const hasInstallTarget =
11
- flag("all") || flag("app") || flag("completions") || flag("skill") || flag("mcp");
12
- const hasUninstallTarget =
13
- flag("all") ||
14
- flag("app") ||
15
- flag("completions") ||
16
- flag("skill") ||
17
- flag("mcp") ||
18
- flag("configure");
10
+ const hasInstallTarget = flag("all") || flag("skill") || flag("mcp");
19
11
 
20
- if (flag("configure") && !flag("uninstall") && !hasInstallTarget) {
21
- return out;
22
- }
23
-
24
- if (flag("uninstall")) {
25
- if (!hasUninstallTarget) {
26
- out.all = "1";
27
- }
12
+ if (flag("configure") && !hasInstallTarget) {
28
13
  return out;
29
14
  }
30
15
 
@@ -8,11 +8,6 @@ import { resolveOpenclawConfigPath } from "./mcp-openclaw.ts";
8
8
  import { resolveOpenCodeConfigPathForInstall } from "./mcp-opencode.ts";
9
9
 
10
10
  export interface InstallPaths {
11
- appDir: string;
12
- appPath: string;
13
- bashCompletion: string;
14
- zshCompletion: string;
15
- fishCompletion: string;
16
11
  cursorSkillDir: string;
17
12
  claudeSkillDir: string;
18
13
  codexSkillDir: string;
@@ -25,8 +20,6 @@ export interface InstallPaths {
25
20
  chatGptMcpPath: string;
26
21
  codexConfigPath: string;
27
22
  openclawConfigPath: string;
28
- bashRc: string;
29
- zshRc: string;
30
23
  mcpName: string;
31
24
  skillDirName: string;
32
25
  }
@@ -38,11 +31,6 @@ export function displayInstallPath(path: string): string {
38
31
  return displayHomePath(path);
39
32
  }
40
33
 
41
- /** App install directory (`~/.local/bin`). */
42
- export function resolveAppDir(): string {
43
- return join(userHome(), ".local", "bin");
44
- }
45
-
46
34
  /** Resolves Claude Desktop `claude_desktop_config.json` for the current OS. */
47
35
  export function resolveClaudeDesktopMcpPath(home: string): string {
48
36
  if (process.platform === "darwin") {
@@ -80,22 +68,14 @@ export function chatGptDesktopPresent(_home: string, configPath: string): boolea
80
68
  /** Resolves all install artifact paths for a program root. */
81
69
  export function resolveInstallPaths(root: CliProgram): InstallPaths {
82
70
  const home = userHome();
83
- const appDir = resolveAppDir();
84
- const key = root.key;
85
71
  const skillDirName = sanitizeToolSegment(root.key);
86
72
  const codexSlug = skillDirNameForTarget(root.key, "codex");
87
73
  const opencodeSlug = skillDirNameForTarget(root.key, "opencode");
88
74
  const openclawSlug = skillDirNameForTarget(root.key, "openclaw");
89
- const xdgConfig = xdgConfigHome(home);
90
75
  const claudeDesktopMcpPath = resolveClaudeDesktopMcpPath(home);
91
76
  const chatGptMcpPath = resolveChatGptMcpPath(home);
92
77
 
93
78
  return {
94
- appDir,
95
- appPath: join(appDir, key),
96
- bashCompletion: join(home, ".bash_completion.d", key),
97
- zshCompletion: join(home, ".zsh", "completions", `_${key}`),
98
- fishCompletion: join(xdgConfig, "fish", "completions", `${key}.fish`),
99
79
  cursorSkillDir: join(home, ".cursor", "skills", skillDirName),
100
80
  claudeSkillDir: join(home, ".claude", "skills", skillDirName),
101
81
  codexSkillDir: join(home, ".codex", "skills", codexSlug),
@@ -108,8 +88,6 @@ export function resolveInstallPaths(root: CliProgram): InstallPaths {
108
88
  chatGptMcpPath,
109
89
  codexConfigPath: join(home, ".codex", "config.toml"),
110
90
  openclawConfigPath: resolveOpenclawConfigPath(home),
111
- bashRc: join(home, ".bashrc"),
112
- zshRc: join(home, ".zshrc"),
113
91
  mcpName: mcpServerId(root),
114
92
  skillDirName,
115
93
  };
@@ -15,26 +15,34 @@ export function buildInstallPlan(
15
15
  return buildInstallPlanFromTargets(root, paths, opts);
16
16
  }
17
17
 
18
- /** Builds update/reinstall actions for detected artifacts within effective targets. */
18
+ /** Builds update/reinstall actions; greenfield fallback when nothing detected. */
19
19
  export function buildUpdatePlan(
20
20
  root: CliProgram,
21
21
  paths: InstallPaths,
22
22
  opts: InstallOpts,
23
23
  ): InstallAction[] {
24
- return buildInstallPlan(root, paths, { ...opts, reinstall: true, all: true });
24
+ const refresh = buildInstallPlanFromTargets(root, paths, {
25
+ ...opts,
26
+ reinstall: true,
27
+ all: true,
28
+ });
29
+ if (refresh.length > 0) {
30
+ return refresh;
31
+ }
32
+ return buildInstallPlanFromTargets(root, paths, { ...opts, reinstall: false, all: true });
25
33
  }
26
34
 
27
35
  /** @deprecated Use resolveInstallPlanMode + shouldIncludeArtifact. */
28
- export function wantsInstallApp(opts: InstallOpts): boolean {
29
- return !!(opts.all || opts.app || opts.reinstall);
36
+ export function wantsInstallApp(_opts: InstallOpts): boolean {
37
+ return false;
30
38
  }
31
39
 
32
40
  /** @deprecated Use {@link wantsInstallApp}. */
33
41
  export const wantsInstallBin = wantsInstallApp;
34
42
 
35
43
  /** @deprecated Use resolveInstallPlanMode + shouldIncludeArtifact. */
36
- export function wantsInstallCompletions(opts: InstallOpts): boolean {
37
- return !!(opts.all || opts.completions);
44
+ export function wantsInstallCompletions(_opts: InstallOpts): boolean {
45
+ return false;
38
46
  }
39
47
 
40
48
  /** @deprecated Use resolveInstallPlanMode + shouldIncludeArtifact. */
@@ -21,20 +21,6 @@ export function rcMarkerEnd(appKey: string, tag: string): string {
21
21
  return `# end ${appKey}:${tag}`;
22
22
  }
23
23
 
24
- /** Returns rc snippet block for PATH, or null if already present. */
25
- export function buildPathRcBlock(appKey: string, appDir: string): string {
26
- const start = rcMarkerStart(appKey, "path");
27
- const end = rcMarkerEnd(appKey, "path");
28
- return [start, `export PATH="${appDir}:$PATH"`, end].join("\n");
29
- }
30
-
31
- /** Returns rc snippet block for zsh fpath, or null if already present. */
32
- export function buildZshFpathRcBlock(appKey: string, completionsDir: string): string {
33
- const start = rcMarkerStart(appKey, "fpath");
34
- const end = rcMarkerEnd(appKey, "fpath");
35
- return [start, `fpath=(${completionsDir} $fpath)`, end].join("\n");
36
- }
37
-
38
24
  /** Removes a marker-delimited block from rc file content. */
39
25
  export function removeRcBlock(content: string, appKey: string, tag: string): string {
40
26
  const start = rcMarkerStart(appKey, tag);
@@ -34,9 +34,9 @@ describe("resolveInstallTargetPreview", () => {
34
34
  const paths = resolveInstallPaths(program);
35
35
  const preview = resolveInstallTargetPreview(program, paths);
36
36
  expect(preview.agentIntegration).toBe("mcp");
37
- expect(preview.all).toContain("app");
38
- expect(preview.all).toContain("completions");
39
- expect(preview.skill).toEqual([]);
37
+ expect(preview.all).not.toContain("app");
38
+ expect(preview.all.length).toBeGreaterThan(0);
39
+ expect(preview.all.every((k) => k.endsWith("Mcp") || k === "configure")).toBe(true);
40
40
  });
41
41
 
42
42
  test("shell app previews skill keys", () => {
@@ -49,7 +49,8 @@ describe("resolveInstallTargetPreview", () => {
49
49
  const paths = resolveInstallPaths(program);
50
50
  const preview = resolveInstallTargetPreview(program, paths);
51
51
  expect(preview.agentIntegration).toBe("skill");
52
- expect(preview.all).toContain("app");
52
+ expect(preview.all).not.toContain("app");
53
+ expect(preview.all.length).toBeGreaterThan(0);
53
54
  expect(preview.mcp).toEqual([]);
54
55
  });
55
56
  });
@@ -76,8 +77,7 @@ describe("printInstallStatus json", () => {
76
77
  effective: { all: string[]; mcp: string[]; skill: string[] };
77
78
  };
78
79
  expect(parsed.agentIntegration).toBe("mcp");
79
- expect(parsed.effective.all).toContain("app");
80
- expect(Array.isArray(parsed.effective.skill)).toBe(true);
80
+ expect(parsed.effective.all.length).toBeGreaterThan(0);
81
81
  } finally {
82
82
  process.stdout.write = orig;
83
83
  }
@@ -42,9 +42,6 @@ export function printInstallStatus(root: CliProgram, opts: InstallOpts): void {
42
42
  },
43
43
  };
44
44
  if (status.app) json.app = status.app;
45
- if (status.bashCompletion) json.bashCompletion = status.bashCompletion;
46
- if (status.zshCompletion) json.zshCompletion = status.zshCompletion;
47
- if (status.fishCompletion) json.fishCompletion = status.fishCompletion;
48
45
  if (status.cursorSkill) json.cursorSkill = status.cursorSkill;
49
46
  if (status.claudeSkill) json.claudeSkill = status.claudeSkill;
50
47
  if (status.cursorMcp) json.cursorMcp = status.cursorMcp;
@@ -60,9 +57,6 @@ export function printInstallStatus(root: CliProgram, opts: InstallOpts): void {
60
57
  installOut(`Installed artifacts for ${root.key}:`, opts);
61
58
  const lines: [string, string | undefined][] = [
62
59
  ["app", status.app],
63
- ["bash completion", status.bashCompletion],
64
- ["zsh completion", status.zshCompletion],
65
- ["fish completion", status.fishCompletion],
66
60
  ["cursor skill", status.cursorSkill],
67
61
  ["claude skill", status.claudeSkill],
68
62
  ["cursor mcp", status.cursorMcp],
@@ -107,8 +107,6 @@ export interface InstallTargetPreview {
107
107
  /** Derives plan mode from install CLI flags. */
108
108
  export function resolveInstallPlanMode(opts: {
109
109
  all?: boolean;
110
- app?: boolean;
111
- completions?: boolean;
112
110
  skill?: boolean;
113
111
  mcp?: boolean;
114
112
  configure?: boolean;
@@ -30,9 +30,6 @@ export { mcpCategoryEnabled } from "./target-effective.ts";
30
30
  function emptyInstalledArtifacts(): InstalledArtifacts {
31
31
  return {
32
32
  app: false,
33
- bashCompletion: false,
34
- zshCompletion: false,
35
- fishCompletion: false,
36
33
  cursorSkill: false,
37
34
  claudeSkill: false,
38
35
  codexSkill: false,
@@ -45,8 +42,6 @@ function emptyInstalledArtifacts(): InstalledArtifacts {
45
42
  codexMcp: false,
46
43
  openclawMcp: false,
47
44
  chatGptMcp: false,
48
- bashRcPath: false,
49
- zshRcFpath: false,
50
45
  };
51
46
  }
52
47
 
@@ -62,18 +57,10 @@ export function detectInstalledArtifacts(
62
57
  return out;
63
58
  }
64
59
 
65
- function isNonInteractiveInstall(opts: InstallOpts): boolean {
66
- return !!(opts.yes || opts.json || opts.reinstall || opts.update || opts.dry);
67
- }
68
-
69
- /** Scope flags; non-interactive `install --mcp` implicitly includes app. */
60
+ /** Scope flags for install/uninstall. */
70
61
  export function resolveInstallScope(opts: InstallOpts): InstallScope {
71
- const assumeApp =
72
- !opts.uninstall && opts.mcp && !opts.all && !opts.app && isNonInteractiveInstall(opts);
73
62
  return {
74
63
  all: opts.all,
75
- app: opts.app || assumeApp,
76
- completions: opts.completions,
77
64
  skill: opts.skill,
78
65
  mcp: opts.mcp,
79
66
  configure: opts.configure,
@@ -133,22 +120,20 @@ export function isArtifactInScope(
133
120
  return t.enabled && t.includedInAll;
134
121
  }
135
122
 
136
- const scoped =
137
- scope.app || scope.completions || scope.skill || scope.mcp || scope.configure || false;
138
-
139
123
  if (mode === "uninstall-scoped" || mode === "install-scoped") {
140
124
  if (!effective[key].enabled) return false;
141
- if (scope.configure) return key === "configure";
142
- if (scope.app) return key === "app";
143
- if (scope.completions) return key === "completions";
125
+
126
+ let inScope = false;
144
127
  if (scope.skill) {
145
- return agentCategoryInScope(key, SKILL_KEYS, "skill", effective, targets, root);
128
+ inScope = inScope || agentCategoryInScope(key, SKILL_KEYS, "skill", effective, targets, root);
146
129
  }
147
130
  if (scope.mcp) {
148
- return agentCategoryInScope(key, MCP_KEYS, "mcp", effective, targets, root);
131
+ inScope = inScope || agentCategoryInScope(key, MCP_KEYS, "mcp", effective, targets, root);
149
132
  }
150
- if (!scoped && mode === "install-scoped") return false;
151
- return false;
133
+ if (scope.configure && key === "configure") {
134
+ inScope = true;
135
+ }
136
+ return inScope;
152
137
  }
153
138
 
154
139
  return false;
@@ -174,7 +159,11 @@ export function shouldIncludeArtifact(
174
159
  return false;
175
160
  }
176
161
  if (mode === "refresh" && detected) {
177
- if (!detected[key]) return false;
162
+ if (key === "app") {
163
+ // app is status-only; refresh skills/MCP only
164
+ } else if (!detected[key]) {
165
+ return false;
166
+ }
178
167
  }
179
168
  if ((mode === "uninstall-all" || mode === "uninstall-scoped") && detected) {
180
169
  const det = detected[key];
@@ -194,6 +183,7 @@ export function shouldIncludeArtifact(
194
183
  return isArtifactInScope(key, scope, effective, mode, root, targets);
195
184
  }
196
185
  if (mode === "refresh") {
186
+ if (key === "app") return false;
197
187
  return effective[key].enabled;
198
188
  }
199
189
  return isArtifactInScope(key, scope, effective, mode, root, targets);
@@ -235,9 +225,6 @@ export function detectedForArtifact(
235
225
  detected: DetectedSnapshot,
236
226
  ): boolean {
237
227
  const target = installTargetForKey(key);
238
- if (key === "completions") {
239
- return detected.bashCompletion || detected.zshCompletion || detected.fishCompletion;
240
- }
241
228
  if (key === "configure") {
242
229
  return detected.appConfig ?? false;
243
230
  }