skillrepo 4.8.0 → 4.8.2
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/bin/skillrepo.mjs +6 -3
- package/package.json +1 -1
- package/src/commands/add.mjs +2 -1
- package/src/commands/init.mjs +15 -5
- package/src/lib/http.mjs +25 -11
- package/src/test/commands/add.test.mjs +79 -2
- package/src/test/commands/get.test.mjs +131 -2
- package/src/test/commands/init-session-sync.test.mjs +724 -0
- package/src/test/commands/init.test.mjs +159 -2
- package/src/test/commands/list.test.mjs +573 -1
- package/src/test/commands/publish.test.mjs +136 -3
- package/src/test/commands/push.test.mjs +280 -1
- package/src/test/commands/remove.test.mjs +221 -2
- package/src/test/commands/search.test.mjs +203 -1
- package/src/test/commands/session-sync.test.mjs +227 -1
- package/src/test/commands/uninstall.test.mjs +216 -0
- package/src/test/commands/update.test.mjs +218 -0
- package/src/test/dispatcher.test.mjs +103 -2
- package/src/test/e2e/advertised-surface.test.mjs +207 -0
- package/src/test/e2e/cli-commands.test.mjs +1 -1
- package/src/test/e2e/uninstall-interactive.test.mjs +93 -0
- package/src/test/e2e/update-check-suppression.test.mjs +135 -0
- package/src/test/integration/update-list-contract.integration.test.mjs +66 -0
- package/src/test/lib/browser-open.test.mjs +43 -0
- package/src/test/lib/config.test.mjs +87 -0
- package/src/test/lib/crypto-shas.test.mjs +17 -0
- package/src/test/lib/file-write.test.mjs +244 -0
- package/src/test/lib/fs-utils.test.mjs +259 -0
- package/src/test/lib/global-install.test.mjs +134 -0
- package/src/test/lib/http-timeout.test.mjs +114 -0
- package/src/test/lib/http.test.mjs +616 -1
- package/src/test/lib/mcp-merge.test.mjs +157 -0
- package/src/test/lib/npm-update-check.test.mjs +180 -0
- package/src/test/lib/placement-walk.test.mjs +132 -0
- package/src/test/lib/skill-walk.test.mjs +39 -1
- package/src/test/lib/sync.test.mjs +139 -5
- package/src/test/lib/telemetry.test.mjs +34 -0
- package/src/test/mergers/claude-mcp.test.mjs +30 -0
- package/src/test/mergers/cursor-mcp.test.mjs +115 -0
- package/src/test/mergers/env-local.test.mjs +126 -0
- package/src/test/mergers/vscode-mcp.test.mjs +177 -0
- package/src/test/mergers/windsurf-mcp.test.mjs +144 -0
- package/src/test/resolve-key.test.mjs +33 -0
|
@@ -359,6 +359,140 @@ describe("installSkillrepoGlobally — happy path", () => {
|
|
|
359
359
|
});
|
|
360
360
|
});
|
|
361
361
|
|
|
362
|
+
// ── installSkillrepoGlobally — synchronous + async spawn errors ──
|
|
363
|
+
|
|
364
|
+
describe("installSkillrepoGlobally — spawn error edge paths", () => {
|
|
365
|
+
it("maps a SYNCHRONOUS non-ENOENT spawn throw to npm-nonzero", async () => {
|
|
366
|
+
// Some Node versions surface certain spawn failures synchronously.
|
|
367
|
+
// The catch around `spawn(...)` distinguishes ENOENT (→ enoent-npm)
|
|
368
|
+
// from everything else (→ npm-nonzero). This covers the non-ENOENT
|
|
369
|
+
// synchronous branch: a thrown error WITHOUT code "ENOENT".
|
|
370
|
+
const spawnThatThrows = () => {
|
|
371
|
+
throw Object.assign(new Error("EPERM: operation not permitted"), {
|
|
372
|
+
code: "EPERM",
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
const result = await installSkillrepoGlobally({
|
|
376
|
+
version: "3.1.2",
|
|
377
|
+
spawn: spawnThatThrows,
|
|
378
|
+
platform: "linux",
|
|
379
|
+
});
|
|
380
|
+
assert.equal(result.success, false);
|
|
381
|
+
assert.equal(result.errorCode, "npm-nonzero");
|
|
382
|
+
assert.match(result.error, /Failed to spawn npm/);
|
|
383
|
+
assert.match(result.error, /EPERM/);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("maps a synchronous throw with no `code` field to npm-nonzero (defensive)", async () => {
|
|
387
|
+
// `err && err.code === "ENOENT"` short-circuits on a falsy code —
|
|
388
|
+
// a bare Error with no code lands on the generic npm-nonzero return.
|
|
389
|
+
const spawnThatThrows = () => {
|
|
390
|
+
throw new Error("weird synchronous failure");
|
|
391
|
+
};
|
|
392
|
+
const result = await installSkillrepoGlobally({
|
|
393
|
+
version: "3.1.2",
|
|
394
|
+
spawn: spawnThatThrows,
|
|
395
|
+
platform: "linux",
|
|
396
|
+
});
|
|
397
|
+
assert.equal(result.errorCode, "npm-nonzero");
|
|
398
|
+
assert.match(result.error, /weird synchronous failure/);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("maps a SYNCHRONOUS ENOENT spawn throw to enoent-npm", async () => {
|
|
402
|
+
// Some Node versions throw ENOENT synchronously from spawn() rather
|
|
403
|
+
// than via the async `error` event. The synchronous catch must
|
|
404
|
+
// recognize `code === "ENOENT"` and return enoent-npm, identical to
|
|
405
|
+
// the async ENOENT path. Covers the `err.code === "ENOENT"` TRUE
|
|
406
|
+
// branch of the synchronous catch.
|
|
407
|
+
const spawnThatThrows = () => {
|
|
408
|
+
throw Object.assign(new Error("spawn npm ENOENT"), { code: "ENOENT" });
|
|
409
|
+
};
|
|
410
|
+
const result = await installSkillrepoGlobally({
|
|
411
|
+
version: "3.1.2",
|
|
412
|
+
spawn: spawnThatThrows,
|
|
413
|
+
platform: "linux",
|
|
414
|
+
});
|
|
415
|
+
assert.equal(result.success, false);
|
|
416
|
+
assert.equal(result.errorCode, "enoent-npm");
|
|
417
|
+
assert.match(result.error, /npm.*not found/i);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it("a synchronous throw of a message-less error falls back to String(err)", async () => {
|
|
421
|
+
// `err?.message ?? String(err)` — the `??` triggers only on
|
|
422
|
+
// null/undefined, so the message must be absent (not empty) to
|
|
423
|
+
// exercise the String() fallback. A thrown plain object with a
|
|
424
|
+
// non-ENOENT code and NO message reaches the npm-nonzero return via
|
|
425
|
+
// String(err).
|
|
426
|
+
const spawnThatThrows = () => {
|
|
427
|
+
throw { code: "EOTHER" }; // no `message` property at all
|
|
428
|
+
};
|
|
429
|
+
const result = await installSkillrepoGlobally({
|
|
430
|
+
version: "3.1.2",
|
|
431
|
+
spawn: spawnThatThrows,
|
|
432
|
+
platform: "linux",
|
|
433
|
+
});
|
|
434
|
+
assert.equal(result.errorCode, "npm-nonzero");
|
|
435
|
+
assert.match(result.error, /Failed to spawn npm/);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("maps an ASYNC non-ENOENT error event (e.g. EACCES at spawn) to npm-nonzero", async () => {
|
|
439
|
+
// makeMockSpawn fires the `error` event when `error` is set. An
|
|
440
|
+
// error whose code is NOT ENOENT hits the spawn-error branch
|
|
441
|
+
// (`settle({ kind: "spawn-error" })` → npm-nonzero "Failed to run npm").
|
|
442
|
+
const eacces = Object.assign(new Error("spawn EACCES"), { code: "EACCES" });
|
|
443
|
+
const spawn = makeMockSpawn({ error: eacces });
|
|
444
|
+
const result = await installSkillrepoGlobally({
|
|
445
|
+
version: "3.1.2",
|
|
446
|
+
spawn,
|
|
447
|
+
platform: "linux",
|
|
448
|
+
});
|
|
449
|
+
assert.equal(result.success, false);
|
|
450
|
+
assert.equal(result.errorCode, "npm-nonzero");
|
|
451
|
+
assert.match(result.error, /Failed to run npm/);
|
|
452
|
+
assert.match(result.error, /EACCES/);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("an async error event with no message falls back to String(err)", async () => {
|
|
456
|
+
// `err?.message ?? String(err)` — `??` only triggers on
|
|
457
|
+
// null/undefined, so the error object must have NO message property
|
|
458
|
+
// (absent, not empty) to exercise the String() fallback in the
|
|
459
|
+
// spawn-error path.
|
|
460
|
+
const weird = { code: "EOTHER" }; // no `message` key
|
|
461
|
+
const spawn = makeMockSpawn({ error: weird });
|
|
462
|
+
const result = await installSkillrepoGlobally({
|
|
463
|
+
version: "3.1.2",
|
|
464
|
+
spawn,
|
|
465
|
+
platform: "linux",
|
|
466
|
+
});
|
|
467
|
+
assert.equal(result.errorCode, "npm-nonzero");
|
|
468
|
+
assert.match(result.error, /Failed to run npm/);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("path-not-updated on Windows appends the new-terminal PATH addendum", async () => {
|
|
472
|
+
// When npm exits 0 but no stable binary is found AND the platform is
|
|
473
|
+
// Windows, the error message carries the Windows-specific addendum
|
|
474
|
+
// about PATH not refreshing in the current terminal. Covers the
|
|
475
|
+
// `isWindows` true branch of the platformAddendum ternary.
|
|
476
|
+
const spawn = makeMockSpawn({ exitCode: 0 });
|
|
477
|
+
const originalPath = process.env.PATH;
|
|
478
|
+
process.env.PATH = "/nonexistent/dir";
|
|
479
|
+
try {
|
|
480
|
+
const result = await installSkillrepoGlobally({
|
|
481
|
+
version: "3.1.2",
|
|
482
|
+
spawn,
|
|
483
|
+
platform: "win32",
|
|
484
|
+
});
|
|
485
|
+
assert.equal(result.success, false);
|
|
486
|
+
assert.equal(result.errorCode, "path-not-updated");
|
|
487
|
+
assert.match(result.error, /On Windows/);
|
|
488
|
+
assert.match(result.error, /new PowerShell or cmd\.exe window/);
|
|
489
|
+
} finally {
|
|
490
|
+
if (originalPath === undefined) delete process.env.PATH;
|
|
491
|
+
else process.env.PATH = originalPath;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
362
496
|
// ── resolveGlobalBinary — npx-cache filtering ───────────────────
|
|
363
497
|
|
|
364
498
|
describe("resolveGlobalBinary — transient-cache filtering", () => {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timeout-branch coverage for `src/lib/http.mjs`'s `safeFetch`.
|
|
3
|
+
*
|
|
4
|
+
* `DEFAULT_TIMEOUT_MS` is evaluated ONCE at module import from
|
|
5
|
+
* `SKILLREPO_TIMEOUT_MS`, so the timeout branches can't be flipped
|
|
6
|
+
* inside the main http.test.mjs process (the module is already cached
|
|
7
|
+
* with the default 30s). This file sets the env var BEFORE a
|
|
8
|
+
* cache-busting dynamic import (`?bust=` makes a distinct ESM module
|
|
9
|
+
* key) so each scenario gets a fresh module evaluation:
|
|
10
|
+
*
|
|
11
|
+
* - `SKILLREPO_TIMEOUT_MS=50` + a slow server → the AbortController
|
|
12
|
+
* fires, `safeFetch` maps the AbortError to a networkError with the
|
|
13
|
+
* "timed out" hint (covers the `err.name === "AbortError"` branch
|
|
14
|
+
* and the finite-timeout `setTimeout` + `clearTimeout` branch).
|
|
15
|
+
* - `SKILLREPO_TIMEOUT_MS=0` → `DEFAULT_TIMEOUT_MS === Infinity`, so
|
|
16
|
+
* `safeFetch` skips installing the timer entirely (covers the
|
|
17
|
+
* `Number.isFinite(DEFAULT_TIMEOUT_MS)` false branch and the
|
|
18
|
+
* `timeoutId !== null` false branch in the finally).
|
|
19
|
+
*
|
|
20
|
+
* Each import is isolated; the module's other behavior is covered by
|
|
21
|
+
* http.test.mjs. We point at `getSkill` (a single read endpoint) to
|
|
22
|
+
* keep the surface minimal.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { describe, it } from "node:test";
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import { createServer } from "node:http";
|
|
28
|
+
import { once } from "node:events";
|
|
29
|
+
|
|
30
|
+
import { CliError, EXIT_NETWORK } from "../../lib/errors.mjs";
|
|
31
|
+
|
|
32
|
+
const VALID_KEY = "sk_live_timeout_test";
|
|
33
|
+
|
|
34
|
+
/** Spin up a server whose response is delayed by `delayMs`. */
|
|
35
|
+
async function startSlowServer(delayMs) {
|
|
36
|
+
const server = createServer((req, res) => {
|
|
37
|
+
setTimeout(() => {
|
|
38
|
+
res.statusCode = 200;
|
|
39
|
+
res.setHeader("Content-Type", "application/json");
|
|
40
|
+
res.end(JSON.stringify({ skill: { owner: "a", name: "b", files: [] } }));
|
|
41
|
+
}, delayMs);
|
|
42
|
+
});
|
|
43
|
+
server.listen(0, "127.0.0.1");
|
|
44
|
+
await once(server, "listening");
|
|
45
|
+
const { port } = server.address();
|
|
46
|
+
return {
|
|
47
|
+
url: `http://127.0.0.1:${port}`,
|
|
48
|
+
close: () =>
|
|
49
|
+
new Promise((resolve) => {
|
|
50
|
+
// Force-destroy any lingering sockets (aborted client fetches +
|
|
51
|
+
// undici keep-alive) so `server.close` can't hang waiting for the
|
|
52
|
+
// pending response timer to drain. Without this, the slow-server
|
|
53
|
+
// connection a client aborted at 50ms keeps `close` blocked until
|
|
54
|
+
// the server's own delay elapses — and on Windows it could hang
|
|
55
|
+
// indefinitely.
|
|
56
|
+
server.closeAllConnections?.();
|
|
57
|
+
server.close(() => resolve());
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Import a fresh copy of http.mjs with the current env honored. */
|
|
63
|
+
async function freshHttp() {
|
|
64
|
+
return import(`../../lib/http.mjs?bust=${Date.now()}-${Math.random()}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Skipped on Windows: these exercise AbortController timing + local-server
|
|
68
|
+
// teardown, which are timing-sensitive on the Windows runner (a no-timeout
|
|
69
|
+
// fetch / a slow-server `close` can hang the 10-minute CLI-Windows job).
|
|
70
|
+
// The branches under test (finite-timeout abort, Infinity = no-timer) are
|
|
71
|
+
// platform-agnostic and fully covered on the Linux/macOS CI run.
|
|
72
|
+
describe("safeFetch — timeout branches", { skip: process.platform === "win32" }, () => {
|
|
73
|
+
it("a slow server past SKILLREPO_TIMEOUT_MS surfaces a 'timed out' networkError", { timeout: 20_000 }, async () => {
|
|
74
|
+
const prev = process.env.SKILLREPO_TIMEOUT_MS;
|
|
75
|
+
process.env.SKILLREPO_TIMEOUT_MS = "50";
|
|
76
|
+
const mod = await freshHttp();
|
|
77
|
+
// The server delays well past the 50ms cap so the AbortController
|
|
78
|
+
// fires on every attempt.
|
|
79
|
+
const srv = await startSlowServer(2000);
|
|
80
|
+
try {
|
|
81
|
+
await assert.rejects(
|
|
82
|
+
() => mod.getSkill(srv.url, VALID_KEY, "a", "b"),
|
|
83
|
+
(err) =>
|
|
84
|
+
err instanceof CliError &&
|
|
85
|
+
err.exitCode === EXIT_NETWORK &&
|
|
86
|
+
/timed out/.test(err.message) &&
|
|
87
|
+
typeof err.hint === "string" &&
|
|
88
|
+
/SKILLREPO_TIMEOUT_MS/.test(err.hint),
|
|
89
|
+
);
|
|
90
|
+
} finally {
|
|
91
|
+
if (prev === undefined) delete process.env.SKILLREPO_TIMEOUT_MS;
|
|
92
|
+
else process.env.SKILLREPO_TIMEOUT_MS = prev;
|
|
93
|
+
await srv.close();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("SKILLREPO_TIMEOUT_MS=0 disables the timer (Infinity) and lets a request complete", { timeout: 20_000 }, async () => {
|
|
98
|
+
const prev = process.env.SKILLREPO_TIMEOUT_MS;
|
|
99
|
+
process.env.SKILLREPO_TIMEOUT_MS = "0";
|
|
100
|
+
const mod = await freshHttp();
|
|
101
|
+
// No timer is installed (DEFAULT_TIMEOUT_MS === Infinity), so even a
|
|
102
|
+
// slightly-delayed response succeeds rather than aborting.
|
|
103
|
+
const srv = await startSlowServer(20);
|
|
104
|
+
try {
|
|
105
|
+
const result = await mod.getSkill(srv.url, VALID_KEY, "a", "b");
|
|
106
|
+
assert.ok(result, "request must complete with the timeout disabled");
|
|
107
|
+
assert.equal(result.owner, "a");
|
|
108
|
+
} finally {
|
|
109
|
+
if (prev === undefined) delete process.env.SKILLREPO_TIMEOUT_MS;
|
|
110
|
+
else process.env.SKILLREPO_TIMEOUT_MS = prev;
|
|
111
|
+
await srv.close();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|