argsbarg 4.1.1 → 5.0.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 (62) hide show
  1. package/CHANGELOG.md +27 -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 +10 -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 +2 -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 +82 -61
  20. package/src/builtins/completion-group.ts +4 -6
  21. package/src/builtins/configure-copy.ts +86 -0
  22. package/src/builtins/configure.ts +70 -0
  23. package/src/builtins/dispatch.ts +13 -33
  24. package/src/builtins/index.ts +1 -1
  25. package/src/builtins/mcp.ts +2 -2
  26. package/src/builtins/registry.ts +6 -6
  27. package/src/capabilities.ts +22 -13
  28. package/src/cli-tool/cli-smoke.test.ts +13 -3
  29. package/src/cli-tool/create.test.ts +23 -1
  30. package/src/cli-tool/create.ts +26 -4
  31. package/src/cli-tool/full-example-capabilities.test.ts +2 -2
  32. package/src/cli-tool/program.ts +20 -5
  33. package/src/cli-tool/run-create.ts +8 -19
  34. package/src/config/bootstrap.ts +11 -9
  35. package/src/config/file.test.ts +1 -1
  36. package/src/config/resolve.ts +2 -2
  37. package/src/configure/configure.test.ts +148 -0
  38. package/src/configure/index.ts +284 -0
  39. package/src/configure/prompt.ts +40 -0
  40. package/src/docs/builtin.ts +3 -5
  41. package/src/docs/docs.test.ts +5 -5
  42. package/src/docs/mcp-guide.ts +4 -4
  43. package/src/index.ts +2 -2
  44. package/src/install/install-validate.test.ts +5 -5
  45. package/src/install/opts.ts +17 -0
  46. package/src/install/target-effective.ts +8 -8
  47. package/src/install/target-scope.ts +11 -8
  48. package/src/install/targets/configure.ts +1 -1
  49. package/src/install/targets.test.ts +4 -4
  50. package/src/invoke.test.ts +1 -1
  51. package/src/mcp/tools.ts +1 -1
  52. package/src/mcp.integration.test.ts +4 -4
  53. package/src/parse.test.ts +11 -13
  54. package/src/schema.ts +1 -9
  55. package/src/skill/hint.ts +2 -2
  56. package/src/types.ts +22 -14
  57. package/src/validate.ts +17 -17
  58. package/docs/install.md +0 -206
  59. package/src/builtins/install.ts +0 -106
  60. package/src/builtins/uninstall.ts +0 -80
  61. package/src/install/index.ts +0 -409
  62. package/src/install/install.test.ts +0 -317
@@ -1,317 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { writeAppConfigFile } from "../config/file.ts";
6
- import type { CliProgram } from "../types.ts";
7
- import { detectInstalledArtifacts } from "./detect-installed.ts";
8
- import {
9
- cliInstall,
10
- interactiveSelectionAssumesApp,
11
- mergeInteractiveSelection,
12
- parseInstallOpts,
13
- runInstallMutation,
14
- runUninstallMutation,
15
- validateInstallOpts,
16
- validateUninstallOpts,
17
- } from "./index.ts";
18
- import { opencodeConfigDir } from "./mcp-opencode.ts";
19
- import { normalizeUninstallRawOpts } from "./normalize-uninstall.ts";
20
- import { resolveClaudeDesktopMcpPath, resolveInstallPaths } from "./paths.ts";
21
- import { buildInstallPlan, buildUpdatePlan } from "./plan.ts";
22
- import { printInstallStatus, writeInteractiveInstallIntro } from "./status.ts";
23
- import { buildUninstallPlan } from "./uninstall.ts";
24
-
25
- const fixture: CliProgram = {
26
- key: "testapp",
27
- version: "0.0.0",
28
- description: "Test",
29
- mcpServer: { enabled: true },
30
- handler: () => {},
31
- };
32
-
33
- let home: string;
34
- let prevHome: string | undefined;
35
- let prevXdg: string | undefined;
36
-
37
- beforeEach(() => {
38
- home = mkdtempSync(join(tmpdir(), "argsbarg-install-"));
39
- prevHome = process.env.HOME;
40
- prevXdg = process.env.XDG_CONFIG_HOME;
41
- process.env.HOME = home;
42
- delete process.env.XDG_CONFIG_HOME;
43
- });
44
-
45
- afterEach(() => {
46
- if (prevHome === undefined) delete process.env.HOME;
47
- else process.env.HOME = prevHome;
48
- if (prevXdg === undefined) delete process.env.XDG_CONFIG_HOME;
49
- else process.env.XDG_CONFIG_HOME = prevXdg;
50
- rmSync(home, { recursive: true, force: true });
51
- });
52
-
53
- describe("install paths", () => {
54
- test("resolveInstallPaths includes skill and mcp paths", () => {
55
- const paths = resolveInstallPaths(fixture);
56
- expect(paths.cursorSkillDir).toContain(".cursor/skills");
57
- expect(paths.cursorMcpPath).toContain("mcp.json");
58
- });
59
-
60
- test("claude desktop mcp path on darwin", () => {
61
- const prev = process.platform;
62
- Object.defineProperty(process, "platform", { value: "darwin" });
63
- try {
64
- expect(resolveClaudeDesktopMcpPath(home)).toBe(
65
- join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
66
- );
67
- } finally {
68
- Object.defineProperty(process, "platform", { value: prev });
69
- }
70
- });
71
- });
72
-
73
- describe("detect installed", () => {
74
- test("detects cursor mcp when configured", () => {
75
- const paths = resolveInstallPaths(fixture);
76
- mkdirSync(join(home, ".cursor"), { recursive: true });
77
- writeFileSync(
78
- paths.cursorMcpPath,
79
- JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
80
- "utf8",
81
- );
82
-
83
- const detected = detectInstalledArtifacts(paths, fixture);
84
- expect(detected.cursorMcp).toBe(true);
85
- expect(detected.app).toBe(false);
86
- });
87
- });
88
-
89
- describe("install plan", () => {
90
- test("buildInstallPlan --all omits app self-install", () => {
91
- const paths = resolveInstallPaths(fixture);
92
- const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
93
- expect(plan.some((a) => a.kind === "app")).toBe(false);
94
- expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(true);
95
- });
96
-
97
- test("buildInstallPlan --mcp without yes omits implicit app", () => {
98
- const paths = resolveInstallPaths(fixture);
99
- const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
100
- expect(plan.some((a) => a.kind === "app")).toBe(false);
101
- });
102
-
103
- test("buildUpdatePlan greenfield falls back to install-all", () => {
104
- const paths = resolveInstallPaths(fixture);
105
- const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
106
- expect(plan.length).toBeGreaterThan(0);
107
- expect(plan.some((a) => a.kind === "app")).toBe(false);
108
- });
109
-
110
- test("buildInstallPlan --mcp skips MCP when agentIntegration is skill", () => {
111
- const skillApp: CliProgram = {
112
- ...fixture,
113
- install: { agentIntegration: "skill" },
114
- };
115
- const paths = resolveInstallPaths(skillApp);
116
- mkdirSync(join(home, ".claude"), { recursive: true });
117
- mkdirSync(join(home, ".cursor"), { recursive: true });
118
- const plan = buildInstallPlan(skillApp, paths, parseInstallOpts({ mcp: "1" }));
119
- expect(plan.some((a) => a.kind.endsWith("-mcp"))).toBe(false);
120
- });
121
-
122
- test("buildInstallPlan --mcp includes claude desktop when app data exists", () => {
123
- const paths = resolveInstallPaths(fixture);
124
- mkdirSync(join(home, ".claude"), { recursive: true });
125
- mkdirSync(join(home, "Library", "Application Support", "Claude"), { recursive: true });
126
- const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
127
- expect(plan.some((a) => a.kind === "claude-desktop-mcp")).toBe(process.platform === "darwin");
128
- expect(plan.some((a) => a.kind === "claude-mcp")).toBe(true);
129
- });
130
-
131
- test("buildInstallPlan --mcp includes opencode when config dir exists", () => {
132
- const paths = resolveInstallPaths(fixture);
133
- mkdirSync(opencodeConfigDir(home), { recursive: true });
134
- const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ mcp: "1" }));
135
- expect(plan.some((a) => a.kind === "opencode-mcp")).toBe(true);
136
- });
137
-
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);
157
- });
158
- });
159
-
160
- describe("install status", () => {
161
- test("printInstallStatus human output with MCP", () => {
162
- const paths = resolveInstallPaths(fixture);
163
- mkdirSync(join(home, ".cursor"), { recursive: true });
164
- writeFileSync(
165
- paths.cursorMcpPath,
166
- JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
167
- "utf8",
168
- );
169
-
170
- const chunks: string[] = [];
171
- const orig = process.stdout.write;
172
- process.stdout.write = ((s: string) => {
173
- chunks.push(s);
174
- return true;
175
- }) as typeof process.stdout.write;
176
- try {
177
- printInstallStatus(fixture, {});
178
- const out = chunks.join("");
179
- expect(out).toContain("Installed artifacts for testapp");
180
- expect(out).toContain("cursor mcp");
181
- } finally {
182
- process.stdout.write = orig;
183
- }
184
- });
185
-
186
- test("printInstallStatus json output", () => {
187
- const paths = resolveInstallPaths(fixture);
188
- mkdirSync(join(home, ".cursor"), { recursive: true });
189
- writeFileSync(
190
- paths.cursorMcpPath,
191
- JSON.stringify({ mcpServers: { testapp: { command: "testapp", args: ["mcp"] } } }),
192
- "utf8",
193
- );
194
-
195
- const chunks: string[] = [];
196
- const orig = process.stdout.write;
197
- process.stdout.write = ((s: string) => {
198
- chunks.push(s);
199
- return true;
200
- }) as typeof process.stdout.write;
201
- try {
202
- printInstallStatus(fixture, { json: true });
203
- const data = JSON.parse(chunks.join(""));
204
- expect(data.cursorMcp).toContain("mcp.json");
205
- } finally {
206
- process.stdout.write = orig;
207
- }
208
- });
209
- });
210
-
211
- describe("interactive selection", () => {
212
- test("mergeInteractiveSelection prepends 1 when app is assumed", () => {
213
- expect(mergeInteractiveSelection([2, 3], 5, true)).toEqual([1, 2, 3]);
214
- expect(mergeInteractiveSelection([1, 3], 5, true)).toEqual([1, 3]);
215
- expect(mergeInteractiveSelection([2, 3], 5, false)).toEqual([2, 3]);
216
- });
217
-
218
- test("interactiveSelectionAssumesApp is always false", () => {
219
- const paths = resolveInstallPaths(fixture);
220
- const plan = buildInstallPlan(fixture, paths, parseInstallOpts({ all: "1" }));
221
- expect(interactiveSelectionAssumesApp(plan, false)).toBe(false);
222
- });
223
- });
224
-
225
- describe("install output", () => {
226
- test("writeInteractiveInstallIntro prints app Setup banner on stderr", () => {
227
- const chunks: string[] = [];
228
- const orig = process.stderr.write;
229
- process.stderr.write = ((s: string | Uint8Array) => {
230
- chunks.push(String(s));
231
- return true;
232
- }) as typeof process.stderr.write;
233
- try {
234
- writeInteractiveInstallIntro(fixture);
235
- expect(chunks.join("")).toBe("\ntestapp Setup\n\n");
236
- } finally {
237
- process.stderr.write = orig;
238
- }
239
- });
240
- });
241
-
242
- describe("validateUninstallOpts", () => {
243
- test("bare uninstall normalizes to --all", () => {
244
- const opts = parseInstallOpts(normalizeUninstallRawOpts({ yes: "1" }));
245
- expect(validateUninstallOpts(opts)).toBeNull();
246
- expect(opts.all).toBe(true);
247
- });
248
-
249
- test("uninstall allows --all", () => {
250
- const opts = parseInstallOpts({ uninstall: "1", all: "1", yes: "1" });
251
- expect(validateUninstallOpts(opts)).toBeNull();
252
- });
253
-
254
- test("uninstall rejects --reinstall", () => {
255
- const opts = parseInstallOpts({ uninstall: "1", reinstall: "1", all: "1" });
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
- });
265
- });
266
-
267
- describe("install mutation", () => {
268
- test("uninstall --all with nothing installed succeeds", async () => {
269
- const result = await runUninstallMutation(fixture, { all: "1", yes: "1" });
270
- expect(result.changed).toEqual([]);
271
- });
272
-
273
- test("install --skill skips cursor and claude when agent dirs missing", async () => {
274
- const result = await runInstallMutation(fixture, { skill: "1", yes: "1", dry: "1" });
275
- expect(
276
- result.changed.every((p) => !p.includes("/.cursor/") && !p.includes("/.claude/skills/")),
277
- ).toBe(true);
278
- });
279
-
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);
299
- });
300
- });
301
-
302
- describe("uninstall plan", () => {
303
- test("buildUninstallPlan --all runs configure last when config exists", () => {
304
- const program: CliProgram = {
305
- ...fixture,
306
- appConfig: {
307
- entries: { token: { description: "API token." } },
308
- },
309
- };
310
- const paths = resolveInstallPaths(program);
311
- writeAppConfigFile(program, { token: "secret" });
312
-
313
- const plan = buildUninstallPlan(program, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
314
- const configureIdx = plan.findIndex((a) => a.summary.startsWith("app config:"));
315
- expect(configureIdx).toBe(plan.length - 1);
316
- });
317
- });