argsbarg 6.3.2 → 7.0.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 (48) hide show
  1. package/CHANGELOG.md +17 -3
  2. package/README.md +2 -2
  3. package/docs/README.md +2 -2
  4. package/docs/ai-skills.md +7 -7
  5. package/docs/bundled-docs.md +2 -2
  6. package/docs/cli-program.md +4 -4
  7. package/docs/configure.md +57 -85
  8. package/docs/developing.md +3 -3
  9. package/docs/distribution-homebrew.md +22 -23
  10. package/docs/mcp.md +3 -3
  11. package/examples/full-example/Formula/full-example.rb +7 -9
  12. package/examples/full-example/docs/cli-schema.json +48 -60
  13. package/examples/full-example/docs/cli.md +66 -36
  14. package/examples/full-example/docs/mcp.md +2 -2
  15. package/examples/full-example/docs/skill.md +1 -1
  16. package/examples/full-example/justfile +12 -12
  17. package/examples/full-example/scripts/formula-shared.ts +7 -13
  18. package/examples/full-example-json/Formula/full-example-json.rb +7 -9
  19. package/examples/full-example-json/docs/cli-schema.json +216 -270
  20. package/examples/full-example-json/docs/cli.md +297 -162
  21. package/examples/full-example-json/docs/mcp.md +2 -2
  22. package/examples/full-example-json/docs/skill.md +1 -1
  23. package/examples/full-example-json/justfile +12 -13
  24. package/examples/full-example-json/scripts/formula-shared.ts +7 -13
  25. package/index.d.ts +7 -8
  26. package/package.json +1 -1
  27. package/src/builtins/builtins.test.ts +21 -28
  28. package/src/builtins/configure-copy.ts +17 -20
  29. package/src/builtins/configure.ts +45 -74
  30. package/src/builtins/dispatch.ts +1 -8
  31. package/src/builtins/index.ts +1 -1
  32. package/src/cli-tool/create.ts +3 -1
  33. package/src/cli-tool/post-create.ts +5 -1
  34. package/src/config/file.ts +1 -0
  35. package/src/config/resolve.ts +1 -1
  36. package/src/configure/artifacts/mcp-config.ts +18 -12
  37. package/src/configure/artifacts/target-mcp-json.ts +19 -18
  38. package/src/configure/artifacts/target-skill.ts +8 -3
  39. package/src/configure/artifacts/targets/agents-mcp.ts +1 -1
  40. package/src/configure/artifacts/targets/configure.ts +2 -3
  41. package/src/configure/artifacts/uninstall.ts +5 -1
  42. package/src/configure/configure.test.ts +45 -113
  43. package/src/configure/index.ts +64 -354
  44. package/src/core/types.ts +7 -8
  45. package/src/docs/docs.test.ts +2 -2
  46. package/src/docs/mcp-guide.ts +2 -2
  47. package/src/skill/generate.ts +1 -1
  48. package/src/skill/install.ts +2 -1
@@ -1,12 +1,15 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import type { CliProgram } from "../../core/types.ts";
4
+ import { displayHomePath } from "../../paths/host.ts";
4
5
 
5
6
  export interface McpServerEntry {
6
7
  command: string;
7
8
  args: string[];
8
9
  }
9
10
 
11
+ export type McpInstallResult = "installed" | "skipped-match" | "skipped-conflict";
12
+
10
13
  export function expectedMcpEntry(root: CliProgram): McpServerEntry {
11
14
  return { command: root.key, args: ["mcp"] };
12
15
  }
@@ -28,18 +31,20 @@ export function readMcpServerEntry(path: string, name: string): McpServerEntry |
28
31
  }
29
32
  }
30
33
 
31
- /** Returns an error message when existing entry conflicts, or null if safe to merge. */
32
- export function checkMcpConflict(path: string, name: string, expected: McpServerEntry, yes: boolean): string | null {
34
+ /** Installs MCP entry when missing; skips when matching; warns and skips on conflict. */
35
+ export function installMcpServerEntry(path: string, name: string, entry: McpServerEntry): McpInstallResult {
33
36
  const existing = readMcpServerEntry(path, name);
34
- if (existing && !entriesEqual(existing, expected) && !yes) {
35
- return (
36
- `MCP server "${name}" in ${path} differs from expected entry.\n` +
37
- ` existing: ${JSON.stringify(existing)}\n` +
38
- ` expected: ${JSON.stringify(expected)}\n` +
39
- `Use --yes to overwrite.`
37
+ if (existing) {
38
+ if (entriesEqual(existing, entry)) {
39
+ return "skipped-match";
40
+ }
41
+ process.stderr.write(
42
+ `MCP server "${name}" in ${displayHomePath(path)} differs; leaving existing entry unchanged.\n`,
40
43
  );
44
+ return "skipped-conflict";
41
45
  }
42
- return null;
46
+ mergeMcpConfig(path, name, entry, false);
47
+ return "installed";
43
48
  }
44
49
 
45
50
  /** Merges MCP server entry into config file. */
@@ -57,10 +62,11 @@ export function mergeMcpConfig(path: string, name: string, entry: McpServerEntry
57
62
  }
58
63
 
59
64
  /** Removes MCP server entry from config file (keeps file if other keys remain). */
60
- export function removeMcpConfig(path: string, name: string, dry: boolean): void {
61
- if (dry || !existsSync(path)) return;
65
+ export function removeMcpConfig(path: string, name: string, dry: boolean): string[] {
66
+ if (dry || !existsSync(path)) return [];
62
67
  const data = JSON.parse(readFileSync(path, "utf8")) as { mcpServers?: Record<string, unknown> };
63
- if (!data.mcpServers?.[name]) return;
68
+ if (!data.mcpServers?.[name]) return [];
64
69
  delete data.mcpServers[name];
65
70
  writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
71
+ return [path];
66
72
  }
@@ -1,11 +1,5 @@
1
1
  import type { CliProgram } from "../../core/types.ts";
2
- import {
3
- checkMcpConflict,
4
- expectedMcpEntry,
5
- mergeMcpConfig,
6
- readMcpServerEntry,
7
- removeMcpConfig,
8
- } from "./mcp-config.ts";
2
+ import { expectedMcpEntry, installMcpServerEntry, readMcpServerEntry, removeMcpConfig } from "./mcp-config.ts";
9
3
  import { displayInstallPath, type InstallPaths } from "./paths.ts";
10
4
  import { InstallTarget } from "./target-base.ts";
11
5
  import type {
@@ -78,22 +72,25 @@ export class McpJsonInstallTarget extends InstallTarget {
78
72
  status[this.spec.statusField] = line;
79
73
  }
80
74
 
81
- preflight(ctx: TargetPlanContext): string | null {
82
- const entry = expectedMcpEntry(ctx.root);
83
- return checkMcpConflict(this.spec.configPath(ctx.paths), ctx.paths.mcpName, entry, !!ctx.opts.yes);
75
+ preflight(_ctx: TargetPlanContext): string | null {
76
+ return null;
84
77
  }
85
78
 
86
79
  protected buildInstallActions(ctx: TargetPlanContext): InstallAction[] {
87
80
  const configPath = this.spec.configPath(ctx.paths);
88
81
  const entry = expectedMcpEntry(ctx.root);
82
+ const displayPath = displayInstallPath(configPath);
89
83
  return [
90
84
  {
91
85
  kind: this.actionKind,
92
- summary: `${this.spec.label}: ${displayInstallPath(configPath)}`,
93
- message: `Merging MCP server "${ctx.paths.mcpName}" into ${displayInstallPath(configPath)}`,
86
+ summary: `${this.spec.label}: ${displayPath}`,
87
+ message: `Merging MCP server "${ctx.paths.mcpName}" into ${displayPath}`,
94
88
  run: () => {
95
- mergeMcpConfig(configPath, ctx.paths.mcpName, entry, ctx.dry);
96
- return [configPath];
89
+ const result = installMcpServerEntry(configPath, ctx.paths.mcpName, entry);
90
+ if (result === "installed") {
91
+ process.stdout.write(`Registered MCP server in ${displayPath}\n`);
92
+ }
93
+ return result === "installed" ? [configPath] : [];
97
94
  },
98
95
  },
99
96
  ];
@@ -101,14 +98,18 @@ export class McpJsonInstallTarget extends InstallTarget {
101
98
 
102
99
  protected buildUninstallActions(ctx: TargetPlanContext): UninstallAction[] {
103
100
  const configPath = this.spec.configPath(ctx.paths);
101
+ const displayPath = displayInstallPath(configPath);
104
102
  return [
105
103
  {
106
104
  kind: this.actionKind,
107
- summary: `${this.spec.label}: ${displayInstallPath(configPath)}`,
108
- message: `Removing MCP server "${ctx.paths.mcpName}" from ${displayInstallPath(configPath)}`,
105
+ summary: `${this.spec.label}: ${displayPath}`,
106
+ message: `Removing MCP server "${ctx.paths.mcpName}" from ${displayPath}`,
109
107
  run: () => {
110
- removeMcpConfig(configPath, ctx.paths.mcpName, ctx.dry);
111
- return [configPath];
108
+ const changed = removeMcpConfig(configPath, ctx.paths.mcpName, ctx.dry);
109
+ if (changed.length > 0 && !ctx.dry) {
110
+ process.stdout.write(`Removed MCP server from ${displayPath}\n`);
111
+ }
112
+ return changed;
112
113
  },
113
114
  },
114
115
  ];
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import type { CliProgram } from "../../core/types.ts";
3
+ import { cliSkillInstall } from "../../skill/install.ts";
3
4
  import { displayInstallPath, type InstallPaths } from "./paths.ts";
4
5
  import { InstallTarget } from "./target-base.ts";
5
6
  import type {
@@ -72,12 +73,16 @@ export class SkillInstallTarget extends InstallTarget {
72
73
 
73
74
  protected buildInstallActions(ctx: TargetPlanContext): InstallAction[] {
74
75
  const dir = this.spec.skillDir(ctx.paths);
76
+ const displayDir = `${displayInstallPath(dir)}/`;
75
77
  return [
76
78
  {
77
79
  kind: this.actionKind,
78
- summary: `${this.spec.label.toLowerCase()}: ${displayInstallPath(dir)}/`,
79
- message: `Installing ${this.spec.label.toLowerCase()} to ${displayInstallPath(dir)}/`,
80
- run: () => [],
80
+ summary: `${this.spec.label.toLowerCase()}: ${displayDir}`,
81
+ message: `Installing ${this.spec.label.toLowerCase()} to ${displayDir}`,
82
+ run: () => {
83
+ cliSkillInstall(ctx.root, { global: true, rimraf: true, dry: ctx.dry });
84
+ return [];
85
+ },
81
86
  },
82
87
  ];
83
88
  }
@@ -7,5 +7,5 @@ export const agentsMcpTarget = new McpJsonInstallTarget({
7
7
  configPath: (p) => p.agentsMcpPath,
8
8
  detectedKey: "agentsMcp",
9
9
  statusField: "agentsMcp",
10
- isAvailable: (_root, _paths) => true,
10
+ isAvailable: (root) => root.mcpServer?.enabled === true,
11
11
  });
@@ -17,12 +17,11 @@ class ConfigureInstallTarget extends InstallTarget {
17
17
  readonly actionKind = "configure" as const;
18
18
  readonly category = "core" as const;
19
19
 
20
- isAvailable(root: CliProgram, _paths: InstallPaths): boolean {
21
- return root.appConfig !== undefined;
20
+ isAvailable(_root: CliProgram, _paths: InstallPaths): boolean {
21
+ return true;
22
22
  }
23
23
 
24
24
  isDetected(_paths: InstallPaths, root: CliProgram): boolean {
25
- if (!root.appConfig) return false;
26
25
  return appConfigInstalled(root);
27
26
  }
28
27
 
@@ -1,5 +1,6 @@
1
1
  import { existsSync, rmSync } from "node:fs";
2
2
  import type { CliProgram } from "../../core/types.ts";
3
+ import { displayHomePath } from "../../paths/host.ts";
3
4
  import type { InstallPaths } from "./paths.ts";
4
5
  import { buildUninstallPlanFromTargets } from "./target-plan-build.ts";
5
6
  import { skillTargetForUninstallSummary } from "./target-registry.ts";
@@ -15,7 +16,10 @@ export function buildUninstallPlan(root: CliProgram, paths: InstallPaths, opts:
15
16
  /** Rimraf skill directories during uninstall. */
16
17
  export function uninstallSkillDir(dir: string, dry: boolean): string[] {
17
18
  if (!existsSync(dir)) return [];
18
- if (!dry) rmSync(dir, { recursive: true, force: true });
19
+ if (!dry) {
20
+ rmSync(dir, { recursive: true, force: true });
21
+ process.stdout.write(`Removed skill from ${displayHomePath(dir)}/\n`);
22
+ }
19
23
  return [`${dir}/`];
20
24
  }
21
25
 
@@ -10,18 +10,14 @@ import { appConfigFileExists } from "../config/file.ts";
10
10
  import type { CliProgram } from "../core/types.ts";
11
11
  import { Cli } from "../index.ts";
12
12
  import { detectInstalledArtifacts } from "./artifacts/detect-installed.ts";
13
+ import { installMcpServerEntry } from "./artifacts/mcp-config.ts";
13
14
  import { parseInstallOpts } from "./artifacts/opts.ts";
14
15
  import { resolveClaudeDesktopMcpPath, resolveInstallPaths } from "./artifacts/paths.ts";
15
16
  import { buildInstallPlan, buildUpdatePlan } from "./artifacts/plan.ts";
16
17
  import { installTargetForKey } from "./artifacts/target-registry.ts";
17
18
  import { buildDetectedSnapshot, buildTargetPlanContext } from "./artifacts/target-scope.ts";
18
19
  import { buildUninstallPlan } from "./artifacts/uninstall.ts";
19
- import {
20
- appConfigHasEntries,
21
- formatConfigureMutationSummary,
22
- parseConfigureOpts,
23
- validateConfigureOpts,
24
- } from "./index.ts";
20
+ import { appConfigHasEntries } from "./index.ts";
25
21
 
26
22
  const fixture: CliProgram = {
27
23
  key: "testapp",
@@ -52,63 +48,6 @@ afterEach(() => {
52
48
  rmSync(home, { recursive: true, force: true });
53
49
  });
54
50
 
55
- /** Tests for configure opts. */
56
- describe("configure opts", () => {
57
- test("validate rejects multiple modes", () => {
58
- const opts = parseConfigureOpts({ refresh: "1", status: "1" });
59
- expect(validateConfigureOpts(opts)).toContain("only one");
60
- });
61
-
62
- test("refresh requires --yes", () => {
63
- const opts = parseConfigureOpts({ refresh: "1" });
64
- expect(validateConfigureOpts(opts)).toContain("--yes");
65
- });
66
-
67
- test("remove-all requires --yes", () => {
68
- const opts = parseConfigureOpts({ "remove-all": "1" });
69
- expect(validateConfigureOpts(opts)).toContain("--yes");
70
- });
71
- });
72
-
73
- describe("configure mutation summary", () => {
74
- test("interactive uninstall reports removed artifacts not file count", () => {
75
- const msg = formatConfigureMutationSummary(
76
- { paths: ["~/.agents/skills/testapp/"], installed: 0, removed: 1, configured: 0 },
77
- {},
78
- );
79
- expect(msg).toBe("Removed 1 artifact.");
80
- });
81
-
82
- test("interactive install reports installed artifacts not file count", () => {
83
- const msg = formatConfigureMutationSummary(
84
- {
85
- paths: ["~/.agents/skills/testapp/SKILL.md", "~/.agents/skills/testapp/reference.md"],
86
- installed: 1,
87
- removed: 0,
88
- configured: 0,
89
- },
90
- {},
91
- );
92
- expect(msg).toBe("Installed 1 artifact.");
93
- });
94
-
95
- test("refresh mode uses refreshed verb and artifact count", () => {
96
- const msg = formatConfigureMutationSummary(
97
- { paths: ["a", "b", "c"], installed: 3, removed: 0, configured: 0 },
98
- { refresh: true },
99
- );
100
- expect(msg).toBe("Refreshed 3 artifacts.");
101
- });
102
-
103
- test("remove-all uses removed verb", () => {
104
- const msg = formatConfigureMutationSummary(
105
- { paths: ["a"], installed: 0, removed: 2, configured: 0 },
106
- { removeAll: true },
107
- );
108
- expect(msg).toBe("Removed 2 artifacts.");
109
- });
110
- });
111
-
112
51
  /** Tests for install paths. */
113
52
  describe("install paths", () => {
114
53
  test("resolveInstallPaths includes agents skill and mcp paths", () => {
@@ -147,8 +86,8 @@ describe("detect installed", () => {
147
86
  });
148
87
  });
149
88
 
150
- /** Tests for refresh plan. */
151
- describe("refresh plan", () => {
89
+ /** Tests for install plan. */
90
+ describe("install plan", () => {
152
91
  test("buildUpdatePlan greenfield includes skill when enabled", () => {
153
92
  const paths = resolveInstallPaths(fixture);
154
93
  const plan = buildUpdatePlan(fixture, paths, parseInstallOpts({ reinstall: "1", yes: "1" }));
@@ -172,26 +111,43 @@ describe("refresh plan", () => {
172
111
  });
173
112
  });
174
113
 
175
- /** Tests for remove plan. */
176
- describe("remove plan", () => {
114
+ /** Tests for uninstall plan. */
115
+ describe("uninstall plan", () => {
177
116
  test("buildUninstallPlan --all with nothing installed is empty", () => {
178
117
  const paths = resolveInstallPaths(fixture);
179
118
  const plan = buildUninstallPlan(fixture, paths, parseInstallOpts({ uninstall: "1", all: "1" }));
180
119
  expect(plan.length).toBe(0);
181
120
  });
182
121
 
183
- test("configure --remove-all removes installed agent skill", async () => {
122
+ test("configure uninstall removes installed agent skill", async () => {
184
123
  const paths = resolveInstallPaths(fixture);
185
124
  mkdirSync(paths.agentsSkillDir, { recursive: true });
186
125
  writeFileSync(join(paths.agentsSkillDir, "SKILL.md"), "# test\n", "utf8");
187
126
 
188
127
  expect(detectInstalledArtifacts(paths, fixture).skill).toBe(true);
189
128
 
190
- const result = await new Cli(fixture).invoke(["configure", "--remove-all", "--yes"]);
129
+ const result = await new Cli(fixture).invoke(["configure", "uninstall", "--yes"]);
191
130
  expect(result.exitCode).toBe(0);
192
131
  expect(existsSync(paths.agentsSkillDir)).toBe(false);
193
132
  });
194
133
 
134
+ test("configure uninstall removes skill when HOME is a Homebrew sandbox", async () => {
135
+ const paths = resolveInstallPaths(fixture);
136
+ mkdirSync(paths.agentsSkillDir, { recursive: true });
137
+ writeFileSync(join(paths.agentsSkillDir, "SKILL.md"), "# test\n", "utf8");
138
+
139
+ const prevHome = process.env.HOME;
140
+ process.env.HOME = "/private/tmp/sqsp-workspaces-postinstall-20260807-fake";
141
+ try {
142
+ const result = await new Cli(fixture).invoke(["configure", "uninstall", "--yes"]);
143
+ expect(result.exitCode).toBe(0);
144
+ expect(existsSync(paths.agentsSkillDir)).toBe(false);
145
+ } finally {
146
+ if (prevHome === undefined) delete process.env.HOME;
147
+ else process.env.HOME = prevHome;
148
+ }
149
+ });
150
+
195
151
  test("scoped skill uninstall action removes directory", () => {
196
152
  const paths = resolveInstallPaths(fixture);
197
153
  mkdirSync(paths.agentsSkillDir, { recursive: true });
@@ -212,7 +168,6 @@ describe("remove plan", () => {
212
168
  });
213
169
  });
214
170
 
215
- /** Tests for app config wizard. */
216
171
  describe("app config wizard", () => {
217
172
  test("appConfigHasEntries when entries exist", () => {
218
173
  expect(
@@ -229,41 +184,41 @@ describe("app config wizard", () => {
229
184
  });
230
185
  });
231
186
 
232
- describe("configure --refresh bootstrap", () => {
187
+ describe("configure install bootstrap", () => {
233
188
  test("creates config.json for apps without appConfig", async () => {
234
189
  const program: CliProgram = {
235
- key: "refreshboot",
190
+ key: "installboot",
236
191
  version: "0.0.0",
237
- description: "Refresh bootstrap test.",
192
+ description: "Install bootstrap test.",
238
193
  skill: { enabled: true },
239
194
  handler: () => {},
240
195
  configure: { enabled: true },
241
196
  };
242
197
  expect(appConfigFileExists(program)).toBe(false);
243
- const result = await new Cli(program).invoke(["configure", "--refresh", "--yes"]);
198
+ const result = await new Cli(program).invoke(["configure", "install"]);
244
199
  expect(result.exitCode).toBe(0);
245
200
  expect(appConfigFileExists(program)).toBe(true);
246
201
  });
247
202
  });
248
203
 
249
204
  describe("configure lifecycle hooks", () => {
250
- test("afterRefresh runs after --refresh plan", async () => {
205
+ test("afterInstall runs after install plan", async () => {
251
206
  const calls: string[] = [];
252
207
  const program: CliProgram = {
253
208
  ...fixture,
254
- key: "hookrefresh",
209
+ key: "hookinstall",
255
210
  configure: {
256
- afterRefresh: async (ctx) => {
211
+ afterInstall: async (ctx) => {
257
212
  calls.push(`after:${ctx.paths.mcpName}:dry=${ctx.dry}`);
258
213
  },
259
214
  },
260
215
  };
261
- const result = await new Cli(program).invoke(["configure", "--refresh", "--yes"]);
216
+ const result = await new Cli(program).invoke(["configure", "install"]);
262
217
  expect(result.exitCode).toBe(0);
263
- expect(calls).toEqual([`after:hookrefresh:dry=false`]);
218
+ expect(calls).toEqual([`after:hookinstall:dry=false`]);
264
219
  });
265
220
 
266
- test("beforeRemoveAll runs before --remove-all plan", async () => {
221
+ test("beforeUninstall runs before uninstall plan", async () => {
267
222
  const paths = resolveInstallPaths({ ...fixture, key: "hookremove" });
268
223
  mkdirSync(paths.agentsSkillDir, { recursive: true });
269
224
  writeFileSync(join(paths.agentsSkillDir, "SKILL.md"), "# test\n", "utf8");
@@ -273,48 +228,25 @@ describe("configure lifecycle hooks", () => {
273
228
  ...fixture,
274
229
  key: "hookremove",
275
230
  configure: {
276
- beforeRemoveAll: async (ctx) => {
231
+ beforeUninstall: async (ctx) => {
277
232
  calls.push(`before:${ctx.paths.skillDirName}`);
278
233
  expect(existsSync(ctx.paths.agentsSkillDir)).toBe(true);
279
234
  },
280
235
  },
281
236
  };
282
- const result = await new Cli(program).invoke(["configure", "--remove-all", "--yes"]);
237
+ const result = await new Cli(program).invoke(["configure", "uninstall", "--yes"]);
283
238
  expect(result.exitCode).toBe(0);
284
239
  expect(calls).toEqual(["before:hookremove"]);
285
240
  expect(existsSync(paths.agentsSkillDir)).toBe(false);
286
241
  });
242
+ });
287
243
 
288
- test("hooks receive dry from --dry", async () => {
289
- const calls: string[] = [];
290
- const program: CliProgram = {
291
- ...fixture,
292
- key: "hookdry",
293
- configure: {
294
- afterRefresh: (ctx) => {
295
- calls.push(`dry=${ctx.dry}`);
296
- },
297
- },
298
- };
299
- const result = await new Cli(program).invoke(["configure", "--refresh", "--yes", "--dry"]);
300
- expect(result.exitCode).toBe(0);
301
- expect(calls).toEqual(["dry=true"]);
302
- });
303
-
304
- test("beforeRemoveAll is not called for --remove-config", async () => {
305
- let called = false;
306
- const program: CliProgram = {
307
- ...fixture,
308
- key: "hookcfg",
309
- appConfig: { entries: { token: { description: "Token." } } },
310
- configure: {
311
- beforeRemoveAll: () => {
312
- called = true;
313
- },
314
- },
315
- };
316
- const result = await new Cli(program).invoke(["configure", "--remove-config", "--yes"]);
317
- expect(result.exitCode).toBe(0);
318
- expect(called).toBe(false);
244
+ describe("MCP install idempotency", () => {
245
+ test("installMcpServerEntry skips matching entry", () => {
246
+ const paths = resolveInstallPaths(fixture);
247
+ mkdirSync(join(home, ".agents"), { recursive: true });
248
+ const entry = { command: "testapp", args: ["mcp"] };
249
+ writeFileSync(paths.agentsMcpPath, JSON.stringify({ mcpServers: { testapp: entry } }), "utf8");
250
+ expect(installMcpServerEntry(paths.agentsMcpPath, "testapp", entry)).toBe("skipped-match");
319
251
  });
320
252
  });