skillrepo 3.1.1 → 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 +4 -2
- package/package.json +1 -1
- package/src/commands/init-session-sync.mjs +307 -0
- package/src/commands/init.mjs +74 -111
- package/src/commands/session-sync-actions.mjs +92 -0
- package/src/lib/binary-locator.mjs +99 -0
- package/src/lib/cli-config.mjs +7 -72
- package/src/lib/cli-version.mjs +56 -0
- package/src/lib/global-install.mjs +387 -0
- package/src/lib/mcp-merge.mjs +16 -5
- package/src/lib/mergers/session-hook.mjs +80 -68
- package/src/lib/transient-runners.mjs +204 -0
- package/src/test/commands/init.test.mjs +662 -1
- package/src/test/commands/session-sync-actions.test.mjs +74 -0
- package/src/test/helpers/mock-spawn.mjs +121 -0
- package/src/test/lib/cli-config.test.mjs +66 -9
- package/src/test/lib/cli-version.test.mjs +47 -0
- package/src/test/lib/global-install.test.mjs +424 -0
- package/src/test/lib/mcp-merge.test.mjs +3 -3
- package/src/test/lib/transient-runners.test.mjs +270 -0
- package/src/test/mergers/session-hook.test.mjs +284 -14
|
@@ -70,8 +70,6 @@
|
|
|
70
70
|
*/
|
|
71
71
|
|
|
72
72
|
import { existsSync, readFileSync } from "node:fs";
|
|
73
|
-
import { execFileSync } from "node:child_process";
|
|
74
|
-
import { isAbsolute } from "node:path";
|
|
75
73
|
import { writeFileAtomic } from "../fs-utils.mjs";
|
|
76
74
|
import { SESSION_HOOK_FINGERPRINT } from "../artifact-registry.mjs";
|
|
77
75
|
import {
|
|
@@ -80,7 +78,7 @@ import {
|
|
|
80
78
|
} from "../paths.mjs";
|
|
81
79
|
import { diskError, validationError } from "../errors.mjs";
|
|
82
80
|
import { removeSettingsSessionHook } from "../removers/settings.mjs";
|
|
83
|
-
import {
|
|
81
|
+
import { resolveBinaryOnPath } from "../binary-locator.mjs";
|
|
84
82
|
import { platformConventions } from "../platform.mjs";
|
|
85
83
|
|
|
86
84
|
/**
|
|
@@ -90,15 +88,31 @@ import { platformConventions } from "../platform.mjs";
|
|
|
90
88
|
* Shell shape is platform-specific — see `platform.mjs` for the full
|
|
91
89
|
* rationale. Summary:
|
|
92
90
|
*
|
|
93
|
-
* - **POSIX** (macOS, Linux):
|
|
91
|
+
* - **POSIX** (macOS, Linux): `'<path>' update --session-hook 2>&1 || true`.
|
|
94
92
|
* `|| true` catches any non-zero exit at the shell level; primary
|
|
95
93
|
* defense is the `--session-hook` flag contract in the Node process.
|
|
96
|
-
* - **Windows** (cmd.exe / PowerShell):
|
|
94
|
+
* - **Windows** (cmd.exe / PowerShell): `"<path>" update --session-hook 2>&1`.
|
|
97
95
|
* `|| true` omitted because cmd.exe doesn't know the `true` builtin.
|
|
98
96
|
* `--session-hook` contract is the only defense; consequences of
|
|
99
97
|
* binary-vanished scenarios are slightly noisier in Claude Code's
|
|
100
98
|
* session log but still non-blocking.
|
|
101
99
|
*
|
|
100
|
+
* Path quoting is mandatory: real-world install paths contain spaces
|
|
101
|
+
* (`C:\Program Files\nodejs\skillrepo.cmd`, `/Users/First Last/.npm-global/bin/skillrepo`)
|
|
102
|
+
* and parentheses (`C:\Program Files (x86)\...`). An unquoted path
|
|
103
|
+
* makes the shell parse the command as multiple arguments and the
|
|
104
|
+
* hook silently fails on session start.
|
|
105
|
+
*
|
|
106
|
+
* The fingerprint constant (`SESSION_HOOK_FINGERPRINT` =
|
|
107
|
+
* ` update --session-hook` with leading space) MUST appear in the
|
|
108
|
+
* resulting command for the uninstaller and idempotency walks to
|
|
109
|
+
* find it. Single/double quotes don't break the fingerprint because
|
|
110
|
+
* the leading space sits between the closing quote and `update`.
|
|
111
|
+
* Backward-compat: existing v3.1.0/v3.1.1 hooks with unquoted paths
|
|
112
|
+
* also match the fingerprint (the space sits between the path and
|
|
113
|
+
* `update`), so re-running init detects them and updates in place
|
|
114
|
+
* to the new quoted shape.
|
|
115
|
+
*
|
|
102
116
|
* The suffix is supplied by `platformConventions().hookShellSuffix` —
|
|
103
117
|
* this function doesn't know which OS it's targeting, it just
|
|
104
118
|
* concatenates the convention's suffix.
|
|
@@ -116,69 +130,57 @@ export function buildHookCommand(binaryPath, { platform: platformOverride } = {}
|
|
|
116
130
|
);
|
|
117
131
|
}
|
|
118
132
|
const conv = platformConventions({ platform: platformOverride });
|
|
119
|
-
|
|
133
|
+
const quotedPath = quoteShellPath(binaryPath, conv.family);
|
|
134
|
+
return `${quotedPath} update --session-hook 2>&1${conv.hookShellSuffix}`;
|
|
120
135
|
}
|
|
121
136
|
|
|
122
137
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* without a global install) — the caller should skip hook installation
|
|
126
|
-
* with a clear warning rather than fail init.
|
|
138
|
+
* Quote a filesystem path for inclusion in a shell command, using the
|
|
139
|
+
* conventions of the target shell family.
|
|
127
140
|
*
|
|
128
|
-
*
|
|
141
|
+
* - **POSIX**: wrap in single quotes; escape any embedded single
|
|
142
|
+
* quote with the standard `'\''` trick (close quote, escaped
|
|
143
|
+
* literal quote, reopen quote). Single quotes suppress ALL shell
|
|
144
|
+
* interpretation, so spaces, `$`, `*`, parens, double quotes,
|
|
145
|
+
* backticks, and backslashes pass through verbatim.
|
|
146
|
+
*
|
|
147
|
+
* - **Windows** (cmd.exe): wrap in double quotes; escape any
|
|
148
|
+
* embedded double quote with `\"`. Backslashes inside the path
|
|
149
|
+
* pass through unchanged (cmd.exe does not interpret backslashes
|
|
150
|
+
* as escapes inside double quotes). Filesystem rules forbid
|
|
151
|
+
* literal `"` in NTFS path components, so the `\"` escape is
|
|
152
|
+
* defensive — paths in the wild won't contain it.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} path
|
|
155
|
+
* @param {"posix" | "windows"} family
|
|
156
|
+
* @returns {string} The quoted path, ready to be interpolated into
|
|
157
|
+
* a shell command.
|
|
129
158
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
// checks only) so it doesn't need the conventions object.
|
|
134
|
-
if (isNpxInvocation()) {
|
|
135
|
-
return null;
|
|
159
|
+
function quoteShellPath(path, family) {
|
|
160
|
+
if (family === "windows") {
|
|
161
|
+
return `"${path.replace(/"/g, '\\"')}"`;
|
|
136
162
|
}
|
|
163
|
+
// POSIX
|
|
164
|
+
return `'${path.replace(/'/g, "'\\''")}'`;
|
|
165
|
+
}
|
|
137
166
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
});
|
|
156
|
-
// Windows `where` can return multiple matching paths (one per
|
|
157
|
-
// PATH entry containing the binary) on separate lines. Take
|
|
158
|
-
// only the first. `which` always returns a single path but the
|
|
159
|
-
// split is harmless there. This is the one line where the
|
|
160
|
-
// platform difference actually leaks through — all platforms
|
|
161
|
-
// receive potentially-multi-line output that we canonicalize
|
|
162
|
-
// the same way.
|
|
163
|
-
const result = raw.split(/\r?\n/)[0].trim();
|
|
164
|
-
if (!result) return null;
|
|
165
|
-
// Sanity: the resolved path must be absolute. A relative
|
|
166
|
-
// result would be meaningless at session-start time because
|
|
167
|
-
// the Claude Code hook runner's cwd is undefined. `isAbsolute`
|
|
168
|
-
// handles both POSIX (`/foo/bar`) and Windows (`C:\foo\bar`)
|
|
169
|
-
// path styles — it's Node's built-in cross-platform check,
|
|
170
|
-
// not a platform-conditional we need to own.
|
|
171
|
-
if (!isAbsolute(result)) return null;
|
|
172
|
-
return result;
|
|
173
|
-
} catch {
|
|
174
|
-
// Locator exits non-zero if the binary isn't on PATH, or throws
|
|
175
|
-
// ENOENT if the locator itself isn't available (e.g. a minimal
|
|
176
|
-
// container image without `which`, or a Windows system with
|
|
177
|
-
// `where.exe` missing which is effectively never — but still
|
|
178
|
-
// safe-handled). Either way: null → caller routes to the
|
|
179
|
-
// architect-specified "requires stable install" skip message.
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
167
|
+
/**
|
|
168
|
+
* Resolve the absolute path of the `skillrepo` binary on PATH,
|
|
169
|
+
* skipping resolution entirely when the current process is itself
|
|
170
|
+
* a transient-runner invocation (npx, pnpx, yarn dlx, bunx) — those
|
|
171
|
+
* cache paths must not be baked into the long-lived hook command.
|
|
172
|
+
*
|
|
173
|
+
* Thin wrapper over `resolveBinaryOnPath` with the
|
|
174
|
+
* `skipIfTransient` flag preset. Kept as a named export for
|
|
175
|
+
* call-site readability and so existing tests keep working.
|
|
176
|
+
*
|
|
177
|
+
* @returns {string | null}
|
|
178
|
+
*/
|
|
179
|
+
export function resolveSkillrepoBinary({ platform: platformOverride } = {}) {
|
|
180
|
+
return resolveBinaryOnPath("skillrepo", {
|
|
181
|
+
skipIfTransient: true,
|
|
182
|
+
platform: platformOverride,
|
|
183
|
+
});
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
/**
|
|
@@ -237,18 +239,28 @@ export function mergeSessionHook({
|
|
|
237
239
|
// 2. `which skillrepo` returned nothing — no global install
|
|
238
240
|
// exists at all.
|
|
239
241
|
// Both are the same problem from the hook's perspective: we
|
|
240
|
-
// can't produce a command that will still work later.
|
|
241
|
-
//
|
|
242
|
-
//
|
|
242
|
+
// can't produce a command that will still work later.
|
|
243
|
+
//
|
|
244
|
+
// v3.1.2 (#894): `init` bypasses this path under npx by running
|
|
245
|
+
// `npm install -g skillrepo@<version>` itself and then calling
|
|
246
|
+
// `mergeSessionHook` with the resulting `binaryPath` explicitly.
|
|
247
|
+
// The remaining callers that hit this branch are:
|
|
248
|
+
// - `skillrepo session-sync enable` invoked under npx (does
|
|
249
|
+
// not auto-install — a deliberate, explicit user invocation
|
|
250
|
+
// should not silently mutate global package state).
|
|
251
|
+
// - The rare bare-install case where `which skillrepo` fails
|
|
252
|
+
// even though we're not under npx (PATH misconfiguration).
|
|
253
|
+
//
|
|
254
|
+
// Both cases get the same actionable hint pointing the user at
|
|
255
|
+
// `skillrepo init`, which DOES auto-install under npx.
|
|
243
256
|
return {
|
|
244
257
|
path: displayPath,
|
|
245
258
|
action: "skipped",
|
|
246
259
|
reason:
|
|
247
260
|
"Session sync requires a stable `skillrepo` binary on PATH. " +
|
|
248
|
-
"
|
|
249
|
-
"
|
|
250
|
-
"
|
|
251
|
-
"`skillrepo session-sync enable`.",
|
|
261
|
+
"Run `npm install -g skillrepo` (or use `skillrepo init`, " +
|
|
262
|
+
"which offers to install globally for you under npx), then " +
|
|
263
|
+
"re-run `skillrepo session-sync enable`.",
|
|
252
264
|
};
|
|
253
265
|
}
|
|
254
266
|
|
|
@@ -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
|
+
}
|