skillrepo 3.2.0 → 4.1.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/README.md +137 -27
- package/bin/skillrepo.mjs +5 -5
- package/package.json +1 -1
- package/src/commands/add.mjs +21 -6
- package/src/commands/get.mjs +20 -4
- package/src/commands/init-cohort-hooks.mjs +127 -0
- package/src/commands/init-session-sync.mjs +1 -1
- package/src/commands/init.mjs +480 -117
- package/src/commands/list.mjs +1 -1
- package/src/commands/remove.mjs +10 -2
- package/src/commands/uninstall.mjs +13 -2
- package/src/commands/update.mjs +112 -19
- package/src/lib/agent-hook-merge.mjs +203 -0
- package/src/lib/agent-registry.mjs +399 -0
- package/src/lib/artifact-registry.mjs +111 -2
- package/src/lib/cli-config.mjs +146 -44
- package/src/lib/detect-agents.mjs +112 -0
- package/src/lib/file-write.mjs +162 -77
- package/src/lib/fs-utils.mjs +16 -1
- package/src/lib/mcp-merge.mjs +17 -36
- package/src/lib/mergers/agent-hook-claude-shape.mjs +519 -0
- package/src/lib/mergers/agent-hook-cursor-shape.mjs +318 -0
- package/src/lib/mergers/gitignore.mjs +55 -28
- package/src/lib/paths.mjs +27 -25
- package/src/lib/prompt-multiselect.mjs +324 -0
- package/src/lib/removers/agent-hooks.mjs +83 -0
- package/src/lib/sync.mjs +18 -19
- package/src/test/commands/add.test.mjs +18 -3
- package/src/test/commands/init-picker.test.mjs +144 -0
- package/src/test/commands/init.test.mjs +508 -41
- package/src/test/commands/remove.test.mjs +4 -1
- package/src/test/commands/update.test.mjs +148 -3
- package/src/test/e2e/cli-agent-permutations.test.mjs +631 -0
- package/src/test/e2e/cli-cohort-hooks.test.mjs +393 -0
- package/src/test/e2e/cli-commands.test.mjs +39 -13
- package/src/test/integration/agent-hooks.integration.test.mjs +340 -0
- package/src/test/integration/file-write.integration.test.mjs +31 -10
- package/src/test/lib/agent-hook-merge.test.mjs +172 -0
- package/src/test/lib/agent-registry.test.mjs +215 -0
- package/src/test/lib/artifact-registry.test.mjs +39 -0
- package/src/test/lib/cli-config.test.mjs +222 -38
- package/src/test/lib/detect-agents.test.mjs +336 -0
- package/src/test/lib/file-write-placement.test.mjs +264 -0
- package/src/test/lib/file-write.test.mjs +231 -30
- package/src/test/lib/mcp-merge.test.mjs +23 -15
- package/src/test/lib/paths.test.mjs +53 -17
- package/src/test/lib/prompt-multiselect.test.mjs +448 -0
- package/src/test/lib/sync.test.mjs +157 -0
- package/src/test/mergers/agent-hook-claude-shape.test.mjs +518 -0
- package/src/test/mergers/agent-hook-cursor-shape.test.mjs +306 -0
- package/src/test/removers/agent-hooks.test.mjs +206 -0
- package/src/lib/detect-ides.mjs +0 -44
- package/src/test/detect-ides.test.mjs +0 -65
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E permutation matrix for the cohort SessionStart hooks (#1240).
|
|
3
|
+
*
|
|
4
|
+
* Spawns the real CLI binary and asserts on the user-visible result —
|
|
5
|
+
* what files land at what paths, what JSON shape they have, what the
|
|
6
|
+
* `--json` summary reports, and whether `uninstall --global` reverses
|
|
7
|
+
* everything cleanly.
|
|
8
|
+
*
|
|
9
|
+
* The harness mirrors `cli-agent-permutations.test.mjs` exactly:
|
|
10
|
+
* shared mock server for the library endpoint, per-test mkdtemp for
|
|
11
|
+
* cwd + HOME, and a `runCli` helper that always resolves with
|
|
12
|
+
* stdout / stderr / status.
|
|
13
|
+
*
|
|
14
|
+
* The gap this file closes is the user-layer: "after I run init with
|
|
15
|
+
* --agent cursor,gemini, what's actually in ~/.cursor/hooks.json and
|
|
16
|
+
* ~/.gemini/settings.json on disk?" The unit tests prove the
|
|
17
|
+
* mergers' shape contracts; the integration tests prove the
|
|
18
|
+
* dispatcher's filesystem behavior; this test proves the binary
|
|
19
|
+
* actually exercises both end-to-end.
|
|
20
|
+
*
|
|
21
|
+
* NOTE on `npx --yes` and the hook command: the universal hook
|
|
22
|
+
* command is `npx --yes skillrepo update --silent`. Invoking `npx`
|
|
23
|
+
* inside the test would either fetch from npm (slow, network-
|
|
24
|
+
* dependent) or fail in offline CI. We do NOT execute the hook
|
|
25
|
+
* command in this test — we only verify it was WRITTEN correctly to
|
|
26
|
+
* disk. Verifying execution end-to-end requires a global install,
|
|
27
|
+
* which the smoke-test script (`scripts/smoke-cohort-hooks.sh`)
|
|
28
|
+
* exercises out-of-band.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { describe, it, before, after, beforeEach, afterEach } from "node:test";
|
|
32
|
+
import assert from "node:assert/strict";
|
|
33
|
+
import {
|
|
34
|
+
mkdtempSync,
|
|
35
|
+
rmSync,
|
|
36
|
+
existsSync,
|
|
37
|
+
readFileSync,
|
|
38
|
+
writeFileSync,
|
|
39
|
+
mkdirSync,
|
|
40
|
+
} from "node:fs";
|
|
41
|
+
import { join, dirname, resolve } from "node:path";
|
|
42
|
+
import { tmpdir } from "node:os";
|
|
43
|
+
import { execFile } from "node:child_process";
|
|
44
|
+
import { fileURLToPath } from "node:url";
|
|
45
|
+
|
|
46
|
+
import { createMockServer } from "./mock-server.mjs";
|
|
47
|
+
import { AGENT_HOOK_COMMAND } from "../../lib/artifact-registry.mjs";
|
|
48
|
+
|
|
49
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
50
|
+
const CLI_BIN = resolve(__dirname, "../../../bin/skillrepo.mjs");
|
|
51
|
+
const VALID_KEY = "sk_live_test_e2e";
|
|
52
|
+
|
|
53
|
+
let server;
|
|
54
|
+
let port;
|
|
55
|
+
let tempDir;
|
|
56
|
+
let tempHome;
|
|
57
|
+
|
|
58
|
+
function makeSkill(owner, name) {
|
|
59
|
+
return {
|
|
60
|
+
owner,
|
|
61
|
+
name,
|
|
62
|
+
version: "1.0.0",
|
|
63
|
+
description: `${name} description`,
|
|
64
|
+
files: [
|
|
65
|
+
{
|
|
66
|
+
path: "SKILL.md",
|
|
67
|
+
content: `---\nname: ${name}\ndescription: ${name} description\n---\n\nbody\n`,
|
|
68
|
+
sha256: "x",
|
|
69
|
+
size: 100,
|
|
70
|
+
contentType: "text/markdown",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
updatedAt: "2025-01-01T00:00:00Z",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function runCli(args, extraEnv = {}) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
execFile(
|
|
80
|
+
process.execPath,
|
|
81
|
+
[CLI_BIN, ...args],
|
|
82
|
+
{
|
|
83
|
+
cwd: tempDir,
|
|
84
|
+
encoding: "utf-8",
|
|
85
|
+
timeout: 15_000,
|
|
86
|
+
env: {
|
|
87
|
+
...process.env,
|
|
88
|
+
HOME: tempHome,
|
|
89
|
+
USERPROFILE: tempHome, // Windows isolation
|
|
90
|
+
NO_COLOR: "1",
|
|
91
|
+
NODE_NO_WARNINGS: "1",
|
|
92
|
+
SKILLREPO_ACCESS_KEY: "",
|
|
93
|
+
SKILLREPO_TIMEOUT_MS: "5000",
|
|
94
|
+
...extraEnv,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
(err, stdout, stderr) => {
|
|
98
|
+
resolve({
|
|
99
|
+
stdout: stdout ?? "",
|
|
100
|
+
stderr: stderr ?? "",
|
|
101
|
+
status: err ? err.code ?? 1 : 0,
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe("CLI E2E — cohort SessionStart hooks (#1240)", () => {
|
|
109
|
+
before(async () => {
|
|
110
|
+
server = createMockServer({});
|
|
111
|
+
port = await server.start();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
after(async () => {
|
|
115
|
+
if (server) await server.stop();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
tempDir = mkdtempSync(join(tmpdir(), "cli-e2e-cohort-"));
|
|
120
|
+
tempHome = mkdtempSync(join(tmpdir(), "cli-e2e-cohort-home-"));
|
|
121
|
+
server.setLibraryResponse({
|
|
122
|
+
skills: [makeSkill("alice", "demo-skill")],
|
|
123
|
+
removals: [],
|
|
124
|
+
syncedAt: "x",
|
|
125
|
+
});
|
|
126
|
+
server.setEtag(null);
|
|
127
|
+
server.clearSkillResponses();
|
|
128
|
+
server.setSearchResponse({
|
|
129
|
+
skills: [],
|
|
130
|
+
pagination: { total: 0, limit: 20, offset: 0 },
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
|
|
136
|
+
if (tempHome) rmSync(tempHome, { recursive: true, force: true });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── Per-vendor file shape verification ─────────────────────────
|
|
140
|
+
|
|
141
|
+
it("init --agent cursor writes ~/.cursor/hooks.json with cursor-shape JSON", async () => {
|
|
142
|
+
const r = await runCli([
|
|
143
|
+
"init",
|
|
144
|
+
"--key", VALID_KEY,
|
|
145
|
+
"--url", `http://127.0.0.1:${port}`,
|
|
146
|
+
"--yes",
|
|
147
|
+
"--agent", "cursor",
|
|
148
|
+
"--json",
|
|
149
|
+
]);
|
|
150
|
+
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
151
|
+
|
|
152
|
+
const filePath = join(tempHome, ".cursor", "hooks.json");
|
|
153
|
+
assert.ok(existsSync(filePath));
|
|
154
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
155
|
+
assert.equal(parsed.version, 1);
|
|
156
|
+
assert.equal(parsed.hooks.sessionStart[0].command, AGENT_HOOK_COMMAND);
|
|
157
|
+
|
|
158
|
+
const json = JSON.parse(r.stdout);
|
|
159
|
+
assert.equal(json.sessionSync.cohortHooks.length, 1);
|
|
160
|
+
assert.equal(json.sessionSync.cohortHooks[0].vendorKey, "cursor");
|
|
161
|
+
assert.equal(json.sessionSync.cohortHooks[0].action, "installed");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("init --agent gemini writes ~/.gemini/settings.json with claude-shape + timeout: 60000", async () => {
|
|
165
|
+
const r = await runCli([
|
|
166
|
+
"init",
|
|
167
|
+
"--key", VALID_KEY,
|
|
168
|
+
"--url", `http://127.0.0.1:${port}`,
|
|
169
|
+
"--yes",
|
|
170
|
+
"--agent", "gemini",
|
|
171
|
+
"--json",
|
|
172
|
+
]);
|
|
173
|
+
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
174
|
+
|
|
175
|
+
const filePath = join(tempHome, ".gemini", "settings.json");
|
|
176
|
+
assert.ok(existsSync(filePath));
|
|
177
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
178
|
+
const inner = parsed.hooks.SessionStart[0].hooks[0];
|
|
179
|
+
assert.equal(inner.command, AGENT_HOOK_COMMAND);
|
|
180
|
+
assert.equal(inner.type, "command");
|
|
181
|
+
assert.equal(inner.timeout, 60000);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("init --agent codex writes ~/.codex/hooks.json with timeout: 60 seconds (#1243)", async () => {
|
|
185
|
+
// Codex's timeout is in SECONDS per the Codex hooks source.
|
|
186
|
+
// Distinct from Gemini's milliseconds. Verified against
|
|
187
|
+
// codex-rs/hooks/.
|
|
188
|
+
const r = await runCli([
|
|
189
|
+
"init",
|
|
190
|
+
"--key", VALID_KEY,
|
|
191
|
+
"--url", `http://127.0.0.1:${port}`,
|
|
192
|
+
"--yes",
|
|
193
|
+
"--agent", "codex",
|
|
194
|
+
"--json",
|
|
195
|
+
]);
|
|
196
|
+
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
197
|
+
|
|
198
|
+
const filePath = join(tempHome, ".codex", "hooks.json");
|
|
199
|
+
assert.ok(existsSync(filePath));
|
|
200
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
201
|
+
const inner = parsed.hooks.SessionStart[0].hooks[0];
|
|
202
|
+
assert.equal(inner.command, AGENT_HOOK_COMMAND);
|
|
203
|
+
assert.equal(inner.type, "command");
|
|
204
|
+
assert.equal(inner.timeout, 60);
|
|
205
|
+
// No matcher on Codex group — that's Gemini-specific (#1242)
|
|
206
|
+
assert.equal(parsed.hooks.SessionStart[0].matcher, undefined);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("init --agent copilot writes ~/.copilot/hooks/skillrepo-update.json with lowercase sessionStart event (#1244)", async () => {
|
|
210
|
+
// Copilot's spec uses lowercase `sessionStart` per VS Code +
|
|
211
|
+
// Copilot's hook docs. Distinct from Gemini and Codex's
|
|
212
|
+
// uppercase. Per-tool single file (no merge concerns within).
|
|
213
|
+
const r = await runCli([
|
|
214
|
+
"init",
|
|
215
|
+
"--key", VALID_KEY,
|
|
216
|
+
"--url", `http://127.0.0.1:${port}`,
|
|
217
|
+
"--yes",
|
|
218
|
+
"--agent", "copilot",
|
|
219
|
+
"--json",
|
|
220
|
+
]);
|
|
221
|
+
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
222
|
+
|
|
223
|
+
const filePath = join(tempHome, ".copilot", "hooks", "skillrepo-update.json");
|
|
224
|
+
assert.ok(existsSync(filePath), "Copilot per-tool hook file must exist");
|
|
225
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf-8"));
|
|
226
|
+
// Lowercase event name (Phase 4 fix)
|
|
227
|
+
assert.ok(Array.isArray(parsed.hooks.sessionStart));
|
|
228
|
+
assert.equal(parsed.hooks.SessionStart, undefined);
|
|
229
|
+
const inner = parsed.hooks.sessionStart[0].hooks[0];
|
|
230
|
+
assert.equal(inner.command, AGENT_HOOK_COMMAND);
|
|
231
|
+
assert.equal(inner.type, "command");
|
|
232
|
+
assert.equal(inner.timeout, 60);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ── Multi-vendor combined run ──────────────────────────────────
|
|
236
|
+
|
|
237
|
+
it("init --agent cursor,gemini,codex,copilot writes all four hook files", async () => {
|
|
238
|
+
const r = await runCli([
|
|
239
|
+
"init",
|
|
240
|
+
"--key", VALID_KEY,
|
|
241
|
+
"--url", `http://127.0.0.1:${port}`,
|
|
242
|
+
"--yes",
|
|
243
|
+
"--agent", "cursor,gemini,codex,copilot",
|
|
244
|
+
"--json",
|
|
245
|
+
]);
|
|
246
|
+
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
247
|
+
|
|
248
|
+
assert.ok(existsSync(join(tempHome, ".cursor", "hooks.json")));
|
|
249
|
+
assert.ok(existsSync(join(tempHome, ".gemini", "settings.json")));
|
|
250
|
+
assert.ok(existsSync(join(tempHome, ".codex", "hooks.json")));
|
|
251
|
+
assert.ok(
|
|
252
|
+
existsSync(join(tempHome, ".copilot", "hooks", "skillrepo-update.json")),
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const json = JSON.parse(r.stdout);
|
|
256
|
+
assert.equal(json.sessionSync.cohortHooks.length, 4);
|
|
257
|
+
for (const h of json.sessionSync.cohortHooks) {
|
|
258
|
+
assert.equal(h.action, "installed");
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ── --no-session-sync escape hatch ─────────────────────────────
|
|
263
|
+
|
|
264
|
+
it("init --no-session-sync skips ALL cohort hooks (semantics widened in #1240)", async () => {
|
|
265
|
+
const r = await runCli([
|
|
266
|
+
"init",
|
|
267
|
+
"--key", VALID_KEY,
|
|
268
|
+
"--url", `http://127.0.0.1:${port}`,
|
|
269
|
+
"--yes",
|
|
270
|
+
"--no-session-sync",
|
|
271
|
+
"--agent", "cursor,gemini,codex,copilot",
|
|
272
|
+
"--json",
|
|
273
|
+
]);
|
|
274
|
+
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
275
|
+
|
|
276
|
+
// None of the hook files should exist
|
|
277
|
+
assert.ok(!existsSync(join(tempHome, ".cursor", "hooks.json")));
|
|
278
|
+
assert.ok(!existsSync(join(tempHome, ".gemini", "settings.json")));
|
|
279
|
+
assert.ok(!existsSync(join(tempHome, ".codex", "hooks.json")));
|
|
280
|
+
assert.ok(
|
|
281
|
+
!existsSync(join(tempHome, ".copilot", "hooks", "skillrepo-update.json")),
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const json = JSON.parse(r.stdout);
|
|
285
|
+
assert.equal(json.sessionSync.cohortHooks.length, 0);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ── Idempotency on re-run ──────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
it("re-running init with the same flags reports unchanged for every cohort vendor", async () => {
|
|
291
|
+
const baseArgs = [
|
|
292
|
+
"init",
|
|
293
|
+
"--key", VALID_KEY,
|
|
294
|
+
"--url", `http://127.0.0.1:${port}`,
|
|
295
|
+
"--yes",
|
|
296
|
+
"--agent", "cursor,gemini",
|
|
297
|
+
];
|
|
298
|
+
const r1 = await runCli(baseArgs);
|
|
299
|
+
assert.equal(r1.status, 0, `first init stderr: ${r1.stderr}`);
|
|
300
|
+
|
|
301
|
+
const r2 = await runCli([...baseArgs, "--json"]);
|
|
302
|
+
assert.equal(r2.status, 0, `second init stderr: ${r2.stderr}`);
|
|
303
|
+
const json = JSON.parse(r2.stdout);
|
|
304
|
+
for (const h of json.sessionSync.cohortHooks) {
|
|
305
|
+
assert.equal(h.action, "unchanged", `${h.vendorKey} should be unchanged`);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ── Multi-tool merge surface ───────────────────────────────────
|
|
310
|
+
|
|
311
|
+
it("init --agent cursor preserves a pre-existing 1Password entry in ~/.cursor/hooks.json", async () => {
|
|
312
|
+
// Seed the hook file with a "1Password" entry before init runs.
|
|
313
|
+
// The cohort installer must add the SkillRepo entry alongside
|
|
314
|
+
// without removing the pre-existing one.
|
|
315
|
+
mkdirSync(join(tempHome, ".cursor"), { recursive: true });
|
|
316
|
+
writeFileSync(
|
|
317
|
+
join(tempHome, ".cursor", "hooks.json"),
|
|
318
|
+
JSON.stringify(
|
|
319
|
+
{
|
|
320
|
+
version: 1,
|
|
321
|
+
hooks: { sessionStart: [{ command: "1password-agent-helper" }] },
|
|
322
|
+
},
|
|
323
|
+
null,
|
|
324
|
+
2,
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const r = await runCli([
|
|
329
|
+
"init",
|
|
330
|
+
"--key", VALID_KEY,
|
|
331
|
+
"--url", `http://127.0.0.1:${port}`,
|
|
332
|
+
"--yes",
|
|
333
|
+
"--agent", "cursor",
|
|
334
|
+
]);
|
|
335
|
+
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
336
|
+
|
|
337
|
+
const parsed = JSON.parse(
|
|
338
|
+
readFileSync(join(tempHome, ".cursor", "hooks.json"), "utf-8"),
|
|
339
|
+
);
|
|
340
|
+
assert.equal(parsed.hooks.sessionStart.length, 2);
|
|
341
|
+
assert.equal(
|
|
342
|
+
parsed.hooks.sessionStart[0].command,
|
|
343
|
+
"1password-agent-helper",
|
|
344
|
+
);
|
|
345
|
+
assert.equal(parsed.hooks.sessionStart[1].command, AGENT_HOOK_COMMAND);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ── Uninstall round-trip ──────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
it("uninstall --global --yes removes every cohort hook the previous init wrote", async () => {
|
|
351
|
+
// Install all four
|
|
352
|
+
const init = await runCli([
|
|
353
|
+
"init",
|
|
354
|
+
"--key", VALID_KEY,
|
|
355
|
+
"--url", `http://127.0.0.1:${port}`,
|
|
356
|
+
"--yes",
|
|
357
|
+
"--agent", "cursor,gemini,codex,copilot",
|
|
358
|
+
]);
|
|
359
|
+
assert.equal(init.status, 0, `init stderr: ${init.stderr}`);
|
|
360
|
+
|
|
361
|
+
// Confirm files exist
|
|
362
|
+
const files = [
|
|
363
|
+
join(tempHome, ".cursor", "hooks.json"),
|
|
364
|
+
join(tempHome, ".gemini", "settings.json"),
|
|
365
|
+
join(tempHome, ".codex", "hooks.json"),
|
|
366
|
+
join(tempHome, ".copilot", "hooks", "skillrepo-update.json"),
|
|
367
|
+
];
|
|
368
|
+
for (const f of files) assert.ok(existsSync(f), `${f} must exist before uninstall`);
|
|
369
|
+
|
|
370
|
+
// Run uninstall --global --yes
|
|
371
|
+
const uninstall = await runCli([
|
|
372
|
+
"uninstall",
|
|
373
|
+
"--global",
|
|
374
|
+
"--yes",
|
|
375
|
+
"--json",
|
|
376
|
+
]);
|
|
377
|
+
assert.equal(uninstall.status, 0, `uninstall stderr: ${uninstall.stderr}`);
|
|
378
|
+
|
|
379
|
+
// Each cohort hook is no longer present in its file.
|
|
380
|
+
// The file may or may not exist depending on whether uninstall
|
|
381
|
+
// walked it down to {} or left an empty container — both are
|
|
382
|
+
// valid post-uninstall states. What matters is no SkillRepo
|
|
383
|
+
// command remains.
|
|
384
|
+
for (const f of files) {
|
|
385
|
+
if (!existsSync(f)) continue;
|
|
386
|
+
const content = readFileSync(f, "utf-8");
|
|
387
|
+
assert.ok(
|
|
388
|
+
!content.includes("skillrepo update --silent"),
|
|
389
|
+
`${f} still contains a SkillRepo command after uninstall`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
});
|
|
@@ -432,7 +432,9 @@ describe("CLI E2E — read commands", () => {
|
|
|
432
432
|
// ── PR3b: init ──────────────────────────────────────────────────
|
|
433
433
|
|
|
434
434
|
it("init writes config + MCP + runs first sync (happy path)", async () => {
|
|
435
|
-
// Create a .claude/ marker so
|
|
435
|
+
// Create a .claude/ marker so detection finds claudeCode (the
|
|
436
|
+
// Phase 3 picker pre-checks the Claude Code row when its signal
|
|
437
|
+
// fires).
|
|
436
438
|
mkdirSync(join(tempDir, ".claude"), { recursive: true });
|
|
437
439
|
|
|
438
440
|
const r = await runCli([
|
|
@@ -468,28 +470,52 @@ describe("CLI E2E — read commands", () => {
|
|
|
468
470
|
assert.equal(json.account.slug, "mock");
|
|
469
471
|
});
|
|
470
472
|
|
|
471
|
-
it("init --
|
|
473
|
+
it("init --agent claude works in empty dir (non-interactive scenario)", async () => {
|
|
472
474
|
const r = await runCli([
|
|
473
475
|
"init",
|
|
474
476
|
"--key", VALID_KEY,
|
|
475
477
|
"--url", `http://127.0.0.1:${port}`,
|
|
476
478
|
"--yes",
|
|
477
|
-
"--
|
|
479
|
+
"--agent", "claude",
|
|
478
480
|
]);
|
|
479
481
|
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
480
482
|
assert.ok(existsSync(join(tempDir, ".mcp.json")));
|
|
481
483
|
});
|
|
482
484
|
|
|
483
|
-
it("init
|
|
484
|
-
// No
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
485
|
+
it("init in an empty dir under --yes configures both default targets (#1236)", async () => {
|
|
486
|
+
// Phase 3 (#1236) replaced the "No agent targets detected → refuse"
|
|
487
|
+
// branch with the two-row picker. Under --yes with no detection
|
|
488
|
+
// signals, both rows are pre-checked (the spec rationale: writing
|
|
489
|
+
// a few KB the user didn't strictly need is trivial; CI running
|
|
490
|
+
// `init --yes` on a fresh clone and writing nothing is broken
|
|
491
|
+
// automation). The CLI must succeed and configure both targets.
|
|
492
|
+
//
|
|
493
|
+
// The env-var clears below force the genuine "no signal" state.
|
|
494
|
+
// The test host's shell may have CLAUDECODE=1 (Claude Code
|
|
495
|
+
// session), CURSOR_AGENT=1, etc. — without clearing those, the
|
|
496
|
+
// subprocess detection would pre-fire, defeating the test.
|
|
497
|
+
const r = await runCli(
|
|
498
|
+
[
|
|
499
|
+
"init",
|
|
500
|
+
"--key", VALID_KEY,
|
|
501
|
+
"--url", `http://127.0.0.1:${port}`,
|
|
502
|
+
"--yes",
|
|
503
|
+
"--json",
|
|
504
|
+
],
|
|
505
|
+
{
|
|
506
|
+
CLAUDECODE: "",
|
|
507
|
+
CURSOR_AGENT: "",
|
|
508
|
+
CURSOR_CLI: "",
|
|
509
|
+
GEMINI_CLI: "",
|
|
510
|
+
CLINE_ACTIVE: "",
|
|
511
|
+
},
|
|
512
|
+
);
|
|
513
|
+
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
514
|
+
const json = JSON.parse(r.stdout);
|
|
515
|
+
assert.equal(json.action, "initialized");
|
|
516
|
+
// Both targets present: claudeCode + the cohort.
|
|
517
|
+
assert.ok(json.vendors.includes("claudeCode"));
|
|
518
|
+
assert.ok(json.vendors.length > 1, "cohort vendors must also be configured");
|
|
493
519
|
});
|
|
494
520
|
|
|
495
521
|
it("init with 401 from validate exits 2", async () => {
|