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
|
@@ -157,16 +157,26 @@ describe("runUpdate — flag handling", () => {
|
|
|
157
157
|
assert.ok(dir.startsWith(process.env.HOME));
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
it("--
|
|
160
|
+
it("--agent cursor writes to .agents/skills/", async () => {
|
|
161
161
|
server.setLibraryResponse({
|
|
162
162
|
skills: [makeSkill("cursor-test")],
|
|
163
163
|
removals: [],
|
|
164
164
|
syncedAt: "x",
|
|
165
165
|
});
|
|
166
|
-
await runUpdate(["--key", VALID_KEY, "--url", serverUrl, "--
|
|
167
|
-
const dir = resolvePlacementDir("
|
|
166
|
+
await runUpdate(["--key", VALID_KEY, "--url", serverUrl, "--agent", "cursor"]);
|
|
167
|
+
const dir = resolvePlacementDir("agentsProject", "cursor-test");
|
|
168
168
|
assert.ok(existsSync(dir));
|
|
169
169
|
});
|
|
170
|
+
|
|
171
|
+
it("rejects --agent none with a validation error (no targets to sync)", async () => {
|
|
172
|
+
await assert.rejects(
|
|
173
|
+
() => runUpdate(["--key", VALID_KEY, "--url", serverUrl, "--agent", "none"]),
|
|
174
|
+
(err) =>
|
|
175
|
+
err instanceof CliError &&
|
|
176
|
+
err.exitCode === EXIT_VALIDATION &&
|
|
177
|
+
/--agent none has no effect on `skillrepo update`/.test(err.message),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
170
180
|
});
|
|
171
181
|
|
|
172
182
|
// ── --session-hook mode (#884) ────────────────────────────────────────
|
|
@@ -326,3 +336,138 @@ describe("runUpdate — --session-hook contract", () => {
|
|
|
326
336
|
assert.equal(stdout.text(), "", "zero-delta 200 is silent");
|
|
327
337
|
});
|
|
328
338
|
});
|
|
339
|
+
|
|
340
|
+
// ── --silent mode (#1240) ─────────────────────────────────────────────
|
|
341
|
+
//
|
|
342
|
+
// Silent mode is what the cohort SessionStart hooks Cursor / Gemini CLI /
|
|
343
|
+
// Codex CLI / VS Code + Copilot install via the universal command
|
|
344
|
+
// `npx --yes skillrepo update --silent`. Contract:
|
|
345
|
+
//
|
|
346
|
+
// - stdout produces ONE line on success: `{}` (Gemini's hook stdout
|
|
347
|
+
// must be valid JSON; the others tolerate empty JSON).
|
|
348
|
+
// - On failure, stdout writes nothing extra; the typed CliError
|
|
349
|
+
// propagates and the dispatcher exits non-zero with an stderr
|
|
350
|
+
// message.
|
|
351
|
+
// - sync.mjs's non-fatal warnings (e.g. "failed to persist last-sync
|
|
352
|
+
// state") still go to stderr — operators running `update --silent`
|
|
353
|
+
// in a terminal still see them.
|
|
354
|
+
//
|
|
355
|
+
// Distinct from `--session-hook`. That mode is exit-0-on-everything
|
|
356
|
+
// because Claude Code blocks session start on hook failures. Cohort
|
|
357
|
+
// vendors do not block, so `--silent` propagates real exit codes.
|
|
358
|
+
|
|
359
|
+
describe("runUpdate — --silent contract", () => {
|
|
360
|
+
beforeEach(setup);
|
|
361
|
+
afterEach(teardown);
|
|
362
|
+
|
|
363
|
+
it("accepts the --silent flag without throwing Unknown argument", async () => {
|
|
364
|
+
// Same regression-guard pattern as the --session-hook test —
|
|
365
|
+
// resolveFlags would normally reject the flag, so an
|
|
366
|
+
// acceptPositional callback must consume it.
|
|
367
|
+
server.setLibraryResponse({ skills: [], removals: [], syncedAt: "x" });
|
|
368
|
+
await assert.doesNotReject(
|
|
369
|
+
() =>
|
|
370
|
+
runUpdate(
|
|
371
|
+
["--key", VALID_KEY, "--url", serverUrl, "--silent"],
|
|
372
|
+
{ stdout },
|
|
373
|
+
),
|
|
374
|
+
"update --silent must NOT throw Unknown argument",
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("emits exactly `{}\\n` on a successful empty sync (Gemini contract)", async () => {
|
|
379
|
+
// INTENT: Gemini specifically requires hook stdout to be valid JSON.
|
|
380
|
+
// The minimal valid value `{}` injects no model context.
|
|
381
|
+
server.setLibraryResponse({ skills: [], removals: [], syncedAt: "x" });
|
|
382
|
+
await runUpdate(
|
|
383
|
+
["--key", VALID_KEY, "--url", serverUrl, "--silent"],
|
|
384
|
+
{ stdout },
|
|
385
|
+
);
|
|
386
|
+
assert.equal(stdout.text(), "{}\n");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("emits `{}\\n` even when the sync produced changes (no progress lines on stdout)", async () => {
|
|
390
|
+
server.setLibraryResponse({
|
|
391
|
+
skills: [makeSkill("silent-test")],
|
|
392
|
+
removals: [],
|
|
393
|
+
syncedAt: "x",
|
|
394
|
+
});
|
|
395
|
+
await runUpdate(
|
|
396
|
+
["--key", VALID_KEY, "--url", serverUrl, "--silent"],
|
|
397
|
+
{ stdout },
|
|
398
|
+
);
|
|
399
|
+
// Stdout still ONLY `{}` — progress is not part of the hook
|
|
400
|
+
// contract. The skill files themselves still landed on disk
|
|
401
|
+
// (verified separately in the integration tests).
|
|
402
|
+
assert.equal(stdout.text(), "{}\n");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("propagates a real exit code on failure (does NOT silently swallow errors)", async () => {
|
|
406
|
+
// INTENT: distinct from --session-hook. Cohort vendors do not
|
|
407
|
+
// block session start on non-zero hook exit, so a failure should
|
|
408
|
+
// surface as a typed CliError that the dispatcher maps to the
|
|
409
|
+
// documented exit code. Silent-on-success does NOT mean
|
|
410
|
+
// silent-on-failure.
|
|
411
|
+
server.setForcedStatus(401, { error: "Invalid access key" });
|
|
412
|
+
await assert.rejects(
|
|
413
|
+
() =>
|
|
414
|
+
runUpdate(
|
|
415
|
+
["--key", VALID_KEY, "--url", serverUrl, "--silent"],
|
|
416
|
+
{ stdout },
|
|
417
|
+
),
|
|
418
|
+
(err) => err instanceof CliError && err.exitCode === EXIT_AUTH,
|
|
419
|
+
"auth error must propagate as CliError under --silent",
|
|
420
|
+
);
|
|
421
|
+
// No `{}` was written — failure surfaces via the throw, not a
|
|
422
|
+
// half-success stdout line.
|
|
423
|
+
assert.equal(stdout.text(), "", "no stdout on failure");
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it("propagates validation error (e.g. missing --agent value) under --silent", async () => {
|
|
427
|
+
// resolveFlags throws validationError on a malformed --agent.
|
|
428
|
+
// Silent mode must NOT catch that — same rationale as the auth
|
|
429
|
+
// error case.
|
|
430
|
+
await assert.rejects(
|
|
431
|
+
() =>
|
|
432
|
+
runUpdate(
|
|
433
|
+
["--key", VALID_KEY, "--url", serverUrl, "--silent", "--agent"],
|
|
434
|
+
{ stdout },
|
|
435
|
+
),
|
|
436
|
+
(err) => err instanceof CliError && err.exitCode === EXIT_VALIDATION,
|
|
437
|
+
);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it("--silent + --session-hook simultaneously: session-hook wins (defensive precedence) (#1239 QA)", async () => {
|
|
441
|
+
// INTENT: defensive precedence test. The two flags have different
|
|
442
|
+
// exit-code contracts: --session-hook is exit-0-on-everything
|
|
443
|
+
// (Claude blocks session start on non-zero), --silent propagates
|
|
444
|
+
// real exit codes. Today no installer writes a hook command
|
|
445
|
+
// containing both flags, but a future code path or manual user
|
|
446
|
+
// edit could. The branches in update.mjs check sessionHook FIRST,
|
|
447
|
+
// so when both are present, the session-hook contract wins.
|
|
448
|
+
//
|
|
449
|
+
// We force a failure (unreachable server) and assert the
|
|
450
|
+
// resolveFlags-doesnt-throw + emit-failure-line-on-stdout pattern
|
|
451
|
+
// of session-hook mode, NOT silent mode's exit-non-zero pattern.
|
|
452
|
+
server.setForcedStatus(401, { error: "bad key" });
|
|
453
|
+
await assert.doesNotReject(
|
|
454
|
+
() =>
|
|
455
|
+
runUpdate(
|
|
456
|
+
[
|
|
457
|
+
"--key", VALID_KEY,
|
|
458
|
+
"--url", serverUrl,
|
|
459
|
+
"--silent",
|
|
460
|
+
"--session-hook",
|
|
461
|
+
],
|
|
462
|
+
{ stdout },
|
|
463
|
+
),
|
|
464
|
+
"session-hook precedence over --silent must NOT throw on auth failure",
|
|
465
|
+
);
|
|
466
|
+
// Session-hook mode emits the one-line failure marker on stdout
|
|
467
|
+
assert.match(
|
|
468
|
+
stdout.text(),
|
|
469
|
+
/\[SkillRepo\] Sync failed:/,
|
|
470
|
+
"session-hook precedence must produce the session-hook failure line, not silent's empty stdout",
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
});
|