skillrepo 3.1.0 → 3.1.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/README.md +6 -2
- package/package.json +1 -1
- package/src/commands/init-session-sync.mjs +307 -0
- package/src/commands/init.mjs +111 -101
- package/src/commands/session-sync-actions.mjs +92 -0
- package/src/lib/artifact-registry.mjs +43 -3
- package/src/lib/binary-locator.mjs +99 -0
- package/src/lib/cli-config.mjs +16 -3
- package/src/lib/cli-version.mjs +56 -0
- package/src/lib/config.mjs +6 -3
- package/src/lib/file-write.mjs +8 -3
- package/src/lib/fs-utils.mjs +9 -10
- package/src/lib/global-install.mjs +387 -0
- package/src/lib/mcp-merge.mjs +16 -5
- package/src/lib/mergers/session-hook.mjs +125 -33
- package/src/lib/platform.mjs +124 -0
- package/src/lib/sync.mjs +26 -0
- package/src/lib/transient-runners.mjs +204 -0
- package/src/test/commands/add.test.mjs +10 -4
- package/src/test/commands/get.test.mjs +10 -4
- package/src/test/commands/init.test.mjs +889 -15
- package/src/test/commands/list.test.mjs +10 -4
- package/src/test/commands/remove.test.mjs +10 -4
- package/src/test/commands/search.test.mjs +10 -4
- package/src/test/commands/session-sync-actions.test.mjs +74 -0
- package/src/test/commands/session-sync.test.mjs +25 -23
- package/src/test/commands/uninstall.test.mjs +20 -14
- package/src/test/commands/update.test.mjs +10 -4
- package/src/test/helpers/mock-spawn.mjs +121 -0
- package/src/test/helpers/sandbox-home.mjs +161 -0
- package/src/test/helpers/skillrepo-shim.mjs +133 -0
- package/src/test/integration/file-write.integration.test.mjs +10 -4
- package/src/test/lib/cli-config.test.mjs +182 -4
- package/src/test/lib/cli-version.test.mjs +47 -0
- package/src/test/lib/config.test.mjs +10 -4
- package/src/test/lib/file-write.test.mjs +24 -10
- package/src/test/lib/global-install.test.mjs +424 -0
- package/src/test/lib/mcp-merge.test.mjs +13 -7
- package/src/test/lib/paths.test.mjs +10 -4
- package/src/test/lib/platform.test.mjs +135 -0
- package/src/test/lib/sync.test.mjs +20 -4
- package/src/test/lib/transient-runners.test.mjs +270 -0
- package/src/test/mergers/session-hook.test.mjs +722 -22
- package/src/test/mergers/uninstall-settings.test.mjs +12 -1
- package/src/test/mergers/uninstall-windsurf-mcp.test.mjs +10 -4
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for src/lib/platform.mjs (v3.1.1).
|
|
3
|
+
*
|
|
4
|
+
* INTENT-BASED. The platform module is the single source of truth
|
|
5
|
+
* for every platform-specific difference the CLI honors. These
|
|
6
|
+
* tests lock in the contract that consumers (session-hook merger,
|
|
7
|
+
* future vendors) depend on. If a future refactor adds a new
|
|
8
|
+
* convention or changes an existing one, these tests must be
|
|
9
|
+
* updated — the data flow through this module is load-bearing.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { platform as osPlatform } from "node:os";
|
|
15
|
+
|
|
16
|
+
import { platformConventions, isWindows } from "../../lib/platform.mjs";
|
|
17
|
+
|
|
18
|
+
describe("platformConventions", () => {
|
|
19
|
+
it("returns POSIX conventions on macOS (darwin)", () => {
|
|
20
|
+
// INTENT: macOS is a POSIX family platform. `which` is the
|
|
21
|
+
// binary locator; shell supports `|| true` backstop; chmod
|
|
22
|
+
// works meaningfully; atomic directory rename works. A refactor
|
|
23
|
+
// that treats macOS as a special case would break every
|
|
24
|
+
// developer on the team.
|
|
25
|
+
const conv = platformConventions({ platform: "darwin" });
|
|
26
|
+
assert.equal(conv.family, "posix");
|
|
27
|
+
assert.equal(conv.binaryLocator, "which");
|
|
28
|
+
assert.equal(conv.hookShellSuffix, " || true");
|
|
29
|
+
assert.equal(conv.supportsPosixPermissions, true);
|
|
30
|
+
assert.equal(conv.supportsAtomicDirectoryRename, true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("returns POSIX conventions on Linux", () => {
|
|
34
|
+
const conv = platformConventions({ platform: "linux" });
|
|
35
|
+
assert.equal(conv.family, "posix");
|
|
36
|
+
assert.equal(conv.binaryLocator, "which");
|
|
37
|
+
assert.equal(conv.hookShellSuffix, " || true");
|
|
38
|
+
assert.equal(conv.supportsPosixPermissions, true);
|
|
39
|
+
assert.equal(conv.supportsAtomicDirectoryRename, true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns Windows conventions on win32", () => {
|
|
43
|
+
// INTENT: Windows has four material differences that can't be
|
|
44
|
+
// abstracted away:
|
|
45
|
+
// 1. `where.exe` instead of `which` (no shell binary lookup)
|
|
46
|
+
// 2. no `|| true` (cmd.exe doesn't know the `true` builtin)
|
|
47
|
+
// 3. chmod mode bits don't map to the ACL model — applying
|
|
48
|
+
// 0o600 on Windows looks like it worked but doesn't
|
|
49
|
+
// restrict access the way a POSIX 0600 does
|
|
50
|
+
// 4. renameSync fails on existing directory targets, so
|
|
51
|
+
// directory replacement needs a remove-then-rename dance
|
|
52
|
+
// with a small non-atomic window
|
|
53
|
+
// This test locks all four values for Windows — a refactor
|
|
54
|
+
// that breaks any one of them breaks every Windows user.
|
|
55
|
+
const conv = platformConventions({ platform: "win32" });
|
|
56
|
+
assert.equal(conv.family, "windows");
|
|
57
|
+
assert.equal(conv.binaryLocator, "where");
|
|
58
|
+
assert.equal(conv.hookShellSuffix, "");
|
|
59
|
+
assert.equal(conv.supportsPosixPermissions, false);
|
|
60
|
+
assert.equal(conv.supportsAtomicDirectoryRename, false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("treats unknown platforms as POSIX (conservative default)", () => {
|
|
64
|
+
// INTENT: Node supports aix, freebsd, openbsd, sunos, netbsd,
|
|
65
|
+
// android — all POSIX-family. A newly-released `NodeJS.Platform`
|
|
66
|
+
// value we've never seen must NOT crash the CLI. Falling back
|
|
67
|
+
// to POSIX is correct for every non-Windows Node target and
|
|
68
|
+
// matches how `node:path` / `node:fs` handle the same cases.
|
|
69
|
+
const conv = platformConventions({ platform: "aix" });
|
|
70
|
+
assert.equal(conv.family, "posix");
|
|
71
|
+
assert.equal(conv.binaryLocator, "which");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("uses os.platform() when called without an override", () => {
|
|
75
|
+
// INTENT: production callers let the override default. Verify
|
|
76
|
+
// the default correctly reflects the current runtime, so a
|
|
77
|
+
// CI run on Linux sees POSIX conventions without the test
|
|
78
|
+
// needing to know the specific platform.
|
|
79
|
+
const conv = platformConventions();
|
|
80
|
+
const actualPlatform = osPlatform();
|
|
81
|
+
if (actualPlatform === "win32") {
|
|
82
|
+
assert.equal(conv.family, "windows");
|
|
83
|
+
} else {
|
|
84
|
+
assert.equal(conv.family, "posix");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns a frozen object — callers cannot mutate conventions at runtime", () => {
|
|
89
|
+
// INTENT: the whole point of the single-source-of-truth design
|
|
90
|
+
// is that nobody gets to reach in and rewrite the rules. If a
|
|
91
|
+
// caller mutated the returned object, the next caller would
|
|
92
|
+
// see unexpected values. Freezing guarantees isolation.
|
|
93
|
+
const conv = platformConventions({ platform: "linux" });
|
|
94
|
+
assert.ok(Object.isFrozen(conv), "conventions object must be frozen");
|
|
95
|
+
assert.throws(
|
|
96
|
+
() => {
|
|
97
|
+
conv.binaryLocator = "pwned";
|
|
98
|
+
},
|
|
99
|
+
/Cannot assign to read only property/,
|
|
100
|
+
"attempting to mutate a frozen convention must throw in strict mode",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns the SAME frozen instance across calls for the same platform", () => {
|
|
105
|
+
// INTENT: identity guarantee. Two calls with the same platform
|
|
106
|
+
// return the same object — callers can cache without worrying
|
|
107
|
+
// about subtle drift between invocations.
|
|
108
|
+
const a = platformConventions({ platform: "darwin" });
|
|
109
|
+
const b = platformConventions({ platform: "darwin" });
|
|
110
|
+
assert.strictEqual(a, b);
|
|
111
|
+
|
|
112
|
+
const c = platformConventions({ platform: "win32" });
|
|
113
|
+
const d = platformConventions({ platform: "win32" });
|
|
114
|
+
assert.strictEqual(c, d);
|
|
115
|
+
|
|
116
|
+
// And of course, two different platforms don't collide:
|
|
117
|
+
assert.notStrictEqual(a, c);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("isWindows", () => {
|
|
122
|
+
it("returns true only when platform is win32", () => {
|
|
123
|
+
assert.equal(isWindows({ platform: "win32" }), true);
|
|
124
|
+
assert.equal(isWindows({ platform: "darwin" }), false);
|
|
125
|
+
assert.equal(isWindows({ platform: "linux" }), false);
|
|
126
|
+
assert.equal(isWindows({ platform: "aix" }), false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("defaults to os.platform() when called without an override", () => {
|
|
130
|
+
// INTENT: same default as platformConventions — production
|
|
131
|
+
// callers get the right answer for their runtime platform.
|
|
132
|
+
const actual = osPlatform();
|
|
133
|
+
assert.equal(isWindows(), actual === "win32");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -39,10 +39,16 @@ import { resolvePlacementDir } from "../../lib/file-write.mjs";
|
|
|
39
39
|
import { globalLastSyncPath } from "../../lib/paths.mjs";
|
|
40
40
|
import { CliError, EXIT_VALIDATION } from "../../lib/errors.mjs";
|
|
41
41
|
import { createMockServer } from "../e2e/mock-server.mjs";
|
|
42
|
+
import {
|
|
43
|
+
captureHome,
|
|
44
|
+
setSandboxHome,
|
|
45
|
+
restoreHome,
|
|
46
|
+
} from "../helpers/sandbox-home.mjs";
|
|
42
47
|
|
|
43
48
|
let sandbox;
|
|
44
49
|
let originalCwd;
|
|
45
|
-
|
|
50
|
+
/** @type {import("../helpers/sandbox-home.mjs").HomeEnvSnapshot} */
|
|
51
|
+
let originalHomeEnv;
|
|
46
52
|
let server;
|
|
47
53
|
let serverUrl;
|
|
48
54
|
const VALID_KEY = "sk_live_test123";
|
|
@@ -71,9 +77,9 @@ async function setupServer() {
|
|
|
71
77
|
mkdirSync(join(sandbox, "project"), { recursive: true });
|
|
72
78
|
mkdirSync(join(sandbox, "home"), { recursive: true });
|
|
73
79
|
originalCwd = process.cwd();
|
|
74
|
-
|
|
80
|
+
originalHomeEnv = captureHome();
|
|
75
81
|
process.chdir(join(sandbox, "project"));
|
|
76
|
-
|
|
82
|
+
setSandboxHome(join(sandbox, "home"));
|
|
77
83
|
|
|
78
84
|
server = createMockServer({});
|
|
79
85
|
const port = await server.start();
|
|
@@ -83,7 +89,7 @@ async function setupServer() {
|
|
|
83
89
|
async function teardownServer() {
|
|
84
90
|
if (server) await server.stop();
|
|
85
91
|
process.chdir(originalCwd);
|
|
86
|
-
|
|
92
|
+
restoreHome(originalHomeEnv);
|
|
87
93
|
if (sandbox) rmSync(sandbox, { recursive: true, force: true });
|
|
88
94
|
server = null;
|
|
89
95
|
}
|
|
@@ -146,6 +152,10 @@ describe("runSync — empty library", () => {
|
|
|
146
152
|
assert.equal(result.updated, 0);
|
|
147
153
|
assert.equal(result.removed, 0);
|
|
148
154
|
assert.equal(result.notModified, false);
|
|
155
|
+
// v3.1.1: no prior .last-sync state → this was a full sync.
|
|
156
|
+
// init.mjs distinguishes full vs delta to render accurate
|
|
157
|
+
// zero-counters messages (empty library vs up to date).
|
|
158
|
+
assert.equal(result.fullSync, true, "no last-sync state means fullSync:true");
|
|
149
159
|
});
|
|
150
160
|
});
|
|
151
161
|
|
|
@@ -338,6 +348,12 @@ describe("runSync — ETag round-trip", () => {
|
|
|
338
348
|
assert.equal(second.added, 0);
|
|
339
349
|
assert.equal(second.updated, 0);
|
|
340
350
|
assert.equal(second.removed, 0);
|
|
351
|
+
// v3.1.1: first sync had no prior state → fullSync:true. Second
|
|
352
|
+
// sync had the first's .last-sync written → fullSync:false.
|
|
353
|
+
// init.mjs uses this to distinguish the "empty library" vs
|
|
354
|
+
// "nothing changed since last sync" cases when counters are all 0.
|
|
355
|
+
assert.equal(first.fullSync, true, "first sync must report fullSync:true");
|
|
356
|
+
assert.equal(second.fullSync, false, "304 after prior sync must report fullSync:false");
|
|
341
357
|
});
|
|
342
358
|
|
|
343
359
|
it("304 short-circuit does not delete or modify on-disk skills", async () => {
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for src/lib/transient-runners.mjs (#894 / v3.1.2).
|
|
3
|
+
*
|
|
4
|
+
* Covers the three exports:
|
|
5
|
+
* - detectTransientRunner — returns the runner's display NAME
|
|
6
|
+
* (npx / pnpx / yarn dlx / bunx) or null. Used by init.mjs's
|
|
7
|
+
* Next-Steps prefix.
|
|
8
|
+
* - isTransientRunnerInvocation — boolean shortcut. Used as the
|
|
9
|
+
* gate for auto-install and the npx-guard in mergers/session-hook.
|
|
10
|
+
* - isTransientCachePath — boolean for filtering `where`/`which`
|
|
11
|
+
* output. Used by the binary locator.
|
|
12
|
+
*
|
|
13
|
+
* The boolean shortcut's behavior is also covered transitively by
|
|
14
|
+
* `cli-config.test.mjs` (which re-exports it). This file specifically
|
|
15
|
+
* locks in:
|
|
16
|
+
* - The runner-NAME return contract (v3.1.2 added this so init's
|
|
17
|
+
* Next-Steps shows `pnpx skillrepo list` for pnpm dlx users
|
|
18
|
+
* instead of always `npx skillrepo list`).
|
|
19
|
+
* - The `isTransientCachePath` filter behavior the binary locator
|
|
20
|
+
* depends on.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it } from "node:test";
|
|
24
|
+
import assert from "node:assert/strict";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
detectTransientRunner,
|
|
28
|
+
isTransientRunnerInvocation,
|
|
29
|
+
isTransientCachePath,
|
|
30
|
+
globalInstallCommandFor,
|
|
31
|
+
} from "../../lib/transient-runners.mjs";
|
|
32
|
+
|
|
33
|
+
// ── detectTransientRunner — returns the runner display name ────────
|
|
34
|
+
|
|
35
|
+
describe("detectTransientRunner", () => {
|
|
36
|
+
it("returns null for a vanilla stable-install argv", () => {
|
|
37
|
+
const result = detectTransientRunner({
|
|
38
|
+
argv: ["/usr/local/bin/node", "/usr/local/bin/skillrepo"],
|
|
39
|
+
env: {},
|
|
40
|
+
});
|
|
41
|
+
assert.equal(result, null);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns 'npx' for an npm npx invocation", () => {
|
|
45
|
+
const result = detectTransientRunner({
|
|
46
|
+
argv: [
|
|
47
|
+
"/usr/local/bin/node",
|
|
48
|
+
"/Users/alice/.npm/_npx/abc123/node_modules/.bin/skillrepo",
|
|
49
|
+
],
|
|
50
|
+
env: {},
|
|
51
|
+
});
|
|
52
|
+
assert.equal(result, "npx");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("returns 'pnpx' for a pnpm dlx invocation (cache substring)", () => {
|
|
56
|
+
const result = detectTransientRunner({
|
|
57
|
+
argv: [
|
|
58
|
+
"/usr/local/bin/node",
|
|
59
|
+
"/Users/alice/.local/share/pnpm/store/dlx-abc123/node_modules/.bin/skillrepo",
|
|
60
|
+
],
|
|
61
|
+
env: {},
|
|
62
|
+
});
|
|
63
|
+
assert.equal(result, "pnpx");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns 'yarn dlx' for a yarn berry dlx invocation", () => {
|
|
67
|
+
const result = detectTransientRunner({
|
|
68
|
+
argv: [
|
|
69
|
+
"/usr/local/bin/node",
|
|
70
|
+
"/Users/alice/project/.yarn/berry/cache/skillrepo-npm-3.1.2-abc/node_modules/skillrepo/bin/skillrepo.mjs",
|
|
71
|
+
],
|
|
72
|
+
env: {},
|
|
73
|
+
});
|
|
74
|
+
assert.equal(result, "yarn dlx");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("returns 'bunx' for a bun bunx invocation (cache substring)", () => {
|
|
78
|
+
const result = detectTransientRunner({
|
|
79
|
+
argv: [
|
|
80
|
+
"/usr/local/bin/node",
|
|
81
|
+
"/Users/alice/.bun/install/cache/skillrepo@3.1.2/node_modules/.bin/skillrepo",
|
|
82
|
+
],
|
|
83
|
+
env: {},
|
|
84
|
+
});
|
|
85
|
+
assert.equal(result, "bunx");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns 'npx' from `_` env-var fallback when argv path doesn't match", () => {
|
|
89
|
+
// E.g., a wrapper script symlinks the npx binary somewhere
|
|
90
|
+
// outside the npx cache. The `_` signal still identifies the
|
|
91
|
+
// launcher.
|
|
92
|
+
const result = detectTransientRunner({
|
|
93
|
+
argv: ["/usr/local/bin/node", "/somewhere/bin/skillrepo"],
|
|
94
|
+
env: { _: "/usr/local/bin/npx" },
|
|
95
|
+
});
|
|
96
|
+
assert.equal(result, "npx");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns 'pnpx' from `_` env-var fallback", () => {
|
|
100
|
+
const result = detectTransientRunner({
|
|
101
|
+
argv: ["/usr/local/bin/node", "/somewhere/bin/skillrepo"],
|
|
102
|
+
env: { _: "/usr/local/bin/pnpx" },
|
|
103
|
+
});
|
|
104
|
+
assert.equal(result, "pnpx");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns 'bunx' from `_` env-var fallback", () => {
|
|
108
|
+
const result = detectTransientRunner({
|
|
109
|
+
argv: ["/usr/local/bin/node", "/somewhere/bin/skillrepo"],
|
|
110
|
+
env: { _: "/Users/alice/.bun/bin/bunx" },
|
|
111
|
+
});
|
|
112
|
+
assert.equal(result, "bunx");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("argv signal beats `_` env-var (first-match wins on argv)", () => {
|
|
116
|
+
// If both signal a runner, argv wins. Realistically these would
|
|
117
|
+
// signal the SAME runner (npx invocation sets argv to the npx
|
|
118
|
+
// cache AND `_` to the npx binary), but the test pins the
|
|
119
|
+
// priority for the rare disagreement case.
|
|
120
|
+
const result = detectTransientRunner({
|
|
121
|
+
argv: [
|
|
122
|
+
"/usr/local/bin/node",
|
|
123
|
+
"/Users/alice/.bun/install/cache/skillrepo@3.1.2/node_modules/.bin/skillrepo",
|
|
124
|
+
],
|
|
125
|
+
env: { _: "/usr/local/bin/npx" },
|
|
126
|
+
});
|
|
127
|
+
// bunx wins because argv is checked first.
|
|
128
|
+
assert.equal(result, "bunx");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("does not false-positive on a directory named 'dlx-utils' (substring not in pnpm cache)", () => {
|
|
132
|
+
// Defensive: `/dlx-` is the substring, but a user-named directory
|
|
133
|
+
// like `/Users/alice/utility-dlx/` doesn't trigger because the
|
|
134
|
+
// substring requires a leading `/` separator before `dlx-`.
|
|
135
|
+
const result = detectTransientRunner({
|
|
136
|
+
argv: ["/usr/local/bin/node", "/Users/alice/utility-dlx/bin/skillrepo"],
|
|
137
|
+
env: {},
|
|
138
|
+
});
|
|
139
|
+
assert.equal(result, null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("does not false-positive on an `npx`-named user dir without the _npx cache pattern", () => {
|
|
143
|
+
const result = detectTransientRunner({
|
|
144
|
+
argv: ["/usr/local/bin/node", "/opt/npxtools/bin/skillrepo"],
|
|
145
|
+
env: {},
|
|
146
|
+
});
|
|
147
|
+
assert.equal(result, null);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ── isTransientRunnerInvocation — boolean shortcut ──────────────────
|
|
152
|
+
|
|
153
|
+
describe("isTransientRunnerInvocation (boolean shortcut)", () => {
|
|
154
|
+
// The boolean version reads from process.argv / process.env directly
|
|
155
|
+
// (no override hook for this convenience wrapper). The full table
|
|
156
|
+
// of detection cases is covered above via detectTransientRunner;
|
|
157
|
+
// these tests just verify the boolean shape.
|
|
158
|
+
let originalArgv;
|
|
159
|
+
let originalUnderscore;
|
|
160
|
+
|
|
161
|
+
function setup() {
|
|
162
|
+
originalArgv = process.argv;
|
|
163
|
+
originalUnderscore = process.env._;
|
|
164
|
+
delete process.env._;
|
|
165
|
+
}
|
|
166
|
+
function teardown() {
|
|
167
|
+
process.argv = originalArgv;
|
|
168
|
+
if (originalUnderscore === undefined) delete process.env._;
|
|
169
|
+
else process.env._ = originalUnderscore;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
it("returns true when argv signals a transient runner", () => {
|
|
173
|
+
setup();
|
|
174
|
+
try {
|
|
175
|
+
process.argv = [
|
|
176
|
+
"/usr/local/bin/node",
|
|
177
|
+
"/Users/alice/.npm/_npx/abc/skillrepo",
|
|
178
|
+
];
|
|
179
|
+
assert.equal(isTransientRunnerInvocation(), true);
|
|
180
|
+
} finally {
|
|
181
|
+
teardown();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("returns false for a stable-install argv", () => {
|
|
186
|
+
setup();
|
|
187
|
+
try {
|
|
188
|
+
process.argv = ["/usr/local/bin/node", "/usr/local/bin/skillrepo"];
|
|
189
|
+
assert.equal(isTransientRunnerInvocation(), false);
|
|
190
|
+
} finally {
|
|
191
|
+
teardown();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── isTransientCachePath — used by binary locator ──────────────────
|
|
197
|
+
|
|
198
|
+
describe("isTransientCachePath", () => {
|
|
199
|
+
it("identifies an npx cache path", () => {
|
|
200
|
+
assert.equal(
|
|
201
|
+
isTransientCachePath("/Users/alice/.npm/_npx/abc/node_modules/.bin/skillrepo"),
|
|
202
|
+
true,
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("identifies a pnpm dlx cache path", () => {
|
|
207
|
+
assert.equal(
|
|
208
|
+
isTransientCachePath("/Users/alice/.local/share/pnpm/store/dlx-abc/node_modules/.bin/skillrepo"),
|
|
209
|
+
true,
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("identifies a yarn berry cache path", () => {
|
|
214
|
+
assert.equal(
|
|
215
|
+
isTransientCachePath(
|
|
216
|
+
"/Users/alice/proj/.yarn/berry/cache/skillrepo-npm-3.1.2-abc/...",
|
|
217
|
+
),
|
|
218
|
+
true,
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("identifies a bun cache path", () => {
|
|
223
|
+
assert.equal(
|
|
224
|
+
isTransientCachePath("/Users/alice/.bun/install/cache/skillrepo@3.1.2/bin/skillrepo"),
|
|
225
|
+
true,
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("identifies a Windows-style npx cache path", () => {
|
|
230
|
+
assert.equal(
|
|
231
|
+
isTransientCachePath(
|
|
232
|
+
"C:\\Users\\alice\\.npm\\_npx\\abc\\node_modules\\.bin\\skillrepo.cmd",
|
|
233
|
+
),
|
|
234
|
+
true,
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("returns false for a stable global path", () => {
|
|
239
|
+
assert.equal(isTransientCachePath("/usr/local/bin/skillrepo"), false);
|
|
240
|
+
assert.equal(isTransientCachePath("/opt/homebrew/bin/skillrepo"), false);
|
|
241
|
+
assert.equal(
|
|
242
|
+
isTransientCachePath("C:\\Program Files\\nodejs\\skillrepo.cmd"),
|
|
243
|
+
false,
|
|
244
|
+
);
|
|
245
|
+
assert.equal(
|
|
246
|
+
isTransientCachePath("/Users/alice/.npm-global/bin/skillrepo"),
|
|
247
|
+
false,
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── globalInstallCommandFor — per-runner install hint ──────────────
|
|
253
|
+
|
|
254
|
+
describe("globalInstallCommandFor", () => {
|
|
255
|
+
it("returns the right install command for each known runner", () => {
|
|
256
|
+
assert.equal(globalInstallCommandFor("npx"), "npm install -g skillrepo");
|
|
257
|
+
assert.equal(globalInstallCommandFor("pnpx"), "pnpm add -g skillrepo");
|
|
258
|
+
// yarn berry has no `yarn global add`; falls back to npm
|
|
259
|
+
// (documented in the TRANSIENT_RUNNERS comment).
|
|
260
|
+
assert.equal(globalInstallCommandFor("yarn dlx"), "npm install -g skillrepo");
|
|
261
|
+
assert.equal(globalInstallCommandFor("bunx"), "bun add -g skillrepo");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("returns null for unknown / null runner names so callers can fall back", () => {
|
|
265
|
+
assert.equal(globalInstallCommandFor(null), null);
|
|
266
|
+
assert.equal(globalInstallCommandFor(undefined), null);
|
|
267
|
+
assert.equal(globalInstallCommandFor(""), null);
|
|
268
|
+
assert.equal(globalInstallCommandFor("not-a-real-runner"), null);
|
|
269
|
+
});
|
|
270
|
+
});
|