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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform conventions — single source of truth for OS-specific
|
|
3
|
+
* differences the CLI has to honor.
|
|
4
|
+
*
|
|
5
|
+
* The CLI runs on POSIX systems (macOS, Linux) and Windows. Most
|
|
6
|
+
* code paths are platform-neutral via Node built-ins (path.join,
|
|
7
|
+
* os.homedir, fs.rmSync, etc.) — but a handful of surfaces have
|
|
8
|
+
* real platform differences that can't be abstracted away at the
|
|
9
|
+
* Node level:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Binary-locator command**. POSIX provides `which`; Windows
|
|
12
|
+
* provides `where.exe`. `execFileSync` doesn't spawn a shell,
|
|
13
|
+
* so the literal name must exist on disk.
|
|
14
|
+
*
|
|
15
|
+
* 2. **Hook shell backstop suffix**. The SessionStart hook command
|
|
16
|
+
* relies on a shell-level fallback (`|| true`) to guarantee
|
|
17
|
+
* exit 0 even if the binary vanishes. POSIX shells support it;
|
|
18
|
+
* cmd.exe doesn't know the `true` builtin and would emit a
|
|
19
|
+
* confusing error. The `--session-hook` flag's exit-0 contract
|
|
20
|
+
* inside the Node process is the primary defense regardless of
|
|
21
|
+
* platform; the shell backstop is belt-and-suspenders that we
|
|
22
|
+
* lose on Windows.
|
|
23
|
+
*
|
|
24
|
+
* 3. **POSIX file permissions**. `chmodSync(0o600)` silently
|
|
25
|
+
* succeeds on Windows but doesn't produce the intended effect —
|
|
26
|
+
* Windows's ACL model doesn't map to the Unix mode bits. Any
|
|
27
|
+
* call meant to restrict permissions on credential files must
|
|
28
|
+
* be guarded so Windows users aren't misled into thinking their
|
|
29
|
+
* files are access-controlled when they aren't.
|
|
30
|
+
*
|
|
31
|
+
* 4. **Atomic directory replacement semantics**. POSIX's
|
|
32
|
+
* `renameSync` over an existing directory is atomic on the same
|
|
33
|
+
* filesystem — the swap is instantaneous from the perspective
|
|
34
|
+
* of any concurrent reader. Windows fails with EEXIST/EPERM if
|
|
35
|
+
* the target exists; the replacement must be done as a
|
|
36
|
+
* remove-then-rename sequence with a small window where the
|
|
37
|
+
* target is missing. Callers that write skills have to know
|
|
38
|
+
* which strategy applies so they can surface a meaningful
|
|
39
|
+
* recovery hint if the Windows path fails mid-sequence.
|
|
40
|
+
*
|
|
41
|
+
* This module exposes a single `platformConventions()` function that
|
|
42
|
+
* returns a frozen object with every platform-specific value the
|
|
43
|
+
* CLI needs. New platform-specific surfaces should be added here
|
|
44
|
+
* rather than spreading ad-hoc `platform() === "win32"` checks
|
|
45
|
+
* across the codebase. This is a convention, not an enforced rule —
|
|
46
|
+
* it's documentation plus a consumer pattern, not a linter.
|
|
47
|
+
*
|
|
48
|
+
* The `platform` parameter is an optional override for tests —
|
|
49
|
+
* production callers let it default to `os.platform()`.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { platform as osPlatform } from "node:os";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {Object} PlatformConventions
|
|
56
|
+
* @property {"posix" | "windows"} family - High-level family name.
|
|
57
|
+
* @property {string} binaryLocator - Command used to resolve a
|
|
58
|
+
* binary's absolute path from PATH. `"which"` on POSIX,
|
|
59
|
+
* `"where"` on Windows.
|
|
60
|
+
* @property {string} hookShellSuffix - Suffix appended to hook
|
|
61
|
+
* commands to guarantee exit 0 at the shell level. `" || true"`
|
|
62
|
+
* on POSIX (appended to the base command), empty string on
|
|
63
|
+
* Windows (the `--session-hook` flag's exit-0 contract is
|
|
64
|
+
* the only defense).
|
|
65
|
+
* @property {boolean} supportsPosixPermissions - True when
|
|
66
|
+
* `chmodSync(mode)` produces the intended POSIX mode-bit
|
|
67
|
+
* effect. False on Windows, where the call nominally
|
|
68
|
+
* succeeds but doesn't restrict ACLs the way a 0600 bit
|
|
69
|
+
* would on Unix. Callers use this to skip chmod on
|
|
70
|
+
* Windows rather than leave misleading "perms applied"
|
|
71
|
+
* success paths that don't actually restrict access.
|
|
72
|
+
* @property {boolean} supportsAtomicDirectoryRename - True when
|
|
73
|
+
* `renameSync` over an existing directory is atomic.
|
|
74
|
+
* POSIX: true. Windows: false — callers must implement
|
|
75
|
+
* remove-then-rename, accepting the small non-atomic
|
|
76
|
+
* window where the target doesn't exist. The Windows
|
|
77
|
+
* code path MUST produce a recoverable failure state if
|
|
78
|
+
* the rename step fails (i.e. leave the `.tmp/` dir on
|
|
79
|
+
* disk so the user can rename it manually).
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
const POSIX = Object.freeze({
|
|
83
|
+
family: "posix",
|
|
84
|
+
binaryLocator: "which",
|
|
85
|
+
hookShellSuffix: " || true",
|
|
86
|
+
supportsPosixPermissions: true,
|
|
87
|
+
supportsAtomicDirectoryRename: true,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const WINDOWS = Object.freeze({
|
|
91
|
+
family: "windows",
|
|
92
|
+
binaryLocator: "where",
|
|
93
|
+
hookShellSuffix: "",
|
|
94
|
+
supportsPosixPermissions: false,
|
|
95
|
+
supportsAtomicDirectoryRename: false,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Return the platform-specific convention set for the current
|
|
100
|
+
* platform, or an override.
|
|
101
|
+
*
|
|
102
|
+
* @param {object} [options]
|
|
103
|
+
* @param {NodeJS.Platform} [options.platform] - Override for testing.
|
|
104
|
+
* Production callers should let this default to the real
|
|
105
|
+
* runtime platform.
|
|
106
|
+
* @returns {PlatformConventions}
|
|
107
|
+
*/
|
|
108
|
+
export function platformConventions({ platform: platformOverride } = {}) {
|
|
109
|
+
const plat = platformOverride ?? osPlatform();
|
|
110
|
+
return plat === "win32" ? WINDOWS : POSIX;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* True if the current (or overridden) platform is Windows.
|
|
115
|
+
* Convenience wrapper — prefer `platformConventions().family ===
|
|
116
|
+
* "windows"` in call sites that already hold a conventions object.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} [options]
|
|
119
|
+
* @param {NodeJS.Platform} [options.platform]
|
|
120
|
+
* @returns {boolean}
|
|
121
|
+
*/
|
|
122
|
+
export function isWindows({ platform: platformOverride } = {}) {
|
|
123
|
+
return platformConventions({ platform: platformOverride }).family === "windows";
|
|
124
|
+
}
|
package/src/lib/sync.mjs
CHANGED
|
@@ -27,6 +27,22 @@
|
|
|
27
27
|
* @property {number} updated - Skills overwritten on disk
|
|
28
28
|
* @property {number} removed - Tombstones applied
|
|
29
29
|
* @property {boolean} notModified - True if 304 short-circuit fired
|
|
30
|
+
* @property {boolean | null} fullSync - True if this was a full (non-delta)
|
|
31
|
+
* sync — i.e. no prior `.last-sync` state
|
|
32
|
+
* existed so no `since` was sent. False if
|
|
33
|
+
* this was a delta sync. `null` only ever
|
|
34
|
+
* appears in SYNTHESIZED failure summaries
|
|
35
|
+
* (e.g. `init.mjs` when runSync threw) to
|
|
36
|
+
* mark the state as genuinely unknown —
|
|
37
|
+
* consumers should never see `null` from
|
|
38
|
+
* runSync itself. A full sync returning
|
|
39
|
+
* zero skills genuinely means "empty
|
|
40
|
+
* library"; a delta sync returning zero
|
|
41
|
+
* means "nothing changed since last sync".
|
|
42
|
+
* Consumers must distinguish these to
|
|
43
|
+
* render accurate user messages (see
|
|
44
|
+
* init.mjs step 7).
|
|
45
|
+
* @property {string} [syncedAt]
|
|
30
46
|
* @property {string} syncedAt - ISO timestamp from the server response
|
|
31
47
|
*
|
|
32
48
|
* @typedef {Object} SyncStateFile
|
|
@@ -182,6 +198,14 @@ export async function runSync(options) {
|
|
|
182
198
|
if (lastSync?.etag) opts.ifNoneMatch = lastSync.etag;
|
|
183
199
|
if (lastSync?.syncedAt) opts.since = lastSync.syncedAt;
|
|
184
200
|
|
|
201
|
+
// Track whether this is a full or delta sync BEFORE the network
|
|
202
|
+
// call, for the returned summary's `fullSync` field. A "full" sync
|
|
203
|
+
// is one where no `since` was sent — which is exactly when no
|
|
204
|
+
// prior last-sync state existed. The distinction matters to
|
|
205
|
+
// consumers (init.mjs) that need to tell "empty library" from
|
|
206
|
+
// "nothing changed since last sync" in the zero-counters case.
|
|
207
|
+
const fullSync = !lastSync?.syncedAt;
|
|
208
|
+
|
|
185
209
|
const result = await getLibrary(serverUrl, apiKey, opts);
|
|
186
210
|
|
|
187
211
|
// Step 4: 304 short-circuit
|
|
@@ -191,6 +215,7 @@ export async function runSync(options) {
|
|
|
191
215
|
updated: 0,
|
|
192
216
|
removed: 0,
|
|
193
217
|
notModified: true,
|
|
218
|
+
fullSync,
|
|
194
219
|
syncedAt: lastSync?.syncedAt ?? new Date().toISOString(),
|
|
195
220
|
};
|
|
196
221
|
}
|
|
@@ -201,6 +226,7 @@ export async function runSync(options) {
|
|
|
201
226
|
updated: 0,
|
|
202
227
|
removed: 0,
|
|
203
228
|
notModified: false,
|
|
229
|
+
fullSync,
|
|
204
230
|
syncedAt: result.syncedAt,
|
|
205
231
|
};
|
|
206
232
|
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transient package-runner detection (#894 / v3.1.2).
|
|
3
|
+
*
|
|
4
|
+
* The CLI cares about transient package runners — npx, pnpm dlx,
|
|
5
|
+
* yarn berry dlx, bunx — in two distinct ways:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Detect when the current process IS a transient invocation**
|
|
8
|
+
* (`detectTransientRunner` / `isTransientRunnerInvocation`).
|
|
9
|
+
* Used by `init` to gate the auto-install-global flow and to
|
|
10
|
+
* pick the right runner-prefix in Next-Steps output, and by
|
|
11
|
+
* `mergers/session-hook` to refuse baking a transient cache
|
|
12
|
+
* path into a long-lived hook command.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Detect when a candidate filesystem path is INSIDE a runner's
|
|
15
|
+
* transient cache** (`isTransientCachePath`). Used by the binary
|
|
16
|
+
* locator (`lib/binary-locator.mjs`) to filter `where`/`which`
|
|
17
|
+
* output so a cache-located binary doesn't shadow a real global
|
|
18
|
+
* install.
|
|
19
|
+
*
|
|
20
|
+
* The substring patterns for each runner's cache are the same data
|
|
21
|
+
* for both use cases; centralizing here is the single-source-of-truth
|
|
22
|
+
* fix for the duplication that existed in v3.1.2's first cleanup
|
|
23
|
+
* pass (cli-config.mjs and global-install.mjs each carried their own
|
|
24
|
+
* frozen array of the same substrings).
|
|
25
|
+
*
|
|
26
|
+
* ## Adding a new runner
|
|
27
|
+
*
|
|
28
|
+
* Add an entry to `TRANSIENT_RUNNERS` with:
|
|
29
|
+
* - `name`: the canonical runner-command string used in Next-Steps
|
|
30
|
+
* output (e.g. `"npx"`, `"pnpx"`, `"yarn dlx"`, `"bunx"`).
|
|
31
|
+
* - `cacheSubstrings`: substrings (POSIX and Windows separator
|
|
32
|
+
* variants) that uniquely identify the runner's per-invocation
|
|
33
|
+
* cache directory inside an absolute path. Both `argv[1]`-style
|
|
34
|
+
* paths and `where`/`which` output paths are matched against
|
|
35
|
+
* this list.
|
|
36
|
+
* - `commandSuffixes`: launcher-binary names that, when set as
|
|
37
|
+
* `process.env._` (the shell's "last command" var), uniquely
|
|
38
|
+
* identify this runner. Suffix-matched. Empty array if the
|
|
39
|
+
* runner has no canonical `_` form (e.g. `yarn dlx` with a
|
|
40
|
+
* space — `_` only gets the binary name, not the full command).
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} TransientRunner
|
|
45
|
+
* @property {string} name - Display name used in user-facing output
|
|
46
|
+
* (also the prefix for `<name> skillrepo list` next-step
|
|
47
|
+
* hints).
|
|
48
|
+
* @property {readonly string[]} cacheSubstrings - Path substrings
|
|
49
|
+
* that uniquely identify the runner's cache directory.
|
|
50
|
+
* @property {readonly string[]} commandSuffixes - `_` env-var
|
|
51
|
+
* suffix patterns matching the launcher binary name.
|
|
52
|
+
* @property {string} globalInstallCommand - The canonical "install
|
|
53
|
+
* skillrepo globally with this package manager" command
|
|
54
|
+
* we suggest to a user who just ran via this runner.
|
|
55
|
+
* Used by init's Next-Steps Tip when no global is yet
|
|
56
|
+
* active. Yarn berry uses `npm install -g` because yarn
|
|
57
|
+
* berry intentionally has no `yarn global add` equivalent
|
|
58
|
+
* (it directs users to `dlx` for one-offs and away from
|
|
59
|
+
* globals); npm-installed binaries land on the user's
|
|
60
|
+
* PATH the same regardless of which runner they used to
|
|
61
|
+
* bootstrap.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Catalog of supported transient runners. Order matters only for
|
|
66
|
+
* tie-breaking when multiple runners' substrings match the same
|
|
67
|
+
* path (extremely unlikely given the specificity of each); the
|
|
68
|
+
* first match wins.
|
|
69
|
+
*/
|
|
70
|
+
export const TRANSIENT_RUNNERS = Object.freeze([
|
|
71
|
+
Object.freeze({
|
|
72
|
+
name: "npx",
|
|
73
|
+
// npx writes to `~/.npm/_npx/<hash>/...`.
|
|
74
|
+
cacheSubstrings: Object.freeze(["/_npx/", "\\_npx\\"]),
|
|
75
|
+
commandSuffixes: Object.freeze(["/npx", "\\npx"]),
|
|
76
|
+
globalInstallCommand: "npm install -g skillrepo",
|
|
77
|
+
}),
|
|
78
|
+
Object.freeze({
|
|
79
|
+
name: "pnpx",
|
|
80
|
+
// pnpm dlx writes to `<store>/dlx-<hash>/...`. Both pnpm dlx
|
|
81
|
+
// and the legacy `pnpx` shim hit this cache.
|
|
82
|
+
cacheSubstrings: Object.freeze(["/dlx-", "\\dlx-"]),
|
|
83
|
+
commandSuffixes: Object.freeze(["/pnpx", "\\pnpx"]),
|
|
84
|
+
globalInstallCommand: "pnpm add -g skillrepo",
|
|
85
|
+
}),
|
|
86
|
+
Object.freeze({
|
|
87
|
+
name: "yarn dlx",
|
|
88
|
+
// yarn berry dlx caches under `.yarn/berry/cache/...` and
|
|
89
|
+
// resolves PnP virtuals under `.yarn/$$virtual/...`. The
|
|
90
|
+
// launcher is `yarn dlx <pkg>` which sets `_` to `yarn`, NOT
|
|
91
|
+
// `yarn dlx` — so commandSuffixes is empty; argv-path detection
|
|
92
|
+
// is the only signal.
|
|
93
|
+
cacheSubstrings: Object.freeze([
|
|
94
|
+
"/.yarn/berry/",
|
|
95
|
+
"\\.yarn\\berry\\",
|
|
96
|
+
"/.yarn/$$virtual/",
|
|
97
|
+
"\\.yarn\\$$virtual\\",
|
|
98
|
+
]),
|
|
99
|
+
commandSuffixes: Object.freeze([]),
|
|
100
|
+
// Yarn berry deliberately doesn't ship a `yarn global add`
|
|
101
|
+
// (the team directs users away from globals toward `yarn dlx`
|
|
102
|
+
// for one-offs). For users who DO want a persistent global,
|
|
103
|
+
// `npm install -g` is the universal fallback that works
|
|
104
|
+
// alongside yarn berry without conflict.
|
|
105
|
+
globalInstallCommand: "npm install -g skillrepo",
|
|
106
|
+
}),
|
|
107
|
+
Object.freeze({
|
|
108
|
+
name: "bunx",
|
|
109
|
+
cacheSubstrings: Object.freeze([
|
|
110
|
+
"/.bun/install/cache/",
|
|
111
|
+
"\\.bun\\install\\cache\\",
|
|
112
|
+
]),
|
|
113
|
+
commandSuffixes: Object.freeze(["/bunx", "\\bunx"]),
|
|
114
|
+
globalInstallCommand: "bun add -g skillrepo",
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Look up the canonical global-install command for a runner display
|
|
120
|
+
* name (as returned by `detectTransientRunner`). Returns null when
|
|
121
|
+
* the name doesn't match any registered runner — caller should fall
|
|
122
|
+
* back to the universal `npm install -g skillrepo` text.
|
|
123
|
+
*
|
|
124
|
+
* @param {string | null} runnerName
|
|
125
|
+
* @returns {string | null}
|
|
126
|
+
*/
|
|
127
|
+
export function globalInstallCommandFor(runnerName) {
|
|
128
|
+
if (!runnerName) return null;
|
|
129
|
+
const runner = TRANSIENT_RUNNERS.find((r) => r.name === runnerName);
|
|
130
|
+
return runner ? runner.globalInstallCommand : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Identify the transient runner that launched the current process,
|
|
135
|
+
* if any.
|
|
136
|
+
*
|
|
137
|
+
* Detection signals (first match wins):
|
|
138
|
+
* 1. `process.argv[1]` contains one of the runner's
|
|
139
|
+
* `cacheSubstrings`. The executable's path itself names the
|
|
140
|
+
* runner's cache directory.
|
|
141
|
+
* 2. `process.env._` ends with one of the runner's
|
|
142
|
+
* `commandSuffixes`. Defensive against shim layouts where
|
|
143
|
+
* `argv[1]` has been symlinked through a path that doesn't
|
|
144
|
+
* contain the cache substring.
|
|
145
|
+
*
|
|
146
|
+
* Why NOT `process.env.npm_command === "exec"`: that signal also
|
|
147
|
+
* fires for stable-install users running `npm exec skillrepo ...`
|
|
148
|
+
* directly or via a `package.json` lifecycle script. Adding it
|
|
149
|
+
* trades a minor coverage gain for a real false-positive surface
|
|
150
|
+
* affecting users with a real global install.
|
|
151
|
+
*
|
|
152
|
+
* @param {object} [options]
|
|
153
|
+
* @param {string[]} [options.argv=process.argv] - Test override.
|
|
154
|
+
* @param {NodeJS.ProcessEnv} [options.env=process.env] - Test override.
|
|
155
|
+
* @returns {string | null} The runner's display name, or `null` if
|
|
156
|
+
* the process is NOT a transient invocation.
|
|
157
|
+
*/
|
|
158
|
+
export function detectTransientRunner({
|
|
159
|
+
argv = process.argv,
|
|
160
|
+
env = process.env,
|
|
161
|
+
} = {}) {
|
|
162
|
+
const execPath = argv[1] ?? "";
|
|
163
|
+
for (const runner of TRANSIENT_RUNNERS) {
|
|
164
|
+
for (const substring of runner.cacheSubstrings) {
|
|
165
|
+
if (execPath.includes(substring)) return runner.name;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const underscore = env._ ?? "";
|
|
169
|
+
for (const runner of TRANSIENT_RUNNERS) {
|
|
170
|
+
for (const suffix of runner.commandSuffixes) {
|
|
171
|
+
if (underscore.endsWith(suffix)) return runner.name;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Boolean shortcut over `detectTransientRunner`. Use when a caller
|
|
179
|
+
* only needs to know "is this a transient invocation at all?" and
|
|
180
|
+
* doesn't care which runner.
|
|
181
|
+
*
|
|
182
|
+
* @returns {boolean}
|
|
183
|
+
*/
|
|
184
|
+
export function isTransientRunnerInvocation() {
|
|
185
|
+
return detectTransientRunner() !== null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* True when `absPath` is inside any registered runner's transient
|
|
190
|
+
* cache directory. Used by the binary locator to filter
|
|
191
|
+
* `where`/`which` output so a cache-located binary doesn't get
|
|
192
|
+
* baked into a long-lived hook command.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} absPath - An absolute filesystem path.
|
|
195
|
+
* @returns {boolean}
|
|
196
|
+
*/
|
|
197
|
+
export function isTransientCachePath(absPath) {
|
|
198
|
+
for (const runner of TRANSIENT_RUNNERS) {
|
|
199
|
+
for (const substring of runner.cacheSubstrings) {
|
|
200
|
+
if (absPath.includes(substring)) return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
@@ -13,12 +13,18 @@ import { resolvePlacementDir } from "../../lib/file-write.mjs";
|
|
|
13
13
|
import { CliError, EXIT_VALIDATION, EXIT_AUTH, EXIT_SCOPE } from "../../lib/errors.mjs";
|
|
14
14
|
import { createMockServer } from "../e2e/mock-server.mjs";
|
|
15
15
|
import { createCaptureStream } from "../helpers/capture-stream.mjs";
|
|
16
|
+
import {
|
|
17
|
+
captureHome,
|
|
18
|
+
setSandboxHome,
|
|
19
|
+
restoreHome,
|
|
20
|
+
} from "../helpers/sandbox-home.mjs";
|
|
16
21
|
|
|
17
22
|
let sandbox;
|
|
18
23
|
let server;
|
|
19
24
|
let serverUrl;
|
|
20
25
|
let originalCwd;
|
|
21
|
-
|
|
26
|
+
/** @type {import("../helpers/sandbox-home.mjs").HomeEnvSnapshot} */
|
|
27
|
+
let originalHomeEnv;
|
|
22
28
|
let stdout;
|
|
23
29
|
const VALID_KEY = "sk_live_test";
|
|
24
30
|
|
|
@@ -46,9 +52,9 @@ async function setup() {
|
|
|
46
52
|
mkdirSync(join(sandbox, "project"), { recursive: true });
|
|
47
53
|
mkdirSync(join(sandbox, "home"), { recursive: true });
|
|
48
54
|
originalCwd = process.cwd();
|
|
49
|
-
|
|
55
|
+
originalHomeEnv = captureHome();
|
|
50
56
|
process.chdir(join(sandbox, "project"));
|
|
51
|
-
|
|
57
|
+
setSandboxHome(join(sandbox, "home"));
|
|
52
58
|
delete process.env.SKILLREPO_ACCESS_KEY;
|
|
53
59
|
|
|
54
60
|
server = createMockServer({});
|
|
@@ -61,7 +67,7 @@ async function setup() {
|
|
|
61
67
|
async function teardown() {
|
|
62
68
|
if (server) await server.stop();
|
|
63
69
|
process.chdir(originalCwd);
|
|
64
|
-
|
|
70
|
+
restoreHome(originalHomeEnv);
|
|
65
71
|
if (sandbox) rmSync(sandbox, { recursive: true, force: true });
|
|
66
72
|
server = null;
|
|
67
73
|
}
|
|
@@ -13,12 +13,18 @@ import { resolvePlacementDir } from "../../lib/file-write.mjs";
|
|
|
13
13
|
import { CliError, EXIT_VALIDATION } from "../../lib/errors.mjs";
|
|
14
14
|
import { createMockServer } from "../e2e/mock-server.mjs";
|
|
15
15
|
import { createCaptureStream } from "../helpers/capture-stream.mjs";
|
|
16
|
+
import {
|
|
17
|
+
captureHome,
|
|
18
|
+
setSandboxHome,
|
|
19
|
+
restoreHome,
|
|
20
|
+
} from "../helpers/sandbox-home.mjs";
|
|
16
21
|
|
|
17
22
|
let sandbox;
|
|
18
23
|
let server;
|
|
19
24
|
let serverUrl;
|
|
20
25
|
let originalCwd;
|
|
21
|
-
|
|
26
|
+
/** @type {import("../helpers/sandbox-home.mjs").HomeEnvSnapshot} */
|
|
27
|
+
let originalHomeEnv;
|
|
22
28
|
let stdout;
|
|
23
29
|
const VALID_KEY = "sk_live_test";
|
|
24
30
|
|
|
@@ -53,9 +59,9 @@ async function setup() {
|
|
|
53
59
|
mkdirSync(join(sandbox, "project"), { recursive: true });
|
|
54
60
|
mkdirSync(join(sandbox, "home"), { recursive: true });
|
|
55
61
|
originalCwd = process.cwd();
|
|
56
|
-
|
|
62
|
+
originalHomeEnv = captureHome();
|
|
57
63
|
process.chdir(join(sandbox, "project"));
|
|
58
|
-
|
|
64
|
+
setSandboxHome(join(sandbox, "home"));
|
|
59
65
|
delete process.env.SKILLREPO_ACCESS_KEY;
|
|
60
66
|
delete process.env.SKILLREPO_URL;
|
|
61
67
|
|
|
@@ -69,7 +75,7 @@ async function setup() {
|
|
|
69
75
|
async function teardown() {
|
|
70
76
|
if (server) await server.stop();
|
|
71
77
|
process.chdir(originalCwd);
|
|
72
|
-
|
|
78
|
+
restoreHome(originalHomeEnv);
|
|
73
79
|
if (sandbox) rmSync(sandbox, { recursive: true, force: true });
|
|
74
80
|
server = null;
|
|
75
81
|
}
|