@aexol/spectral 0.9.205 → 0.9.206
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/agent/agents.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAC/C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChD,QAAQ,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,eAAO,MAAM,aAAa,EAAE,WAAW,
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/agent/agents.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAC/C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC;IAChD,QAAQ,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED,eAAO,MAAM,aAAa,EAAE,WAAW,EAmDtC,CAAC;AA0DF,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAO3E;AACD;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,yEAAyE;IACzE,YAAY,EAAE,IAAI,CAAC;IACnB,6CAA6C;IAC7C,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG;IACF,YAAY,EAAE,KAAK,CAAC;IACpB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA6EnC;AAkBD,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,EACjB,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,oBAAoB,CAmBtB;AACD,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,KAAK,GAAE,UAAmB,EAC1B,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,WAAW,GAAG,IAAI,CAepB;AACD;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACvC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,WAAW,GAAG,IAAI,CAgBpB;AACD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,YAAY,EAAE,IAAI,CAChB,WAAW,EACX,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAC7D,GAAG,IAAI,GACP,MAAM,GAAG,SAAS,CAQpB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,IAAI,CACT,WAAW,EACX,MAAM,GAAG,aAAa,GAAG,cAAc,GAAG,OAAO,GAAG,QAAQ,CAC7D,GAAG,IAAI,GACP,MAAM,CAgBR"}
|
package/dist/agent/agents.js
CHANGED
|
@@ -6,9 +6,9 @@ import { classifyGraphqlError, createBackendClient, graphqlErrorMessage, scalars
|
|
|
6
6
|
export const SYSTEM_AGENTS = [
|
|
7
7
|
{
|
|
8
8
|
name: "ghost",
|
|
9
|
-
description: "Built-in orchestrator primary agent that delegates work to researcher, executor, and
|
|
9
|
+
description: "Built-in orchestrator primary agent that delegates work to researcher, executor, reviewer, and fork subagents.",
|
|
10
10
|
mode: "primary",
|
|
11
|
-
systemPrompt: "You are ghost, the built-in system orchestrator. Plan the work, then delegate by calling the `subagent` tool with one of your subagents, then integrate the results into a coherent outcome.\n\nYour subagents (call them via the `subagent` tool, e.g. subagent(agent=\"researcher\", task=\"...\")):\n- `researcher` — investigate the codebase, read code and documentation, and report precise, cited findings without making changes.\n- `executor` — implement the requested changes minimally and safely, run verification, and report exactly what changed with evidence.\n- `reviewer` — review proposed or implemented changes for correctness, edge cases, security, and consistency with the codebase.\n
|
|
11
|
+
systemPrompt: "You are ghost, the built-in system orchestrator. Plan the work, then delegate by calling the `subagent` tool with one of your subagents, then integrate the results into a coherent outcome.\n\nYour subagents (call them via the `subagent` tool, e.g. subagent(agent=\"researcher\", task=\"...\")):\n- `researcher` — investigate the codebase, read code and documentation, and report precise, cited findings without making changes.\n- `executor` — implement the requested changes minimally and safely, run verification, and report exactly what changed with evidence.\n- `reviewer` — review proposed or implemented changes for correctness, edge cases, security, and consistency with the codebase.\n- `fork` — a generalist worker with the same broad capabilities as the main thread; give it a self-contained task and it carries it end-to-end.\n\nTry to delegate most of the work to your subagents: use `researcher` for research/investigation, `executor` for implementation, `reviewer` for review, and `fork` for generalist tasks that don't fit the other three.",
|
|
12
12
|
source: "system",
|
|
13
13
|
filePath: "system:ghost",
|
|
14
14
|
},
|
|
@@ -36,6 +36,14 @@ export const SYSTEM_AGENTS = [
|
|
|
36
36
|
source: "system",
|
|
37
37
|
filePath: "system:reviewer",
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
name: "fork",
|
|
41
|
+
description: "Built-in system generalist subagent — works like the main thread on a delegated task.",
|
|
42
|
+
mode: "subagent",
|
|
43
|
+
systemPrompt: "You are the fork subagent for ghost — a generalist with the same broad capabilities as the main thread. Own the delegated task end-to-end: investigate what you need, do the work, verify the result, and report the outcome with evidence.",
|
|
44
|
+
source: "system",
|
|
45
|
+
filePath: "system:fork",
|
|
46
|
+
},
|
|
39
47
|
];
|
|
40
48
|
function loadAgentsFromDir(dir, source) {
|
|
41
49
|
const agents = [];
|
|
@@ -3,5 +3,34 @@ export interface NpxResolution {
|
|
|
3
3
|
extraArgs: string[];
|
|
4
4
|
isJs: boolean;
|
|
5
5
|
}
|
|
6
|
-
export
|
|
6
|
+
export interface ResolveOptions {
|
|
7
|
+
/** Override for the global node_modules root (tests / custom environments). */
|
|
8
|
+
globalRoot?: string | null;
|
|
9
|
+
/** Override for the npm cache dir (tests / custom environments). */
|
|
10
|
+
npmCacheDir?: string | null;
|
|
11
|
+
/** Override for the registry "latest version" lookup (tests / offline). */
|
|
12
|
+
latestVersionFetcher?: (packageName: string) => Promise<string | null>;
|
|
13
|
+
/** Override for the npx-cache population step (tests). */
|
|
14
|
+
forceInstall?: () => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
interface ParsedInvocation {
|
|
17
|
+
packageSpec: string;
|
|
18
|
+
binName?: string;
|
|
19
|
+
extraArgs: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function resolveNpxBinary(command: string, args: string[], options?: ResolveOptions): Promise<NpxResolution | null>;
|
|
22
|
+
export declare function parseNpxArgs(args: string[]): ParsedInvocation | null;
|
|
23
|
+
export declare function parseNpmExecArgs(args: string[]): ParsedInvocation | null;
|
|
24
|
+
export declare function extractPackageName(spec: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* A "floating" spec is one without a version pin: bare name, `@latest` or `@*`.
|
|
27
|
+
* Only floating specs may resolve from the global tree and only they get the
|
|
28
|
+
* registry freshness check — a pinned spec must match its exact version.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isFloatingSpec(spec: string): boolean;
|
|
31
|
+
/** Reset per-process memoizations (used by tests). */
|
|
32
|
+
export declare function resetNpxResolverCachesForTests(): void;
|
|
33
|
+
/** Semver-ish comparison; release beats prerelease, identifiers compare numerically when both numeric. */
|
|
34
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
35
|
+
export {};
|
|
7
36
|
//# sourceMappingURL=npx-resolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"npx-resolver.d.ts","sourceRoot":"","sources":["../../src/mcp/npx-resolver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"npx-resolver.d.ts","sourceRoot":"","sources":["../../src/mcp/npx-resolver.ts"],"names":[],"mappings":"AAqCA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,2EAA2E;IAC3E,oBAAoB,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvE,0DAA0D;IAC1D,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,UAAU,gBAAgB;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA0D/B;AAMD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,IAAI,CAkDpE;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,GAAG,IAAI,CAmCxE;AAqKD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc9D;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAKpD;AAqED,sDAAsD;AACtD,wBAAgB,8BAA8B,IAAI,IAAI,CAGrD;AA+GD,0GAA0G;AAC1G,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAW5D"}
|
package/dist/mcp/npx-resolver.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
// npx-resolver.ts - Resolve npx/npm exec binaries to avoid npm parent processes
|
|
2
|
+
//
|
|
3
|
+
// Resolution order (mirrors `npm exec` semantics, minus the local project tree):
|
|
4
|
+
// 1. Global install (`<prefix>/lib/node_modules/<pkg>`) — beats the npx cache,
|
|
5
|
+
// so `npm i -g pkg@latest` takes effect on the next server (re)connect.
|
|
6
|
+
// Pinned specs (`pkg@1.2.3`) never resolve from global.
|
|
7
|
+
// 2. Own resolution cache (`~/.spectral/agent/mcp-npx-cache.json`, 24h TTL) —
|
|
8
|
+
// hot path, no registry traffic.
|
|
9
|
+
// 3. npx cache scan (`~/.npm/_npx/*`); floating specs (`pkg`, `pkg@latest`,
|
|
10
|
+
// `pkg@*`) get a registry freshness check once per TTL window and the
|
|
11
|
+
// cache is refreshed via `npm exec` when a newer version exists.
|
|
12
|
+
// 4. Slow path: force-populate the npx cache, then re-scan.
|
|
2
13
|
import { existsSync, readFileSync, realpathSync, readdirSync, statSync, writeFileSync, renameSync, mkdirSync, openSync, readSync, closeSync } from "node:fs";
|
|
3
14
|
import { join, dirname, extname, resolve, sep } from "node:path";
|
|
4
15
|
import { getAgentPath } from "./agent-dir.js";
|
|
5
16
|
import spawn from "cross-spawn";
|
|
6
17
|
const CACHE_VERSION = 1;
|
|
7
18
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
8
|
-
|
|
19
|
+
const CACHE_PRUNE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
20
|
+
const REGISTRY_CHECK_TIMEOUT_MS = 10_000;
|
|
21
|
+
const GLOBAL_ROOT_TIMEOUT_MS = 5_000;
|
|
22
|
+
const FORCE_CACHE_TIMEOUT_MS = 30_000;
|
|
23
|
+
export async function resolveNpxBinary(command, args, options = {}) {
|
|
9
24
|
const parsed = command === "npx"
|
|
10
25
|
? parseNpxArgs(args)
|
|
11
26
|
: command === "npm"
|
|
@@ -16,24 +31,52 @@ export async function resolveNpxBinary(command, args) {
|
|
|
16
31
|
const cacheKey = JSON.stringify([command, ...args]);
|
|
17
32
|
const cache = loadCache();
|
|
18
33
|
const cached = cache?.entries?.[cacheKey];
|
|
34
|
+
// 1. Global install wins (npm exec semantics: global tree beats npx cache).
|
|
35
|
+
const global = resolveFromGlobal(parsed.packageSpec, parsed.binName, options.globalRoot);
|
|
36
|
+
if (global) {
|
|
37
|
+
if (!cached || cached.resolvedBin !== global.resolvedBin) {
|
|
38
|
+
saveCacheEntry(cacheKey, global);
|
|
39
|
+
}
|
|
40
|
+
return toResolution(global, parsed.extraArgs);
|
|
41
|
+
}
|
|
42
|
+
// 2. Own resolution cache (hot path — no registry traffic, TTL-governed).
|
|
19
43
|
if (cached && Date.now() - cached.resolvedAt < CACHE_TTL_MS && existsSync(cached.resolvedBin)) {
|
|
20
|
-
return
|
|
44
|
+
return toResolution(cached, parsed.extraArgs);
|
|
45
|
+
}
|
|
46
|
+
// 3. npx cache scan; floating specs get a registry freshness check on refresh.
|
|
47
|
+
const scanned = resolveFromNpmCache(parsed.packageSpec, parsed.binName, options.npmCacheDir);
|
|
48
|
+
if (scanned && isFloatingSpec(parsed.packageSpec) && scanned.packageVersion) {
|
|
49
|
+
const fetcher = options.latestVersionFetcher ?? fetchLatestVersion;
|
|
50
|
+
const packageName = extractPackageName(parsed.packageSpec) ?? parsed.packageSpec;
|
|
51
|
+
const latest = await fetcher(packageName);
|
|
52
|
+
if (latest && compareVersions(latest, scanned.packageVersion) > 0) {
|
|
53
|
+
const force = options.forceInstall ?? (() => forceNpxCache(parsed.packageSpec));
|
|
54
|
+
await force();
|
|
55
|
+
const refreshed = resolveFromNpmCache(parsed.packageSpec, parsed.binName, options.npmCacheDir);
|
|
56
|
+
if (refreshed) {
|
|
57
|
+
saveCacheEntry(cacheKey, refreshed);
|
|
58
|
+
return toResolution(refreshed, parsed.extraArgs);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
21
61
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
return { binPath: resolved.resolvedBin, extraArgs: parsed.extraArgs, isJs: resolved.isJs };
|
|
62
|
+
if (scanned) {
|
|
63
|
+
saveCacheEntry(cacheKey, scanned);
|
|
64
|
+
return toResolution(scanned, parsed.extraArgs);
|
|
26
65
|
}
|
|
27
|
-
// Slow path: force npx cache population
|
|
28
|
-
|
|
29
|
-
|
|
66
|
+
// 4. Slow path: force npx cache population.
|
|
67
|
+
const force = options.forceInstall ?? (() => forceNpxCache(parsed.packageSpec));
|
|
68
|
+
await force();
|
|
69
|
+
const resolvedAfterInstall = resolveFromNpmCache(parsed.packageSpec, parsed.binName, options.npmCacheDir);
|
|
30
70
|
if (resolvedAfterInstall) {
|
|
31
71
|
saveCacheEntry(cacheKey, resolvedAfterInstall);
|
|
32
|
-
return
|
|
72
|
+
return toResolution(resolvedAfterInstall, parsed.extraArgs);
|
|
33
73
|
}
|
|
34
74
|
return null;
|
|
35
75
|
}
|
|
36
|
-
function
|
|
76
|
+
function toResolution(entry, extraArgs) {
|
|
77
|
+
return { binPath: entry.resolvedBin, extraArgs, isJs: entry.isJs };
|
|
78
|
+
}
|
|
79
|
+
export function parseNpxArgs(args) {
|
|
37
80
|
const separatorIndex = args.indexOf("--");
|
|
38
81
|
const before = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
|
|
39
82
|
const after = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
|
|
@@ -87,7 +130,7 @@ function parseNpxArgs(args) {
|
|
|
87
130
|
const extraArgs = positionals.slice(1).concat(after);
|
|
88
131
|
return { packageSpec: packagePositional, extraArgs };
|
|
89
132
|
}
|
|
90
|
-
function parseNpmExecArgs(args) {
|
|
133
|
+
export function parseNpmExecArgs(args) {
|
|
91
134
|
if (args[0] !== "exec")
|
|
92
135
|
return null;
|
|
93
136
|
const execArgs = args.slice(1);
|
|
@@ -128,8 +171,65 @@ function parseNpmExecArgs(args) {
|
|
|
128
171
|
const extraArgs = after.slice(1);
|
|
129
172
|
return { packageSpec, binName, extraArgs };
|
|
130
173
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
174
|
+
function pickBinEntry(binField, packageName, explicitBin) {
|
|
175
|
+
if (typeof binField === "string") {
|
|
176
|
+
return { name: defaultBinName(packageName), rel: binField };
|
|
177
|
+
}
|
|
178
|
+
for (const candidate of buildBinCandidates(packageName, explicitBin)) {
|
|
179
|
+
if (binField[candidate]) {
|
|
180
|
+
return { name: candidate, rel: binField[candidate] };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const firstEntry = Object.entries(binField)[0];
|
|
184
|
+
if (firstEntry) {
|
|
185
|
+
return { name: firstEntry[0], rel: firstEntry[1] };
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Resolve the package from the npm global install tree. Global beats the npx
|
|
191
|
+
* cache, but only floating specs (`pkg`, `pkg@latest`, `pkg@*`) may resolve
|
|
192
|
+
* from global — a pinned spec must match its exact version.
|
|
193
|
+
*/
|
|
194
|
+
function resolveFromGlobal(packageSpec, binName, globalRootOverride) {
|
|
195
|
+
if (!isFloatingSpec(packageSpec))
|
|
196
|
+
return null;
|
|
197
|
+
const packageName = extractPackageName(packageSpec);
|
|
198
|
+
if (!packageName)
|
|
199
|
+
return null;
|
|
200
|
+
const root = globalRootOverride ?? getGlobalRoot();
|
|
201
|
+
if (!root)
|
|
202
|
+
return null;
|
|
203
|
+
const pkgDir = join(root, ...packageName.split("/"));
|
|
204
|
+
const packageJsonPath = join(pkgDir, "package.json");
|
|
205
|
+
if (!existsSync(packageJsonPath))
|
|
206
|
+
return null;
|
|
207
|
+
let pkg = null;
|
|
208
|
+
try {
|
|
209
|
+
pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const binField = pkg?.bin;
|
|
215
|
+
if (!binField)
|
|
216
|
+
return null;
|
|
217
|
+
const picked = pickBinEntry(binField, packageName, binName);
|
|
218
|
+
if (!picked)
|
|
219
|
+
return null;
|
|
220
|
+
const resolvedBin = resolve(pkgDir, picked.rel);
|
|
221
|
+
if (!existsSync(resolvedBin))
|
|
222
|
+
return null;
|
|
223
|
+
return {
|
|
224
|
+
resolvedBin,
|
|
225
|
+
resolvedAt: Date.now(),
|
|
226
|
+
packageVersion: pkg?.version,
|
|
227
|
+
isJs: detectJsBinary(resolvedBin),
|
|
228
|
+
source: "global",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function resolveFromNpmCache(packageSpec, binName, cacheDirOverride) {
|
|
232
|
+
const cacheDir = cacheDirOverride ?? getNpmCacheDir();
|
|
133
233
|
if (!cacheDir)
|
|
134
234
|
return null;
|
|
135
235
|
const packageName = extractPackageName(packageSpec);
|
|
@@ -151,59 +251,36 @@ function resolveFromNpmCache(packageSpec, binName) {
|
|
|
151
251
|
const binField = pkg?.bin;
|
|
152
252
|
if (!binField)
|
|
153
253
|
return null;
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
let binRel;
|
|
157
|
-
if (typeof binField === "string") {
|
|
158
|
-
chosenBinName = defaultBinName(packageName);
|
|
159
|
-
binRel = binField;
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
for (const candidate of candidates) {
|
|
163
|
-
if (binField[candidate]) {
|
|
164
|
-
chosenBinName = candidate;
|
|
165
|
-
binRel = binField[candidate];
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (!binRel) {
|
|
170
|
-
const firstEntry = Object.entries(binField)[0];
|
|
171
|
-
if (firstEntry) {
|
|
172
|
-
chosenBinName = firstEntry[0];
|
|
173
|
-
binRel = firstEntry[1];
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (!binRel)
|
|
254
|
+
const picked = pickBinEntry(binField, packageName, binName);
|
|
255
|
+
if (!picked)
|
|
178
256
|
return null;
|
|
179
257
|
const nodeModulesDir = findNodeModulesDir(packageDir);
|
|
180
|
-
const binLink =
|
|
181
|
-
let resolvedBin =
|
|
258
|
+
const binLink = join(nodeModulesDir, ".bin", picked.name);
|
|
259
|
+
let resolvedBin = existsSync(binLink) ? safeRealpath(binLink) : "";
|
|
182
260
|
if (!resolvedBin) {
|
|
183
|
-
resolvedBin = resolve(packageDir,
|
|
261
|
+
resolvedBin = resolve(packageDir, picked.rel);
|
|
184
262
|
if (!existsSync(resolvedBin))
|
|
185
263
|
return null;
|
|
186
264
|
}
|
|
187
|
-
const isJs = detectJsBinary(resolvedBin);
|
|
188
265
|
return {
|
|
189
266
|
resolvedBin,
|
|
190
267
|
resolvedAt: Date.now(),
|
|
191
268
|
packageVersion: pkg?.version,
|
|
192
|
-
isJs,
|
|
269
|
+
isJs: detectJsBinary(resolvedBin),
|
|
270
|
+
source: "npx-cache",
|
|
193
271
|
};
|
|
194
272
|
}
|
|
195
|
-
const FORCE_CACHE_TIMEOUT_MS = 30_000;
|
|
196
273
|
async function forceNpxCache(packageSpec) {
|
|
197
274
|
try {
|
|
198
|
-
await new Promise((
|
|
275
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
199
276
|
const proc = spawn("npm", ["exec", "--yes", "--package", packageSpec, "--", "node", "-e", "1"], { stdio: "ignore" });
|
|
200
277
|
const timer = setTimeout(() => {
|
|
201
278
|
proc.kill();
|
|
202
|
-
|
|
279
|
+
rejectPromise(new Error("timeout"));
|
|
203
280
|
}, FORCE_CACHE_TIMEOUT_MS);
|
|
204
281
|
timer.unref();
|
|
205
|
-
proc.on("close", () => { clearTimeout(timer);
|
|
206
|
-
proc.on("error", (err) => { clearTimeout(timer);
|
|
282
|
+
proc.on("close", () => { clearTimeout(timer); resolvePromise(); });
|
|
283
|
+
proc.on("error", (err) => { clearTimeout(timer); rejectPromise(err); });
|
|
207
284
|
});
|
|
208
285
|
}
|
|
209
286
|
catch {
|
|
@@ -221,13 +298,15 @@ function buildBinCandidates(packageName, explicitBin) {
|
|
|
221
298
|
candidates.push(namePart);
|
|
222
299
|
if (scopePart && namePart)
|
|
223
300
|
candidates.push(`${scopePart}-${namePart}`);
|
|
301
|
+
if (scopePart)
|
|
302
|
+
candidates.push(scopePart);
|
|
224
303
|
}
|
|
225
304
|
else {
|
|
226
305
|
candidates.push(packageName);
|
|
227
306
|
}
|
|
228
307
|
return [...new Set(candidates.filter(Boolean))];
|
|
229
308
|
}
|
|
230
|
-
function extractPackageName(spec) {
|
|
309
|
+
export function extractPackageName(spec) {
|
|
231
310
|
const trimmed = spec.trim();
|
|
232
311
|
if (!trimmed)
|
|
233
312
|
return null;
|
|
@@ -244,6 +323,18 @@ function extractPackageName(spec) {
|
|
|
244
323
|
const atIndex = trimmed.indexOf("@");
|
|
245
324
|
return atIndex >= 0 ? trimmed.slice(0, atIndex) : trimmed;
|
|
246
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* A "floating" spec is one without a version pin: bare name, `@latest` or `@*`.
|
|
328
|
+
* Only floating specs may resolve from the global tree and only they get the
|
|
329
|
+
* registry freshness check — a pinned spec must match its exact version.
|
|
330
|
+
*/
|
|
331
|
+
export function isFloatingSpec(spec) {
|
|
332
|
+
const name = extractPackageName(spec);
|
|
333
|
+
if (!name)
|
|
334
|
+
return false;
|
|
335
|
+
const trimmed = spec.trim();
|
|
336
|
+
return trimmed === name || trimmed === `${name}@latest` || trimmed === `${name}@*`;
|
|
337
|
+
}
|
|
247
338
|
function defaultBinName(packageName) {
|
|
248
339
|
if (packageName.startsWith("@")) {
|
|
249
340
|
const parts = packageName.split("/");
|
|
@@ -265,7 +356,9 @@ function findCachedPackageDir(cacheDir, packageName) {
|
|
|
265
356
|
const mtime = safeStatMtime(full);
|
|
266
357
|
return { name: entry.name, mtime };
|
|
267
358
|
})
|
|
268
|
-
|
|
359
|
+
// Newest first; on equal mtimes fall back to a stable name order so
|
|
360
|
+
// resolution is deterministic.
|
|
361
|
+
.sort((a, b) => (b.mtime - a.mtime) || a.name.localeCompare(b.name));
|
|
269
362
|
for (const entry of candidates) {
|
|
270
363
|
const pkgDir = join(npxDir, entry.name, "node_modules", ...packagePathParts);
|
|
271
364
|
if (existsSync(join(pkgDir, "package.json"))) {
|
|
@@ -303,6 +396,12 @@ function detectJsBinary(binPath) {
|
|
|
303
396
|
}
|
|
304
397
|
}
|
|
305
398
|
let npmCacheDirCached;
|
|
399
|
+
let globalRootCached;
|
|
400
|
+
/** Reset per-process memoizations (used by tests). */
|
|
401
|
+
export function resetNpxResolverCachesForTests() {
|
|
402
|
+
npmCacheDirCached = undefined;
|
|
403
|
+
globalRootCached = undefined;
|
|
404
|
+
}
|
|
306
405
|
function getNpmCacheDir() {
|
|
307
406
|
if (npmCacheDirCached !== undefined)
|
|
308
407
|
return npmCacheDirCached;
|
|
@@ -325,6 +424,155 @@ function getNpmCacheDir() {
|
|
|
325
424
|
npmCacheDirCached = null;
|
|
326
425
|
return null;
|
|
327
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* Resolve the global node_modules root (e.g.
|
|
429
|
+
* `<prefix>/lib/node_modules` on unix, `<prefix>/node_modules` on Windows).
|
|
430
|
+
* Derived from the running node binary to avoid spawning npm on the hot path;
|
|
431
|
+
* falls back to `npm root -g` when the derived path does not exist.
|
|
432
|
+
*/
|
|
433
|
+
function getGlobalRoot() {
|
|
434
|
+
if (globalRootCached !== undefined)
|
|
435
|
+
return globalRootCached;
|
|
436
|
+
const derived = deriveGlobalRoot();
|
|
437
|
+
if (derived && existsSync(derived)) {
|
|
438
|
+
globalRootCached = derived;
|
|
439
|
+
return globalRootCached;
|
|
440
|
+
}
|
|
441
|
+
globalRootCached = discoverGlobalRootViaNpm();
|
|
442
|
+
return globalRootCached;
|
|
443
|
+
}
|
|
444
|
+
function deriveGlobalRoot() {
|
|
445
|
+
const prefix = process.env.NPM_CONFIG_PREFIX?.trim() || process.env.npm_config_prefix?.trim() || "";
|
|
446
|
+
const base = prefix || dirname(dirname(process.execPath));
|
|
447
|
+
if (!base)
|
|
448
|
+
return null;
|
|
449
|
+
return join(base, process.platform === "win32" ? "node_modules" : join("lib", "node_modules"));
|
|
450
|
+
}
|
|
451
|
+
function discoverGlobalRootViaNpm() {
|
|
452
|
+
try {
|
|
453
|
+
const result = spawn.sync("npm", ["root", "-g"], {
|
|
454
|
+
encoding: "utf-8",
|
|
455
|
+
timeout: GLOBAL_ROOT_TIMEOUT_MS,
|
|
456
|
+
});
|
|
457
|
+
if (result.status === 0) {
|
|
458
|
+
const path = String(result.stdout).trim();
|
|
459
|
+
if (path)
|
|
460
|
+
return path;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// Ignore — global resolution simply stays unavailable.
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Registry "latest" lookup for floating specs. Returns null on any failure
|
|
470
|
+
* (offline, timeout) so resolution degrades to the local state.
|
|
471
|
+
*/
|
|
472
|
+
async function fetchLatestVersion(packageName) {
|
|
473
|
+
return new Promise((resolvePromise) => {
|
|
474
|
+
let settled = false;
|
|
475
|
+
let timer;
|
|
476
|
+
const done = (value) => {
|
|
477
|
+
if (settled)
|
|
478
|
+
return;
|
|
479
|
+
settled = true;
|
|
480
|
+
if (timer)
|
|
481
|
+
clearTimeout(timer);
|
|
482
|
+
resolvePromise(value);
|
|
483
|
+
};
|
|
484
|
+
try {
|
|
485
|
+
const proc = spawn("npm", ["view", packageName, "version", "--json"], {
|
|
486
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
487
|
+
});
|
|
488
|
+
let out = "";
|
|
489
|
+
timer = setTimeout(() => {
|
|
490
|
+
proc.kill();
|
|
491
|
+
done(null);
|
|
492
|
+
}, REGISTRY_CHECK_TIMEOUT_MS);
|
|
493
|
+
timer.unref();
|
|
494
|
+
proc.stdout?.on("data", (chunk) => {
|
|
495
|
+
out += chunk.toString("utf-8");
|
|
496
|
+
});
|
|
497
|
+
proc.on("close", () => done(parseLatestVersionOutput(out)));
|
|
498
|
+
proc.on("error", () => done(null));
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
done(null);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
function parseLatestVersionOutput(out) {
|
|
506
|
+
const raw = out.trim();
|
|
507
|
+
if (!raw)
|
|
508
|
+
return null;
|
|
509
|
+
let value = raw;
|
|
510
|
+
try {
|
|
511
|
+
const parsed = JSON.parse(raw);
|
|
512
|
+
if (typeof parsed === "string")
|
|
513
|
+
value = parsed;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
value = raw.replace(/^["']|["']$/g, "");
|
|
517
|
+
}
|
|
518
|
+
return /^v?\d+\.\d+\.\d+/.test(value) ? value : null;
|
|
519
|
+
}
|
|
520
|
+
/** Semver-ish comparison; release beats prerelease, identifiers compare numerically when both numeric. */
|
|
521
|
+
export function compareVersions(a, b) {
|
|
522
|
+
const pa = parseVersion(a);
|
|
523
|
+
const pb = parseVersion(b);
|
|
524
|
+
if (!pa || !pb) {
|
|
525
|
+
// Unparseable — fall back to lexicographic so the comparison stays total.
|
|
526
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
527
|
+
}
|
|
528
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
529
|
+
if (pa[key] !== pb[key])
|
|
530
|
+
return pa[key] < pb[key] ? -1 : 1;
|
|
531
|
+
}
|
|
532
|
+
return comparePrerelease(pa.prerelease, pb.prerelease);
|
|
533
|
+
}
|
|
534
|
+
function parseVersion(v) {
|
|
535
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(v.trim());
|
|
536
|
+
if (!m)
|
|
537
|
+
return null;
|
|
538
|
+
return {
|
|
539
|
+
major: Number(m[1]),
|
|
540
|
+
minor: Number(m[2]),
|
|
541
|
+
patch: Number(m[3]),
|
|
542
|
+
prerelease: m[4] ? m[4].split(".") : null,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function comparePrerelease(a, b) {
|
|
546
|
+
if (!a && !b)
|
|
547
|
+
return 0;
|
|
548
|
+
if (!a)
|
|
549
|
+
return 1;
|
|
550
|
+
if (!b)
|
|
551
|
+
return -1;
|
|
552
|
+
const len = Math.max(a.length, b.length);
|
|
553
|
+
for (let i = 0; i < len; i++) {
|
|
554
|
+
const ai = a[i];
|
|
555
|
+
const bi = b[i];
|
|
556
|
+
if (ai === undefined)
|
|
557
|
+
return -1;
|
|
558
|
+
if (bi === undefined)
|
|
559
|
+
return 1;
|
|
560
|
+
const an = /^\d+$/.test(ai);
|
|
561
|
+
const bn = /^\d+$/.test(bi);
|
|
562
|
+
if (an && bn) {
|
|
563
|
+
const diff = Number(ai) - Number(bi);
|
|
564
|
+
if (diff !== 0)
|
|
565
|
+
return diff < 0 ? -1 : 1;
|
|
566
|
+
}
|
|
567
|
+
else if (an !== bn) {
|
|
568
|
+
return an ? -1 : 1;
|
|
569
|
+
}
|
|
570
|
+
else if (ai !== bi) {
|
|
571
|
+
return ai < bi ? -1 : 1;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return 0;
|
|
575
|
+
}
|
|
328
576
|
function getNpxCachePath() {
|
|
329
577
|
return getAgentPath("mcp-npx-cache.json");
|
|
330
578
|
}
|
|
@@ -350,7 +598,7 @@ function saveCacheEntry(key, entry) {
|
|
|
350
598
|
const cachePath = getNpxCachePath();
|
|
351
599
|
const dir = dirname(cachePath);
|
|
352
600
|
mkdirSync(dir, { recursive: true });
|
|
353
|
-
|
|
601
|
+
const merged = { version: CACHE_VERSION, entries: {} };
|
|
354
602
|
try {
|
|
355
603
|
if (existsSync(cachePath)) {
|
|
356
604
|
const existing = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
@@ -362,6 +610,13 @@ function saveCacheEntry(key, entry) {
|
|
|
362
610
|
catch {
|
|
363
611
|
// Ignore parse errors
|
|
364
612
|
}
|
|
613
|
+
// Prune entries that have not been re-resolved for 30 days.
|
|
614
|
+
const cutoff = Date.now() - CACHE_PRUNE_MS;
|
|
615
|
+
for (const [existingKey, existingEntry] of Object.entries(merged.entries)) {
|
|
616
|
+
if (existingEntry.resolvedAt < cutoff) {
|
|
617
|
+
delete merged.entries[existingKey];
|
|
618
|
+
}
|
|
619
|
+
}
|
|
365
620
|
merged.entries[key] = entry;
|
|
366
621
|
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
367
622
|
writeFileSync(tmpPath, JSON.stringify(merged, null, 2), "utf-8");
|