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.
- package/CHANGELOG.md +17 -3
- package/README.md +2 -2
- package/docs/README.md +2 -2
- package/docs/ai-skills.md +7 -7
- package/docs/bundled-docs.md +2 -2
- package/docs/cli-program.md +4 -4
- package/docs/configure.md +57 -85
- package/docs/developing.md +3 -3
- package/docs/distribution-homebrew.md +22 -23
- package/docs/mcp.md +3 -3
- package/examples/full-example/Formula/full-example.rb +7 -9
- package/examples/full-example/docs/cli-schema.json +48 -60
- package/examples/full-example/docs/cli.md +66 -36
- package/examples/full-example/docs/mcp.md +2 -2
- package/examples/full-example/docs/skill.md +1 -1
- package/examples/full-example/justfile +12 -12
- package/examples/full-example/scripts/formula-shared.ts +7 -13
- package/examples/full-example-json/Formula/full-example-json.rb +7 -9
- package/examples/full-example-json/docs/cli-schema.json +216 -270
- package/examples/full-example-json/docs/cli.md +297 -162
- package/examples/full-example-json/docs/mcp.md +2 -2
- package/examples/full-example-json/docs/skill.md +1 -1
- package/examples/full-example-json/justfile +12 -13
- package/examples/full-example-json/scripts/formula-shared.ts +7 -13
- package/index.d.ts +7 -8
- package/package.json +1 -1
- package/src/builtins/builtins.test.ts +21 -28
- package/src/builtins/configure-copy.ts +17 -20
- package/src/builtins/configure.ts +45 -74
- package/src/builtins/dispatch.ts +1 -8
- package/src/builtins/index.ts +1 -1
- package/src/cli-tool/create.ts +3 -1
- package/src/cli-tool/post-create.ts +5 -1
- package/src/config/file.ts +1 -0
- package/src/config/resolve.ts +1 -1
- package/src/configure/artifacts/mcp-config.ts +18 -12
- package/src/configure/artifacts/target-mcp-json.ts +19 -18
- package/src/configure/artifacts/target-skill.ts +8 -3
- package/src/configure/artifacts/targets/agents-mcp.ts +1 -1
- package/src/configure/artifacts/targets/configure.ts +2 -3
- package/src/configure/artifacts/uninstall.ts +5 -1
- package/src/configure/configure.test.ts +45 -113
- package/src/configure/index.ts +64 -354
- package/src/core/types.ts +7 -8
- package/src/docs/docs.test.ts +2 -2
- package/src/docs/mcp-guide.ts +2 -2
- package/src/skill/generate.ts +1 -1
- 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
|
-
/**
|
|
32
|
-
export function
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
`
|
|
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
|
-
|
|
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):
|
|
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(
|
|
82
|
-
|
|
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}: ${
|
|
93
|
-
message: `Merging MCP server "${ctx.paths.mcpName}" into ${
|
|
86
|
+
summary: `${this.spec.label}: ${displayPath}`,
|
|
87
|
+
message: `Merging MCP server "${ctx.paths.mcpName}" into ${displayPath}`,
|
|
94
88
|
run: () => {
|
|
95
|
-
|
|
96
|
-
|
|
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}: ${
|
|
108
|
-
message: `Removing MCP server "${ctx.paths.mcpName}" from ${
|
|
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
|
-
|
|
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()}: ${
|
|
79
|
-
message: `Installing ${this.spec.label.toLowerCase()} to ${
|
|
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
|
}
|
|
@@ -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(
|
|
21
|
-
return
|
|
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)
|
|
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
|
|
151
|
-
describe("
|
|
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
|
|
176
|
-
describe("
|
|
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
|
|
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", "
|
|
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
|
|
187
|
+
describe("configure install bootstrap", () => {
|
|
233
188
|
test("creates config.json for apps without appConfig", async () => {
|
|
234
189
|
const program: CliProgram = {
|
|
235
|
-
key: "
|
|
190
|
+
key: "installboot",
|
|
236
191
|
version: "0.0.0",
|
|
237
|
-
description: "
|
|
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", "
|
|
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("
|
|
205
|
+
test("afterInstall runs after install plan", async () => {
|
|
251
206
|
const calls: string[] = [];
|
|
252
207
|
const program: CliProgram = {
|
|
253
208
|
...fixture,
|
|
254
|
-
key: "
|
|
209
|
+
key: "hookinstall",
|
|
255
210
|
configure: {
|
|
256
|
-
|
|
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", "
|
|
216
|
+
const result = await new Cli(program).invoke(["configure", "install"]);
|
|
262
217
|
expect(result.exitCode).toBe(0);
|
|
263
|
-
expect(calls).toEqual([`after:
|
|
218
|
+
expect(calls).toEqual([`after:hookinstall:dry=false`]);
|
|
264
219
|
});
|
|
265
220
|
|
|
266
|
-
test("
|
|
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
|
-
|
|
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", "
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
});
|