@dennisrepa/elevate 1.3.0 → 1.5.0

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/dist/cli.js CHANGED
@@ -4,74 +4,82 @@
4
4
  import { render } from "ink";
5
5
 
6
6
  // src/config.ts
7
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
8
- import { dirname as dirname8, join as join16, resolve as resolve10 } from "path";
7
+ import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
8
+ import { dirname as dirname10, join as join23, resolve as resolve14 } from "path";
9
9
 
10
- // src/adapters/npm/npm-discovery.ts
11
- import { readFileSync } from "fs";
12
- import { dirname, join, relative, resolve } from "path";
13
- import mapWorkspaces from "@npmcli/map-workspaces";
14
- function readJson(filePath) {
15
- try {
16
- return JSON.parse(readFileSync(filePath, "utf8"));
17
- } catch {
18
- return null;
19
- }
10
+ // src/domain/version-policy.ts
11
+ import semver2 from "semver";
12
+
13
+ // src/domain/versions.ts
14
+ import semver from "semver";
15
+ var PRE_RELEASE = /(?:^|[.\-_+]|(?<=\d))(alpha|beta|rc|cr|milestone|preview|snapshot|ea|dev|pre|(?:a|b|m)(?=\d))(?=$|[.\-_+]|\d)/i;
16
+ function isPreReleaseVersion(version) {
17
+ return PRE_RELEASE.test(version);
20
18
  }
21
- var NpmModuleDiscoveryAdapter = class {
22
- /**
23
- * The nearest directory, from `startDir` upwards to the boundary, whose
24
- * package.json is valid JSON and declares `workspaces`.
25
- */
26
- findRoot(startDir, boundaryDir) {
27
- let dir = resolve(startDir);
28
- const boundary = boundaryDir ? resolve(boundaryDir) : void 0;
29
- for (; ; ) {
30
- if (readJson(join(dir, "package.json"))?.workspaces) return dir;
31
- const parent = dirname(dir);
32
- if (dir === boundary || parent === dir) return void 0;
33
- dir = parent;
34
- }
35
- }
36
- async discover(rootDir) {
37
- const rootPkg = readJson(join(rootDir, "package.json"));
38
- const modules = [
39
- {
40
- id: rootPkg?.name ?? "root",
41
- name: rootPkg?.name ?? "Root Workspace (npm)",
42
- path: rootDir,
43
- relPath: "Root",
44
- ecosystem: "npm",
45
- isRoot: true,
46
- version: typeof rootPkg?.version === "string" ? rootPkg.version : void 0
47
- }
48
- ];
49
- if (!rootPkg?.workspaces) return modules;
50
- let workspaces;
51
- try {
52
- workspaces = await mapWorkspaces({ cwd: rootDir, pkg: rootPkg });
53
- } catch {
54
- return modules;
55
- }
56
- for (const [name, path] of workspaces) {
57
- const pkg = readJson(join(path, "package.json"));
58
- modules.push({
59
- id: name,
60
- name,
61
- path,
62
- relPath: relative(rootDir, path).replace(/\\/g, "/"),
63
- ecosystem: "npm",
64
- isRoot: false,
65
- version: typeof pkg?.version === "string" ? pkg.version : void 0
66
- });
67
- }
68
- modules.sort((a, b) => a.isRoot ? -1 : b.isRoot ? 1 : a.relPath.localeCompare(b.relPath));
69
- return modules;
19
+ function extractPreReleaseTag(version) {
20
+ const match = version.match(PRE_RELEASE);
21
+ if (!match) return void 0;
22
+ const token = match[1].toLowerCase();
23
+ if (token === "a") return "ALPHA";
24
+ if (token === "b") return "BETA";
25
+ if (token === "m") {
26
+ const number = version.slice((match.index ?? 0) + match[0].length).match(/^\d+/)?.[0];
27
+ return `M${number ?? ""}`;
70
28
  }
29
+ return token.toUpperCase();
30
+ }
31
+ function cleanJavaVersion(version) {
32
+ const match = version.match(/^v?(\d+(?:\.\d+){0,2})/);
33
+ if (!match) return null;
34
+ return semver.coerce(match[1])?.version ?? null;
35
+ }
36
+ function diffVersions(from, to) {
37
+ const kind = semver.diff(from, to);
38
+ if (kind === "major" || kind === "premajor") return "major";
39
+ if (kind === "minor" || kind === "preminor") return "minor";
40
+ return "patch";
41
+ }
42
+ function rangePrefix(range) {
43
+ if (range.startsWith("^")) return "^";
44
+ if (range.startsWith("~")) return "~";
45
+ return "";
46
+ }
47
+
48
+ // src/domain/version-policy.ts
49
+ function semverDiff(current, target, normalize) {
50
+ const to = normalize(target);
51
+ if (!to || !semver2.valid(current)) return "minor";
52
+ return semver2.eq(current, to) ? "patch" : diffVersions(current, to);
53
+ }
54
+ var npmVersionPolicy = {
55
+ normalize: (v) => semver2.valid(v),
56
+ rewrite: (current, target) => `${rangePrefix(current)}${target}`,
57
+ diff: (current, target) => semverDiff(current, target, (v) => semver2.valid(v)),
58
+ allowsManualTargets: true
59
+ };
60
+ var mavenVersionPolicy = {
61
+ normalize: cleanJavaVersion,
62
+ rewrite: (_current, target) => target,
63
+ diff: (current, target) => semverDiff(current, target, cleanJavaVersion),
64
+ allowsManualTargets: false
71
65
  };
72
66
 
73
- // src/adapters/npm/npm-registry.ts
74
- import semver from "semver";
67
+ // src/adapters/npm/npm-package-manager.ts
68
+ import { existsSync as existsSync2, readFileSync } from "fs";
69
+ import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
70
+
71
+ // src/adapters/shared/checkout.ts
72
+ import { existsSync } from "fs";
73
+ import { dirname, join, resolve } from "path";
74
+ function findCheckoutBoundary(startDir) {
75
+ let dir = resolve(startDir);
76
+ for (; ; ) {
77
+ if (existsSync(join(dir, ".git"))) return dir;
78
+ const parent = dirname(dir);
79
+ if (parent === dir) return void 0;
80
+ dir = parent;
81
+ }
82
+ }
75
83
 
76
84
  // src/adapters/shared/process.ts
77
85
  import { exec, spawn } from "child_process";
@@ -86,7 +94,7 @@ var UnsafeArgumentError = class extends Error {
86
94
  var IS_WINDOWS = process.platform === "win32";
87
95
  var WINDOWS_UNSAFE = /["%!^&|<>\r\n]/;
88
96
  function runCommand(command, args, options) {
89
- return new Promise((resolve12, reject) => {
97
+ return new Promise((resolve16, reject) => {
90
98
  const spawnOptions = { cwd: options.cwd, windowsHide: true, env: process.env };
91
99
  let child2;
92
100
  if (IS_WINDOWS) {
@@ -116,19 +124,19 @@ function runCommand(command, args, options) {
116
124
  });
117
125
  child2.on("close", (code) => {
118
126
  if (timer) clearTimeout(timer);
119
- resolve12({ exitCode: code ?? 1, stdout, stderr, timedOut });
127
+ resolve16({ exitCode: code ?? 1, stdout, stderr, timedOut });
120
128
  });
121
129
  });
122
130
  }
123
131
  function runShellScript(script, options) {
124
- return new Promise((resolve12) => {
132
+ return new Promise((resolve16) => {
125
133
  exec(
126
134
  script,
127
135
  { cwd: options.cwd, timeout: options.timeoutMs, maxBuffer: 64 * 1024 * 1024, windowsHide: true },
128
136
  (error, stdout, stderr) => {
129
137
  const timedOut = Boolean(error?.killed);
130
138
  const exitCode = error ? typeof error.code === "number" ? error.code : 1 : 0;
131
- resolve12({ exitCode, stdout, stderr, timedOut });
139
+ resolve16({ exitCode, stdout, stderr, timedOut });
132
140
  }
133
141
  );
134
142
  });
@@ -148,6 +156,213 @@ function quoteWindows(arg) {
148
156
  return /^[\w.:\\/@+-]+$/.test(arg) ? arg : `"${arg}"`;
149
157
  }
150
158
 
159
+ // src/adapters/npm/npm-package-manager.ts
160
+ var MARKER_FILES = [
161
+ ["npm", ["package-lock.json", "npm-shrinkwrap.json"]],
162
+ ["pnpm", ["pnpm-lock.yaml", "pnpm-workspace.yaml"]],
163
+ ["yarn", ["yarn.lock", ".yarnrc.yml"]],
164
+ ["bun", ["bun.lock", "bun.lockb"]]
165
+ ];
166
+ var UnsupportedPackageManagerError = class extends Error {
167
+ constructor(detected) {
168
+ super(
169
+ `This repository is managed by ${detected.name} (${detected.evidence}). Elevate supports npm and pnpm for Node.js projects; using either here would create a lockfile next to ${detected.name}'s. Nothing was changed.`
170
+ );
171
+ this.name = "UnsupportedPackageManagerError";
172
+ }
173
+ };
174
+ function detectPackageManager(dir) {
175
+ const boundary = findCheckoutBoundary(dir);
176
+ let current = resolve2(dir);
177
+ for (; ; ) {
178
+ const found = detectIn(current);
179
+ if (found) return found;
180
+ const parent = dirname2(current);
181
+ if (boundary === void 0 || current === boundary || parent === current) break;
182
+ current = parent;
183
+ }
184
+ return { name: "npm", evidence: "no other package manager detected" };
185
+ }
186
+ function detectIn(dir) {
187
+ const declared = readPackageManagerField(dir);
188
+ const name = declared?.match(/^(npm|pnpm|yarn|bun)@/)?.[1];
189
+ if (declared && name) return { name, evidence: `"packageManager": "${declared}" in package.json` };
190
+ for (const [manager, files] of MARKER_FILES) {
191
+ const found = files.find((file) => existsSync2(join2(dir, file)));
192
+ if (found) return { name: manager, evidence: found };
193
+ }
194
+ return void 0;
195
+ }
196
+ function assertSupportedPackageManager(rootDir) {
197
+ const detected = detectPackageManager(rootDir);
198
+ if (detected.name === "npm" || detected.name === "pnpm") return detected.name;
199
+ throw new UnsupportedPackageManagerError(detected);
200
+ }
201
+ var PackageManagerNotFoundError = class extends Error {
202
+ constructor(reason, detected) {
203
+ super(
204
+ `pnpm could not be run in this repository (${reason}). This repository is managed by pnpm (${detected.evidence}); install pnpm or enable Corepack (corepack enable). Nothing was changed.`
205
+ );
206
+ this.name = "PackageManagerNotFoundError";
207
+ }
208
+ };
209
+ var VERSION_TIMEOUT_MS = 15e3;
210
+ var verifiedRoots = /* @__PURE__ */ new Set();
211
+ async function ensurePnpmRunnable(rootDir, detected) {
212
+ const key = resolve2(rootDir);
213
+ if (verifiedRoots.has(key)) return;
214
+ let reason;
215
+ try {
216
+ const result = await runCommand("pnpm", ["--version"], { cwd: rootDir, timeoutMs: VERSION_TIMEOUT_MS });
217
+ if (result.timedOut) reason = "pnpm --version timed out";
218
+ else if (result.exitCode !== 0) {
219
+ const line = firstLine(result.stderr) || firstLine(result.stdout);
220
+ reason = `pnpm --version exited with code ${result.exitCode}: ${line}`;
221
+ }
222
+ } catch {
223
+ reason = "command not found";
224
+ }
225
+ if (reason !== void 0) throw new PackageManagerNotFoundError(reason, detected);
226
+ verifiedRoots.add(key);
227
+ }
228
+ async function prepareSupportedPackageManager(rootDir) {
229
+ const manager = assertSupportedPackageManager(rootDir);
230
+ if (manager === "pnpm") await ensurePnpmRunnable(rootDir, detectPackageManager(rootDir));
231
+ return manager;
232
+ }
233
+ function firstLine(text) {
234
+ return text.split(/\r?\n/).find((l) => l.trim() !== "")?.trim() ?? "";
235
+ }
236
+ function readPackageManagerField(rootDir) {
237
+ try {
238
+ const field = JSON.parse(readFileSync(join2(rootDir, "package.json"), "utf8"))?.packageManager;
239
+ return typeof field === "string" ? field : void 0;
240
+ } catch {
241
+ return void 0;
242
+ }
243
+ }
244
+
245
+ // src/adapters/npm/npm-discovery.ts
246
+ import { readFileSync as readFileSync3 } from "fs";
247
+ import { dirname as dirname3, join as join4, relative, resolve as resolve3 } from "path";
248
+ import mapWorkspaces from "@npmcli/map-workspaces";
249
+
250
+ // src/adapters/npm/pnpm-workspace.ts
251
+ import { readFileSync as readFileSync2 } from "fs";
252
+ import { join as join3 } from "path";
253
+ import { isMap, isScalar, parse, parseDocument } from "yaml";
254
+ var PNPM_WORKSPACE_FILE = "pnpm-workspace.yaml";
255
+ function isRecord(value) {
256
+ return typeof value === "object" && value !== null && !Array.isArray(value);
257
+ }
258
+ function entries(value) {
259
+ const map = /* @__PURE__ */ new Map();
260
+ if (isRecord(value)) {
261
+ for (const [name, range] of Object.entries(value)) {
262
+ if (typeof range === "string") map.set(name, range);
263
+ }
264
+ }
265
+ return map;
266
+ }
267
+ function readPnpmWorkspace(rootDir) {
268
+ const file = join3(rootDir, PNPM_WORKSPACE_FILE);
269
+ let data;
270
+ try {
271
+ data = parse(readFileSync2(file, "utf8"));
272
+ } catch {
273
+ return void 0;
274
+ }
275
+ if (data !== null && !isRecord(data)) return void 0;
276
+ const doc = data ?? {};
277
+ const packages = Array.isArray(doc.packages) ? doc.packages.filter((p) => typeof p === "string") : [];
278
+ const catalogs = /* @__PURE__ */ new Map();
279
+ const named = isRecord(doc.catalogs) ? doc.catalogs : {};
280
+ for (const [name, content] of Object.entries(named)) catalogs.set(name, entries(content));
281
+ const hasTopLevel = isRecord(doc.catalog);
282
+ const hasNamedDefault = isRecord(named.default);
283
+ if (hasTopLevel) catalogs.set("default", entries(doc.catalog));
284
+ return { file, packages, catalogs, ambiguousDefaultCatalog: hasTopLevel && hasNamedDefault };
285
+ }
286
+ function setCatalogRange(source, catalog, name, newRange) {
287
+ const doc = parseDocument(source);
288
+ const holders = catalog === "default" ? [doc.get("catalog", true), doc.getIn(["catalogs", "default"], true)] : [doc.getIn(["catalogs", catalog], true)];
289
+ for (const holder of holders) {
290
+ if (!isMap(holder)) continue;
291
+ const node = holder.get(name, true);
292
+ if (!isScalar(node) || !node.range) continue;
293
+ const scalar = node;
294
+ const [start, end] = scalar.range;
295
+ const quote = scalar.type === "QUOTE_DOUBLE" ? '"' : scalar.type === "QUOTE_SINGLE" ? "'" : "";
296
+ return source.slice(0, start) + quote + newRange + quote + source.slice(end);
297
+ }
298
+ throw new Error(`No entry for '${name}' in catalog '${catalog}' of ${PNPM_WORKSPACE_FILE}.`);
299
+ }
300
+
301
+ // src/adapters/npm/npm-discovery.ts
302
+ function readJson(filePath) {
303
+ try {
304
+ return JSON.parse(readFileSync3(filePath, "utf8"));
305
+ } catch {
306
+ return null;
307
+ }
308
+ }
309
+ var NpmModuleDiscoveryAdapter = class {
310
+ /**
311
+ * The nearest directory, from `startDir` upwards to the boundary, whose
312
+ * package.json is valid JSON and declares `workspaces`, or that contains a
313
+ * valid `pnpm-workspace.yaml`.
314
+ */
315
+ findRoot(startDir, boundaryDir) {
316
+ let dir = resolve3(startDir);
317
+ const boundary = boundaryDir ? resolve3(boundaryDir) : void 0;
318
+ for (; ; ) {
319
+ if (readJson(join4(dir, "package.json"))?.workspaces || readPnpmWorkspace(dir)) return dir;
320
+ const parent = dirname3(dir);
321
+ if (dir === boundary || parent === dir) return void 0;
322
+ dir = parent;
323
+ }
324
+ }
325
+ async discover(rootDir) {
326
+ const rootPkg = readJson(join4(rootDir, "package.json"));
327
+ const modules = [
328
+ {
329
+ id: rootPkg?.name ?? "root",
330
+ name: rootPkg?.name ?? "Root Workspace (npm)",
331
+ path: rootDir,
332
+ relPath: "Root",
333
+ ecosystem: "npm",
334
+ isRoot: true,
335
+ version: typeof rootPkg?.version === "string" ? rootPkg.version : void 0
336
+ }
337
+ ];
338
+ const patterns = detectPackageManager(rootDir).name === "pnpm" ? readPnpmWorkspace(rootDir)?.packages : rootPkg?.workspaces;
339
+ if (!patterns || Array.isArray(patterns) && patterns.length === 0) return modules;
340
+ let workspaces;
341
+ try {
342
+ workspaces = await mapWorkspaces({ cwd: rootDir, pkg: { ...rootPkg, workspaces: patterns } });
343
+ } catch {
344
+ return modules;
345
+ }
346
+ for (const [name, path] of workspaces) {
347
+ const pkg = readJson(join4(path, "package.json"));
348
+ modules.push({
349
+ id: name,
350
+ name,
351
+ path,
352
+ relPath: relative(rootDir, path).replace(/\\/g, "/"),
353
+ ecosystem: "npm",
354
+ isRoot: false,
355
+ version: typeof pkg?.version === "string" ? pkg.version : void 0
356
+ });
357
+ }
358
+ modules.sort((a, b) => a.isRoot ? -1 : b.isRoot ? 1 : a.relPath.localeCompare(b.relPath));
359
+ return modules;
360
+ }
361
+ };
362
+
363
+ // src/adapters/npm/npm-registry.ts
364
+ import semver3 from "semver";
365
+
151
366
  // src/domain/origin.ts
152
367
  function buildWorkspaceIndex(modules) {
153
368
  const index = /* @__PURE__ */ new Map();
@@ -194,9 +409,24 @@ function npmScopeOf(name) {
194
409
  return slash > 1 ? name.slice(0, slash) : void 0;
195
410
  }
196
411
 
412
+ // src/adapters/npm/npm-commands.ts
413
+ function driverFor(dir) {
414
+ return detectPackageManager(dir).name === "pnpm" ? "pnpm" : "npm";
415
+ }
416
+ var commands = {
417
+ /** Install after Elevate wrote new ranges. pnpm is told explicitly not to freeze: `CI` would otherwise imply it. */
418
+ install: (m) => m === "pnpm" ? { command: "pnpm", args: ["install", "--no-frozen-lockfile"] } : { command: "npm", args: ["install"] },
419
+ /** Restore the installed state after a rollback. */
420
+ restore: (m) => m === "pnpm" ? { command: "pnpm", args: ["install", "--frozen-lockfile"] } : { command: "npm", args: ["install"] },
421
+ /** Default verification. */
422
+ verify: (m) => m === "pnpm" ? { command: "pnpm", args: ["install", "--frozen-lockfile", "--lockfile-only"] } : { command: "npm", args: ["ls"] },
423
+ view: (m, name, field) => ({ command: m, args: ["view", name, field, "--json"] }),
424
+ config: (m) => ({ command: m, args: ["config", "list", "--json"] })
425
+ };
426
+
197
427
  // src/adapters/npm/npm-config.ts
198
428
  var DEFAULT_REGISTRY = "https://registry.npmjs.org/";
199
- var PUBLIC_NPM_HOSTS = /* @__PURE__ */ new Set(["registry.npmjs.org", "registry.npmjs.com", "registry.yarnpkg.com"]);
429
+ var PUBLIC_NPM_HOSTS = /* @__PURE__ */ new Set(["registry.npmjs.org", "registry.npmjs.com", "registry.yarnpkg.com", "npm.jsr.io"]);
200
430
  var NpmConfigReader = class {
201
431
  cache = /* @__PURE__ */ new Map();
202
432
  /** Returns the registry configuration that applies in `cwd` (cached per directory). */
@@ -233,7 +463,8 @@ function parseNpmConfig(json) {
233
463
  }
234
464
  async function loadConfig(cwd) {
235
465
  try {
236
- const result = await runCommand("npm", ["config", "list", "--json"], { cwd, timeoutMs: 15e3 });
466
+ const { command, args } = commands.config(driverFor(cwd));
467
+ const result = await runCommand(command, args, { cwd, timeoutMs: 15e3 });
237
468
  if (result.exitCode !== 0) return parseNpmConfig({});
238
469
  return parseNpmConfig(JSON.parse(result.stdout));
239
470
  } catch {
@@ -260,13 +491,13 @@ var NpmRegistryAdapter = class {
260
491
  if (!isValidPackageName(coordinate.identifier)) return null;
261
492
  if (channel === "stable") {
262
493
  const latest = await this.view(coordinate.identifier, "dist-tags.latest", cwd);
263
- if (typeof latest === "string" && semver.valid(latest) && semver.prerelease(latest) === null) {
494
+ if (typeof latest === "string" && semver3.valid(latest) && semver3.prerelease(latest) === null) {
264
495
  return latest;
265
496
  }
266
497
  const releases = (await this.getAllVersions(coordinate, cwd)).filter(
267
- (v) => semver.valid(v) && semver.prerelease(v) === null
498
+ (v) => semver3.valid(v) && semver3.prerelease(v) === null
268
499
  );
269
- return releases.length > 0 ? semver.rsort(releases)[0] : null;
500
+ return releases.length > 0 ? semver3.rsort(releases)[0] : null;
270
501
  }
271
502
  const version = await this.view(coordinate.identifier, "version", cwd);
272
503
  return typeof version === "string" && version ? version : null;
@@ -277,10 +508,11 @@ var NpmRegistryAdapter = class {
277
508
  const list = Array.isArray(versions) ? versions : versions ? [versions] : [];
278
509
  return list.map(String).reverse();
279
510
  }
280
- /** Runs `npm view <name> <field> --json`; resolves null on any failure. */
511
+ /** Runs `npm view <name> <field> --json` (`pnpm view` in a pnpm repository); resolves null on any failure. */
281
512
  async view(name, field, cwd) {
282
513
  try {
283
- const result = await runCommand("npm", ["view", name, field, "--json"], {
514
+ const { command, args } = commands.view(driverFor(cwd), name, field);
515
+ const result = await runCommand(command, args, {
284
516
  cwd,
285
517
  timeoutMs: LOOKUP_TIMEOUT_MS
286
518
  });
@@ -293,44 +525,9 @@ var NpmRegistryAdapter = class {
293
525
  };
294
526
 
295
527
  // src/adapters/npm/npm-scanner.ts
296
- import { readFileSync as readFileSync3 } from "fs";
297
- import { join as join4 } from "path";
298
- import semver3 from "semver";
299
-
300
- // src/domain/versions.ts
301
- import semver2 from "semver";
302
- var PRE_RELEASE = /(?:^|[.\-_+]|(?<=\d))(alpha|beta|rc|cr|milestone|preview|snapshot|ea|dev|pre|(?:a|b|m)(?=\d))(?=$|[.\-_+]|\d)/i;
303
- function isPreReleaseVersion(version) {
304
- return PRE_RELEASE.test(version);
305
- }
306
- function extractPreReleaseTag(version) {
307
- const match = version.match(PRE_RELEASE);
308
- if (!match) return void 0;
309
- const token = match[1].toLowerCase();
310
- if (token === "a") return "ALPHA";
311
- if (token === "b") return "BETA";
312
- if (token === "m") {
313
- const number = version.slice((match.index ?? 0) + match[0].length).match(/^\d+/)?.[0];
314
- return `M${number ?? ""}`;
315
- }
316
- return token.toUpperCase();
317
- }
318
- function cleanJavaVersion(version) {
319
- const match = version.match(/^v?(\d+(?:\.\d+){0,2})/);
320
- if (!match) return null;
321
- return semver2.coerce(match[1])?.version ?? null;
322
- }
323
- function diffVersions(from, to) {
324
- const kind = semver2.diff(from, to);
325
- if (kind === "major" || kind === "premajor") return "major";
326
- if (kind === "minor" || kind === "preminor") return "minor";
327
- return "patch";
328
- }
329
- function rangePrefix(range) {
330
- if (range.startsWith("^")) return "^";
331
- if (range.startsWith("~")) return "~";
332
- return "";
333
- }
528
+ import { readFileSync as readFileSync4 } from "fs";
529
+ import { join as join5 } from "path";
530
+ import semver4 from "semver";
334
531
 
335
532
  // src/adapters/shared/pool.ts
336
533
  async function mapPooled(items, concurrency, fn) {
@@ -347,72 +544,12 @@ async function mapPooled(items, concurrency, fn) {
347
544
  return results;
348
545
  }
349
546
 
350
- // src/adapters/npm/npm-package-manager.ts
351
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
352
- import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
353
-
354
- // src/adapters/shared/checkout.ts
355
- import { existsSync } from "fs";
356
- import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
357
- function findCheckoutBoundary(startDir) {
358
- let dir = resolve2(startDir);
359
- for (; ; ) {
360
- if (existsSync(join2(dir, ".git"))) return dir;
361
- const parent = dirname2(dir);
362
- if (parent === dir) return void 0;
363
- dir = parent;
364
- }
365
- }
366
-
367
- // src/adapters/npm/npm-package-manager.ts
368
- var MARKER_FILES = [
369
- ["npm", ["package-lock.json", "npm-shrinkwrap.json"]],
370
- ["pnpm", ["pnpm-lock.yaml", "pnpm-workspace.yaml"]],
371
- ["yarn", ["yarn.lock", ".yarnrc.yml"]],
372
- ["bun", ["bun.lock", "bun.lockb"]]
373
- ];
374
- var UnsupportedPackageManagerError = class extends Error {
375
- constructor(detected) {
376
- super(
377
- `This repository is managed by ${detected.name} (${detected.evidence}). Elevate supports npm for Node.js projects; using it here would create an npm lockfile next to ${detected.name}'s. Nothing was changed.`
378
- );
379
- this.name = "UnsupportedPackageManagerError";
380
- }
381
- };
382
- function detectPackageManager(dir) {
383
- const boundary = findCheckoutBoundary(dir);
384
- let current = resolve3(dir);
385
- for (; ; ) {
386
- const found = detectIn(current);
387
- if (found) return found;
388
- const parent = dirname3(current);
389
- if (boundary === void 0 || current === boundary || parent === current) break;
390
- current = parent;
391
- }
392
- return { name: "npm", evidence: "no other package manager detected" };
393
- }
394
- function detectIn(dir) {
395
- const declared = readPackageManagerField(dir);
396
- const name = declared?.match(/^(npm|pnpm|yarn|bun)@/)?.[1];
397
- if (declared && name) return { name, evidence: `"packageManager": "${declared}" in package.json` };
398
- for (const [manager, files] of MARKER_FILES) {
399
- const found = files.find((file) => existsSync2(join3(dir, file)));
400
- if (found) return { name: manager, evidence: found };
401
- }
402
- return void 0;
403
- }
404
- function assertNpmManaged(rootDir) {
405
- const detected = detectPackageManager(rootDir);
406
- if (detected.name !== "npm") throw new UnsupportedPackageManagerError(detected);
407
- }
408
- function readPackageManagerField(rootDir) {
409
- try {
410
- const field = JSON.parse(readFileSync2(join3(rootDir, "package.json"), "utf8"))?.packageManager;
411
- return typeof field === "string" ? field : void 0;
412
- } catch {
413
- return void 0;
414
- }
415
- }
547
+ // src/adapters/npm/npm-protocols.ts
548
+ function splitRangeProtocol(range) {
549
+ if (range.startsWith("workspace:")) return { protocol: "workspace", rest: range.slice("workspace:".length) };
550
+ if (range.startsWith("catalog:")) return { protocol: "catalog", rest: range.slice("catalog:".length) };
551
+ return { protocol: void 0, rest: range };
552
+ }
416
553
 
417
554
  // src/adapters/npm/npm-scanner.ts
418
555
  var CONCURRENCY = 6;
@@ -425,10 +562,11 @@ var NpmDependencyAdapter = class {
425
562
  registry;
426
563
  config;
427
564
  async scan(module, context) {
428
- assertNpmManaged(context.rootDir);
565
+ const manager = await prepareSupportedPackageManager(context.rootDir);
566
+ const pnpm = manager === "pnpm";
429
567
  let pkg;
430
568
  try {
431
- pkg = JSON.parse(readFileSync3(join4(module.path, "package.json"), "utf8"));
569
+ pkg = JSON.parse(readFileSync4(join5(module.path, "package.json"), "utf8"));
432
570
  } catch {
433
571
  return { candidates: [], skipped: [] };
434
572
  }
@@ -442,13 +580,20 @@ var NpmDependencyAdapter = class {
442
580
  [...npmConfig.scopedRegistries].filter(([, url]) => !isPublicNpmRegistry(url))
443
581
  )
444
582
  };
583
+ const workspaceFile = pnpm ? readPnpmWorkspace(context.rootDir) : void 0;
445
584
  const outcomes = await mapPooled(declaredDependencies(pkg), CONCURRENCY, async (dep) => {
446
585
  const origin = classifyOrigin(dep.name, "npm", originContext);
447
586
  if (origin.kind === "workspace") {
448
587
  const target = context.workspace.get(dep.name);
449
- const candidate = target ? alignmentCandidate(dep, target, origin) : null;
588
+ if (pnpm && isUpgradableRange(dep.range) && splitRangeProtocol(dep.range).protocol === void 0) {
589
+ return { skipped: { identifier: dep.name, origin: "workspace", reason: "not-linked", detail: dep.range } };
590
+ }
591
+ const candidate = target ? alignmentCandidate(dep, target, origin, pnpm) : null;
450
592
  return candidate ? { candidate } : null;
451
593
  }
594
+ if (pnpm && splitRangeProtocol(dep.range).protocol === "catalog") {
595
+ return this.catalogOutcome(dep, origin, context, module.path, workspaceFile);
596
+ }
452
597
  return this.registryOutcome(dep, origin, context);
453
598
  });
454
599
  const candidates = [];
@@ -461,8 +606,34 @@ var NpmDependencyAdapter = class {
461
606
  candidates.sort((a, b) => (DIFF_ORDER[a.diff] ?? 3) - (DIFF_ORDER[b.diff] ?? 3));
462
607
  return { candidates, skipped };
463
608
  }
609
+ /** A `catalog:` dependency: the range comes from pnpm-workspace.yaml and is written back there. */
610
+ async catalogOutcome(dep, origin, context, modulePath, workspaceFile) {
611
+ const catalogName = splitRangeProtocol(dep.range).rest || "default";
612
+ const unresolved = (detail) => ({
613
+ skipped: { identifier: dep.name, origin: origin.kind, reason: "catalog-unresolved", detail }
614
+ });
615
+ if (!workspaceFile) return unresolved("pnpm-workspace.yaml is missing or not valid YAML");
616
+ if (catalogName === "default" && workspaceFile.ambiguousDefaultCatalog) {
617
+ return unresolved("catalog 'default' is defined twice (catalog and catalogs.default)");
618
+ }
619
+ const catalog = workspaceFile.catalogs.get(catalogName);
620
+ if (!catalog) return unresolved(`no catalog '${catalogName}'`);
621
+ const range = catalog.get(dep.name);
622
+ if (range === void 0) return unresolved(`no entry for '${dep.name}' in catalog '${catalogName}'`);
623
+ const declaration = {
624
+ file: workspaceFile.file,
625
+ kind: "catalog",
626
+ catalogName,
627
+ displayPath: "pnpm-workspace.yaml"
628
+ };
629
+ const sharedWith = usersOfCatalogEntry(context, modulePath, dep.name, catalogName);
630
+ return this.registryOutcome({ ...dep, range }, origin, context, {
631
+ declaration,
632
+ ...sharedWith.length > 0 ? { sharedWith } : {}
633
+ });
634
+ }
464
635
  /** Looks up a private or public dependency in its registry. */
465
- async registryOutcome(dep, origin, context) {
636
+ async registryOutcome(dep, origin, context, extra = {}) {
466
637
  if (!isUpgradableRange(dep.range)) return null;
467
638
  if (!isValidPackageName(dep.name)) {
468
639
  return { skipped: { identifier: dep.name, origin: origin.kind, reason: "invalid-name" } };
@@ -487,9 +658,9 @@ var NpmDependencyAdapter = class {
487
658
  if (!latest) {
488
659
  return { skipped: { identifier: dep.name, origin: origin.kind, reason: "lookup-failed" } };
489
660
  }
490
- const currentClean = semver3.minVersion(dep.range)?.version;
491
- if (!currentClean || !semver3.valid(latest) || !semver3.gt(latest, currentClean)) return null;
492
- const tag = semver3.prerelease(latest) ? extractPreReleaseTag(latest) ?? "PRE" : void 0;
661
+ const currentClean = semver4.minVersion(dep.range)?.version;
662
+ if (!currentClean || !semver4.valid(latest) || !semver4.gt(latest, currentClean)) return null;
663
+ const tag = semver4.prerelease(latest) ? extractPreReleaseTag(latest) ?? "PRE" : void 0;
493
664
  const diff = diffVersions(currentClean, latest);
494
665
  return {
495
666
  candidate: {
@@ -504,7 +675,8 @@ var NpmDependencyAdapter = class {
504
675
  origin: resolvedOrigin,
505
676
  action: "update",
506
677
  isPreRelease: tag !== void 0,
507
- preReleaseTag: tag
678
+ preReleaseTag: tag,
679
+ ...extra
508
680
  }
509
681
  };
510
682
  }
@@ -523,21 +695,24 @@ function declaredDependencies(pkg) {
523
695
  function isUpgradableRange(range) {
524
696
  const trimmed = range.trim();
525
697
  if (trimmed === "" || trimmed === "*" || trimmed.toLowerCase() === "x") return false;
526
- return semver3.validRange(trimmed) !== null;
698
+ return semver4.validRange(trimmed) !== null;
527
699
  }
528
- function alignmentCandidate(dep, target, origin) {
700
+ function alignmentCandidate(dep, target, origin, pnpm = false) {
701
+ const split = pnpm ? splitRangeProtocol(dep.range) : { protocol: void 0, rest: dep.range };
702
+ const range = split.protocol === "workspace" ? split.rest : dep.range;
703
+ const protocol = split.protocol === "workspace" ? "workspace:" : "";
529
704
  const local = target.version;
530
- if (!local || !semver3.valid(local) || !isUpgradableRange(dep.range)) return null;
531
- if (semver3.satisfies(local, dep.range, { includePrerelease: true })) return null;
532
- const currentClean = semver3.minVersion(dep.range)?.version ?? "0.0.0";
705
+ if (!local || !semver4.valid(local) || !isUpgradableRange(range)) return null;
706
+ if (semver4.satisfies(local, range, { includePrerelease: true })) return null;
707
+ const currentClean = semver4.minVersion(range)?.version ?? "0.0.0";
533
708
  const diff = diffVersions(currentClean, local);
534
- const tag = semver3.prerelease(local) ? extractPreReleaseTag(local) ?? "PRE" : void 0;
709
+ const tag = semver4.prerelease(local) ? extractPreReleaseTag(local) ?? "PRE" : void 0;
535
710
  return {
536
711
  coordinate: { identifier: dep.name, artifact: dep.name, ecosystem: "npm" },
537
712
  currentRange: dep.range,
538
713
  currentClean,
539
714
  latest: local,
540
- newRange: `${rangePrefix(dep.range)}${local}`,
715
+ newRange: `${protocol}${rangePrefix(range)}${local}`,
541
716
  diff,
542
717
  scope: dep.scope,
543
718
  selected: diff !== "major",
@@ -548,10 +723,27 @@ function alignmentCandidate(dep, target, origin) {
548
723
  preReleaseTag: tag
549
724
  };
550
725
  }
726
+ function usersOfCatalogEntry(context, selfPath, name, catalogName) {
727
+ const users = [];
728
+ for (const module of context.workspace.values()) {
729
+ if (module.path === selfPath) continue;
730
+ let pkg;
731
+ try {
732
+ pkg = JSON.parse(readFileSync4(join5(module.path, "package.json"), "utf8"));
733
+ } catch {
734
+ continue;
735
+ }
736
+ const dep = declaredDependencies(pkg).find((d) => d.name === name);
737
+ if (!dep || splitRangeProtocol(dep.range).protocol !== "catalog") continue;
738
+ if ((splitRangeProtocol(dep.range).rest || "default") !== catalogName) continue;
739
+ users.push(module.id);
740
+ }
741
+ return users;
742
+ }
551
743
 
552
744
  // src/adapters/npm/npm-updater.ts
553
- import { readFileSync as readFileSync5, writeFileSync } from "fs";
554
- import { join as join6, relative as relative2 } from "path";
745
+ import { readFileSync as readFileSync7, writeFileSync } from "fs";
746
+ import { join as join8, relative as relative2 } from "path";
555
747
 
556
748
  // src/domain/ports.ts
557
749
  var InstallError = class extends Error {
@@ -564,13 +756,13 @@ var InstallError = class extends Error {
564
756
  };
565
757
 
566
758
  // src/adapters/npm/npm-lockfile.ts
567
- import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
568
- import { join as join5 } from "path";
759
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
760
+ import { join as join6 } from "path";
569
761
  function readLockfile(rootDir) {
570
- const file = join5(rootDir, "package-lock.json");
762
+ const file = join6(rootDir, "package-lock.json");
571
763
  if (!existsSync3(file)) return void 0;
572
764
  try {
573
- return JSON.parse(readFileSync4(file, "utf8"));
765
+ return JSON.parse(readFileSync5(file, "utf8"));
574
766
  } catch {
575
767
  return void 0;
576
768
  }
@@ -591,51 +783,168 @@ function findUnlinkedWorkspaceDependencies(lockfile, moduleRelPath, names) {
591
783
  return violations;
592
784
  }
593
785
 
786
+ // src/adapters/npm/pnpm-audit.ts
787
+ var AUDIT_TIMEOUT_MS = 6e4;
788
+ var LEVELS = ["info", "low", "moderate", "high", "critical"];
789
+ function unavailable(reason) {
790
+ return { message: `Vulnerability check unavailable: ${reason}.`, severity: "warn" };
791
+ }
792
+ function parsePnpmAudit(output) {
793
+ let json;
794
+ try {
795
+ json = JSON.parse(output);
796
+ } catch {
797
+ return unavailable("pnpm audit returned no result");
798
+ }
799
+ const counts = json?.metadata?.vulnerabilities;
800
+ if (counts && typeof counts === "object") {
801
+ const parts = [];
802
+ let total = 0;
803
+ for (const level of LEVELS) {
804
+ const n = Number(counts[level]) || 0;
805
+ total += n;
806
+ if (n > 0) parts.push(`${n} ${level}`);
807
+ }
808
+ if (total === 0) return { message: "No known vulnerabilities found.", severity: "clean" };
809
+ return {
810
+ message: `${total} ${total === 1 ? "vulnerability" : "vulnerabilities"} (${parts.join(", ")})`,
811
+ severity: "warn"
812
+ };
813
+ }
814
+ const errorMessage = json?.error?.message;
815
+ if (typeof errorMessage === "string" && errorMessage) return unavailable(errorMessage);
816
+ return unavailable("pnpm audit returned no result");
817
+ }
818
+
819
+ // src/adapters/npm/pnpm-lockfile.ts
820
+ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
821
+ import { join as join7 } from "path";
822
+ import { parse as parse2 } from "yaml";
823
+ var SECTIONS = ["dependencies", "devDependencies", "optionalDependencies"];
824
+ function readPnpmLockfile(rootDir) {
825
+ const file = join7(rootDir, "pnpm-lock.yaml");
826
+ if (!existsSync4(file)) return { kind: "missing" };
827
+ let data;
828
+ try {
829
+ data = parse2(readFileSync6(file, "utf8"));
830
+ } catch {
831
+ return { kind: "unreadable" };
832
+ }
833
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return { kind: "unreadable" };
834
+ const lockfileVersion = String(data.lockfileVersion ?? "");
835
+ if (!/^9(\.|$)/.test(lockfileVersion)) return { kind: "unsupported", lockfileVersion };
836
+ const importers = data.importers ?? {};
837
+ if (typeof importers !== "object" || Array.isArray(importers)) return { kind: "unreadable" };
838
+ return { kind: "ok", importers };
839
+ }
840
+ function findUnlinkedPnpmDependencies(lockfile, moduleRelPath, names) {
841
+ switch (lockfile.kind) {
842
+ case "missing":
843
+ return ["pnpm-lock.yaml is missing; workspace links cannot be verified"];
844
+ case "unreadable":
845
+ return ["pnpm-lock.yaml could not be read; workspace links cannot be verified"];
846
+ case "unsupported":
847
+ return [
848
+ `pnpm-lock.yaml has lockfileVersion '${lockfile.lockfileVersion}', which Elevate cannot read; workspace links cannot be verified`
849
+ ];
850
+ }
851
+ const importer = lockfile.importers[moduleRelPath === "" ? "." : moduleRelPath] ?? {};
852
+ const violations = [];
853
+ for (const name of names) {
854
+ let entry;
855
+ for (const section of SECTIONS) {
856
+ entry = importer[section]?.[name];
857
+ if (entry) break;
858
+ }
859
+ if (!entry) violations.push(`${name}: not present in pnpm-lock.yaml`);
860
+ else if (!String(entry.version).startsWith("link:")) {
861
+ violations.push(`${name}: resolved to ${entry.version} instead of linking the workspace package`);
862
+ }
863
+ }
864
+ return violations;
865
+ }
866
+
594
867
  // src/adapters/npm/npm-updater.ts
595
868
  var INSTALL_TIMEOUT_MS = 10 * 6e4;
869
+ function failureDetails(manager, result) {
870
+ return manager === "pnpm" ? tail(`${result.stdout}
871
+ ${result.stderr}`) : tail(result.stderr || result.stdout);
872
+ }
596
873
  var NpmUpdaterAdapter = class {
597
874
  affectedFiles(module, rootDir) {
875
+ if (detectPackageManager(rootDir).name === "pnpm") {
876
+ return [
877
+ join8(module.path, "package.json"),
878
+ join8(rootDir, "package.json"),
879
+ join8(rootDir, "pnpm-lock.yaml"),
880
+ join8(rootDir, "pnpm-workspace.yaml")
881
+ ];
882
+ }
598
883
  return [
599
- join6(module.path, "package.json"),
600
- join6(rootDir, "package.json"),
601
- join6(rootDir, "package-lock.json"),
602
- join6(rootDir, "npm-shrinkwrap.json")
884
+ join8(module.path, "package.json"),
885
+ join8(rootDir, "package.json"),
886
+ join8(rootDir, "package-lock.json"),
887
+ join8(rootDir, "npm-shrinkwrap.json")
603
888
  ];
604
889
  }
605
890
  async applyUpdates(module, rootDir, updates, onProgress) {
891
+ let manager;
606
892
  try {
607
- assertNpmManaged(rootDir);
893
+ manager = await prepareSupportedPackageManager(rootDir);
608
894
  } catch (err) {
609
- if (err instanceof UnsupportedPackageManagerError) throw new InstallError(err.message);
895
+ if (err instanceof UnsupportedPackageManagerError || err instanceof PackageManagerNotFoundError) {
896
+ throw new InstallError(err.message);
897
+ }
610
898
  throw err;
611
899
  }
612
- const pkgPath = join6(module.path, "package.json");
613
- let source;
614
- let pkg;
615
- try {
616
- source = readFileSync5(pkgPath, "utf8");
617
- pkg = JSON.parse(source);
618
- } catch {
619
- throw new InstallError(`Cannot read package.json in ${module.path}.`);
900
+ const catalogUpdates = updates.filter((u) => u.declaration?.kind === "catalog");
901
+ const manifestUpdates = updates.filter((u) => u.declaration?.kind !== "catalog");
902
+ const pkgPath = join8(module.path, "package.json");
903
+ let manifest;
904
+ if (manifestUpdates.length > 0 || catalogUpdates.length === 0) {
905
+ try {
906
+ const source = readFileSync7(pkgPath, "utf8");
907
+ manifest = { source, pkg: JSON.parse(source) };
908
+ } catch {
909
+ throw new InstallError(`Cannot read package.json in ${module.path}.`);
910
+ }
620
911
  }
621
- onProgress("Writing new versions to package.json\u2026");
622
- for (const item of updates) {
623
- const name = item.coordinate.artifact;
624
- for (const section of DEPENDENCY_SECTIONS) {
625
- if (typeof pkg[section]?.[name] === "string") pkg[section][name] = item.newRange;
912
+ if (manifest) {
913
+ onProgress("Writing new versions to package.json\u2026");
914
+ for (const item of manifestUpdates) {
915
+ const name = item.coordinate.artifact;
916
+ for (const section of DEPENDENCY_SECTIONS) {
917
+ if (typeof manifest.pkg[section]?.[name] === "string") manifest.pkg[section][name] = item.newRange;
918
+ }
626
919
  }
920
+ writeFileSync(pkgPath, serializeLike(manifest.source, manifest.pkg), "utf8");
627
921
  }
628
- writeFileSync(pkgPath, serializeLike(source, pkg), "utf8");
629
- onProgress("Running npm install\u2026");
630
- const install = await runCommand("npm", ["install"], { cwd: rootDir, timeoutMs: INSTALL_TIMEOUT_MS });
631
- if (install.exitCode !== 0) {
632
- const reason = install.timedOut ? "npm install timed out" : "npm install failed";
633
- throw new InstallError(reason, tail(install.stderr || install.stdout));
634
- }
635
- onProgress("Checking workspace links in package-lock.json\u2026");
636
- const integrityViolations = verifyAlignedLinks(module, rootDir, updates);
637
- const combined = `${install.stdout}
638
- ${install.stderr}`;
922
+ if (catalogUpdates.length > 0) {
923
+ onProgress("Writing new versions to pnpm-workspace.yaml\u2026");
924
+ writeCatalogRanges(catalogUpdates);
925
+ }
926
+ const install = commands.install(manager);
927
+ onProgress(`Running ${manager} install\u2026`);
928
+ const result = await runCommand(install.command, install.args, { cwd: rootDir, timeoutMs: INSTALL_TIMEOUT_MS });
929
+ if (result.exitCode !== 0) {
930
+ const reason = result.timedOut ? `${manager} install timed out` : `${manager} install failed`;
931
+ throw new InstallError(reason, failureDetails(manager, result));
932
+ }
933
+ const lockName = manager === "pnpm" ? "pnpm-lock.yaml" : "package-lock.json";
934
+ onProgress(`Checking workspace links in ${lockName}\u2026`);
935
+ const integrityViolations = verifyAlignedLinks(manager, module, rootDir, updates);
936
+ if (manager === "pnpm") {
937
+ const audit2 = integrityViolations.length > 0 ? void 0 : await runPnpmAudit(rootDir);
938
+ return {
939
+ updatedCount: updates.length,
940
+ auditMessage: audit2?.message ?? "",
941
+ auditSeverity: audit2?.severity ?? "clean",
942
+ fundingMessage: void 0,
943
+ integrityViolations
944
+ };
945
+ }
946
+ const combined = `${result.stdout}
947
+ ${result.stderr}`;
639
948
  const audit = parseAuditSummary(combined);
640
949
  const fundingMatch = combined.match(/(\d+ packages? (?:is|are) looking for funding)/i);
641
950
  return {
@@ -647,14 +956,42 @@ ${install.stderr}`;
647
956
  };
648
957
  }
649
958
  async resync(_module, rootDir, onProgress) {
650
- if (detectPackageManager(rootDir).name !== "npm") return;
651
- onProgress("Restoring node_modules (npm install)\u2026");
652
- const result = await runCommand("npm", ["install"], { cwd: rootDir, timeoutMs: INSTALL_TIMEOUT_MS });
959
+ const name = detectPackageManager(rootDir).name;
960
+ if (name !== "npm" && name !== "pnpm") return;
961
+ const restore = commands.restore(name);
962
+ onProgress(`Restoring node_modules (${name} install)\u2026`);
963
+ const result = await runCommand(restore.command, restore.args, { cwd: rootDir, timeoutMs: INSTALL_TIMEOUT_MS });
653
964
  if (result.exitCode !== 0) {
654
- throw new InstallError("npm install failed while restoring the previous state", tail(result.stderr));
965
+ throw new InstallError(
966
+ `${name} install failed while restoring the previous state`,
967
+ name === "pnpm" ? failureDetails(name, result) : tail(result.stderr)
968
+ );
655
969
  }
656
970
  }
657
971
  };
972
+ function writeCatalogRanges(updates) {
973
+ const sources = /* @__PURE__ */ new Map();
974
+ for (const item of updates) {
975
+ const declaration = item.declaration;
976
+ try {
977
+ let source = sources.get(declaration.file) ?? readFileSync7(declaration.file, "utf8");
978
+ source = setCatalogRange(source, declaration.catalogName ?? "default", item.coordinate.artifact, item.newRange);
979
+ sources.set(declaration.file, source);
980
+ } catch (err) {
981
+ throw new InstallError(err instanceof Error ? err.message : `Cannot update ${declaration.displayPath}.`);
982
+ }
983
+ }
984
+ for (const [file, source] of sources) writeFileSync(file, source, "utf8");
985
+ }
986
+ async function runPnpmAudit(rootDir) {
987
+ try {
988
+ const result = await runCommand("pnpm", ["audit", "--json"], { cwd: rootDir, timeoutMs: AUDIT_TIMEOUT_MS });
989
+ if (result.timedOut) return unavailable(`pnpm audit timed out after ${AUDIT_TIMEOUT_MS / 1e3} seconds`);
990
+ return parsePnpmAudit(result.stdout);
991
+ } catch {
992
+ return unavailable("pnpm audit returned no result");
993
+ }
994
+ }
658
995
  var VULNERABILITY_LINE = /^(?:found\s+)?(\d+)\s+(?:(?:info|low|moderate|high|critical)\s+severity\s+)?vulnerabilit(?:y|ies)\b.*$/i;
659
996
  function parseAuditSummary(output) {
660
997
  for (const line of output.split(/\r?\n/)) {
@@ -663,12 +1000,13 @@ function parseAuditSummary(output) {
663
1000
  }
664
1001
  return { message: "No known vulnerabilities found.", severity: "clean" };
665
1002
  }
666
- function verifyAlignedLinks(module, rootDir, updates) {
1003
+ function verifyAlignedLinks(manager, module, rootDir, updates) {
667
1004
  const aligned = updates.filter((u) => u.origin.kind === "workspace").map((u) => u.coordinate.identifier);
668
1005
  if (aligned.length === 0) return [];
1006
+ const moduleRelPath = relative2(rootDir, module.path).replace(/\\/g, "/");
1007
+ if (manager === "pnpm") return findUnlinkedPnpmDependencies(readPnpmLockfile(rootDir), moduleRelPath, aligned);
669
1008
  const lockfile = readLockfile(rootDir);
670
1009
  if (!lockfile) return ["package-lock.json is missing; workspace links cannot be verified"];
671
- const moduleRelPath = relative2(rootDir, module.path).replace(/\\/g, "/");
672
1010
  return findUnlinkedWorkspaceDependencies(lockfile, moduleRelPath, aligned);
673
1011
  }
674
1012
  function serializeLike(original, value) {
@@ -682,58 +1020,801 @@ function serializeLike(original, value) {
682
1020
  var VERIFY_TIMEOUT_MS = 10 * 6e4;
683
1021
  var NpmVerificationAdapter = class {
684
1022
  async verify(_module, rootDir, options = {}) {
685
- const label = options.customLabel || options.customScript || "Dependency tree integrity (npm ls)";
1023
+ let manager = "npm";
686
1024
  if (!options.customScript) {
687
1025
  try {
688
- assertNpmManaged(rootDir);
1026
+ manager = await prepareSupportedPackageManager(rootDir);
689
1027
  } catch (err) {
690
- if (err instanceof UnsupportedPackageManagerError) return { status: "warn", details: err.message, label };
1028
+ if (err instanceof UnsupportedPackageManagerError || err instanceof PackageManagerNotFoundError) {
1029
+ return { status: "warn", details: err.message, label: options.customLabel || "Dependency verification" };
1030
+ }
691
1031
  throw err;
692
1032
  }
693
1033
  }
694
- const result = options.customScript ? await runShellScript(options.customScript, { cwd: rootDir, timeoutMs: VERIFY_TIMEOUT_MS }) : await runCommand("npm", ["ls"], { cwd: rootDir, timeoutMs: VERIFY_TIMEOUT_MS });
695
- if (result.exitCode === 0) {
696
- return { status: "clean", details: "All dependencies are installed and consistent.", label };
1034
+ const defaultLabel = manager === "pnpm" ? "Lockfile consistency (pnpm install --frozen-lockfile)" : "Dependency tree integrity (npm ls)";
1035
+ const label = options.customLabel || options.customScript || defaultLabel;
1036
+ const verify = commands.verify(manager);
1037
+ const result = options.customScript ? await runShellScript(options.customScript, { cwd: rootDir, timeoutMs: VERIFY_TIMEOUT_MS }) : await runCommand(verify.command, verify.args, { cwd: rootDir, timeoutMs: VERIFY_TIMEOUT_MS });
1038
+ if (result.exitCode === 0) {
1039
+ const details = manager === "pnpm" && !options.customScript ? "pnpm-lock.yaml is up to date with all package.json files." : "All dependencies are installed and consistent.";
1040
+ return { status: "clean", details, label };
1041
+ }
1042
+ const output = manager === "pnpm" && !options.customScript ? tail(`${result.stdout}
1043
+ ${result.stderr}`) : tail(result.stderr || result.stdout);
1044
+ return { status: "warn", details: result.timedOut ? "Verification timed out." : output, label };
1045
+ }
1046
+ };
1047
+
1048
+ // src/adapters/npm/npm-strategy.ts
1049
+ var NpmEcosystemStrategy = class {
1050
+ ecosystem = "npm";
1051
+ displayName = "Node / npm";
1052
+ packageManagerFor(rootDir) {
1053
+ return detectPackageManager(rootDir).name;
1054
+ }
1055
+ displayNameFor(rootDir) {
1056
+ return detectPackageManager(rootDir).name === "pnpm" ? "Node / pnpm" : "Node / npm";
1057
+ }
1058
+ icon = "\u{1F4E6}";
1059
+ manifestFile = "package.json";
1060
+ versioning = npmVersionPolicy;
1061
+ discovery = new NpmModuleDiscoveryAdapter();
1062
+ registry = new NpmRegistryAdapter();
1063
+ reader = new NpmDependencyAdapter(this.registry, this.registry.config);
1064
+ updater = new NpmUpdaterAdapter();
1065
+ verifier = new NpmVerificationAdapter();
1066
+ getTabLabels(counts, t) {
1067
+ return {
1068
+ all: t.tabs.all(counts.total),
1069
+ prod: t.tabs.dependencies(counts.prodCount),
1070
+ dev: t.tabs.devDependencies(counts.devCount)
1071
+ };
1072
+ }
1073
+ };
1074
+
1075
+ // src/adapters/cargo/cargo-strategy.ts
1076
+ import semver10 from "semver";
1077
+
1078
+ // src/adapters/cargo/cargo-discovery.ts
1079
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync8 } from "fs";
1080
+ import { dirname as dirname4, join as join9, relative as relative3, resolve as resolve4 } from "path";
1081
+
1082
+ // src/adapters/cargo/cargo-command.ts
1083
+ var CargoUnavailableError = class extends Error {
1084
+ constructor() {
1085
+ super("Cargo is not available: no `cargo` on the PATH. Install Rust (https://rustup.rs).");
1086
+ this.name = "CargoUnavailableError";
1087
+ }
1088
+ };
1089
+ async function runCargo(args, options) {
1090
+ let result;
1091
+ try {
1092
+ result = await runCommand("cargo", args, options);
1093
+ } catch (err) {
1094
+ if (err.code === "ENOENT") throw new CargoUnavailableError();
1095
+ throw err;
1096
+ }
1097
+ if (looksLikeMissingCommand(result) && !await cargoStarts(options.cwd)) throw new CargoUnavailableError();
1098
+ return result;
1099
+ }
1100
+ function looksLikeMissingCommand(result) {
1101
+ return process.platform === "win32" && result.exitCode !== 0 && !result.stdout.trim() && !/^error/m.test(result.stderr);
1102
+ }
1103
+ async function cargoStarts(cwd) {
1104
+ try {
1105
+ return (await runCommand("cargo", ["--version"], { cwd, timeoutMs: 3e4 })).exitCode === 0;
1106
+ } catch {
1107
+ return false;
1108
+ }
1109
+ }
1110
+ function cargoErrors(output) {
1111
+ const lines = output.split(/\r?\n/);
1112
+ const errors = lines.filter((l) => /^error(\[E\d+\])?:/.test(l));
1113
+ const summary = (errors.length > 0 ? errors : lines.filter((l) => l.trim()).slice(-10)).slice(0, 12);
1114
+ if (/toolchain '.+' is not installed/.test(output)) {
1115
+ summary.push("Install it with `rustup toolchain install` (the project pins it in rust-toolchain.toml).");
1116
+ }
1117
+ return summary.join("\n");
1118
+ }
1119
+
1120
+ // src/adapters/cargo/cargo-metadata.ts
1121
+ var CargoMetadataError = class extends Error {
1122
+ constructor(message2, dir) {
1123
+ super(message2);
1124
+ this.dir = dir;
1125
+ this.name = "CargoMetadataError";
1126
+ }
1127
+ dir;
1128
+ };
1129
+ async function loadMetadata(dir) {
1130
+ const result = await runCargo(["metadata", "--no-deps", "--format-version", "1"], { cwd: dir, timeoutMs: 6e4 });
1131
+ if (result.exitCode !== 0) {
1132
+ throw new CargoMetadataError(`cargo metadata failed in ${dir}: ${cargoErrors(result.stderr || result.stdout)}`, dir);
1133
+ }
1134
+ try {
1135
+ return JSON.parse(result.stdout);
1136
+ } catch {
1137
+ throw new CargoMetadataError(`cargo metadata printed no JSON in ${dir}.`, dir);
1138
+ }
1139
+ }
1140
+
1141
+ // src/adapters/cargo/cargo-discovery.ts
1142
+ var IGNORED = /* @__PURE__ */ new Set(["target", "node_modules", "vendor", "dist", "build", "out", "src"]);
1143
+ var norm = (path) => resolve4(path).replace(/\\/g, "/");
1144
+ function findManifests(root) {
1145
+ const found = [];
1146
+ const walk = (dir, depth) => {
1147
+ let entries2;
1148
+ try {
1149
+ entries2 = readdirSync(dir, { withFileTypes: true });
1150
+ } catch {
1151
+ return;
1152
+ }
1153
+ if (entries2.some((e) => e.isFile() && e.name === "Cargo.toml")) found.push(join9(dir, "Cargo.toml"));
1154
+ for (const entry of entries2) {
1155
+ if (entry.isDirectory() && !entry.name.startsWith(".") && !IGNORED.has(entry.name)) walk(join9(dir, entry.name), depth + 1);
1156
+ }
1157
+ };
1158
+ walk(root, 0);
1159
+ return found.sort((a, b) => a.split(/[\\/]/).length - b.split(/[\\/]/).length || a.localeCompare(b));
1160
+ }
1161
+ var declaresWorkspace = (dir) => {
1162
+ try {
1163
+ return /^\s*\[workspace[\].]/m.test(readFileSync8(join9(dir, "Cargo.toml"), "utf8"));
1164
+ } catch {
1165
+ return false;
1166
+ }
1167
+ };
1168
+ var CargoModuleDiscoveryAdapter = class {
1169
+ problems = [];
1170
+ diagnostics() {
1171
+ return [...this.problems];
1172
+ }
1173
+ /** The nearest ancestor declaring `[workspace]`; failing that, the nearest directory with a manifest. */
1174
+ findRoot(startDir, boundaryDir) {
1175
+ const boundary = boundaryDir ? resolve4(boundaryDir) : void 0;
1176
+ const chain = [];
1177
+ for (let dir = resolve4(startDir); ; ) {
1178
+ chain.push(dir);
1179
+ const parent = dirname4(dir);
1180
+ if (dir === boundary || parent === dir) break;
1181
+ dir = parent;
1182
+ }
1183
+ return chain.find(declaresWorkspace) ?? chain.find((d) => existsSync5(join9(d, "Cargo.toml")));
1184
+ }
1185
+ async discover(rootDir) {
1186
+ const covered = /* @__PURE__ */ new Set();
1187
+ const modules = [];
1188
+ const errors = [];
1189
+ this.problems = [];
1190
+ for (const manifest of findManifests(rootDir)) {
1191
+ if (covered.has(norm(manifest))) continue;
1192
+ try {
1193
+ const metadata = await loadMetadata(dirname4(manifest));
1194
+ for (const pkg of metadata.packages) {
1195
+ covered.add(norm(pkg.manifest_path));
1196
+ const path = dirname4(pkg.manifest_path);
1197
+ const relPath = relative3(rootDir, path).replace(/\\/g, "/");
1198
+ modules.push({
1199
+ id: pkg.name,
1200
+ name: pkg.name,
1201
+ path,
1202
+ relPath: relPath === "" ? "Root" : relPath,
1203
+ ecosystem: "cargo",
1204
+ isRoot: relPath === "",
1205
+ version: pkg.version,
1206
+ aggregatorDir: resolve4(metadata.workspace_root)
1207
+ });
1208
+ }
1209
+ } catch (err) {
1210
+ if (err instanceof CargoUnavailableError) throw err;
1211
+ if (err instanceof CargoMetadataError) {
1212
+ errors.push(err);
1213
+ this.problems.push(err.message);
1214
+ } else throw err;
1215
+ }
1216
+ }
1217
+ if (modules.length === 0 && errors.length > 0) throw errors[0];
1218
+ modules.sort((a, b) => a.isRoot ? -1 : b.isRoot ? 1 : a.relPath.localeCompare(b.relPath));
1219
+ const taken = /* @__PURE__ */ new Set();
1220
+ for (const module of modules) {
1221
+ if (taken.has(module.id)) module.id = `${module.name} (${module.relPath})`;
1222
+ taken.add(module.id);
1223
+ }
1224
+ return modules;
1225
+ }
1226
+ };
1227
+
1228
+ // src/adapters/cargo/cargo-config.ts
1229
+ import { existsSync as existsSync6, readFileSync as readFileSync9 } from "fs";
1230
+ import { homedir } from "os";
1231
+ import { dirname as dirname5, isAbsolute, join as join10, resolve as resolve5 } from "path";
1232
+ import { getStaticTOMLValue, parseTOML } from "toml-eslint-parser";
1233
+ var CRATES_IO = "registry+https://github.com/rust-lang/crates.io-index";
1234
+ var PUBLIC_HOSTS = /* @__PURE__ */ new Set(["index.crates.io", "static.crates.io", "crates.io", "github.com"]);
1235
+ var UnsupportedSourceError = class extends Error {
1236
+ constructor(detail) {
1237
+ super(`Cargo is configured to use a package source Elevate cannot read (${detail}). No lookup was made.`);
1238
+ this.name = "UnsupportedSourceError";
1239
+ }
1240
+ };
1241
+ function isPublicIndex(index) {
1242
+ try {
1243
+ const url = new URL(index.replace(/^(sparse|registry)\+/, ""));
1244
+ return PUBLIC_HOSTS.has(url.hostname.toLowerCase()) && (url.hostname !== "github.com" || url.pathname.includes("crates.io-index"));
1245
+ } catch {
1246
+ return false;
1247
+ }
1248
+ }
1249
+ function merge(base, top) {
1250
+ const out = { ...base };
1251
+ for (const [key, value] of Object.entries(top)) {
1252
+ const existing = out[key];
1253
+ out[key] = value && typeof value === "object" && !Array.isArray(value) && existing && typeof existing === "object" && !Array.isArray(existing) ? merge(existing, value) : value;
1254
+ }
1255
+ return out;
1256
+ }
1257
+ function loadFile(file, depth = 0) {
1258
+ let table;
1259
+ try {
1260
+ table = getStaticTOMLValue(parseTOML(readFileSync9(file, "utf8")));
1261
+ } catch {
1262
+ return {};
1263
+ }
1264
+ const includes = table.include;
1265
+ delete table.include;
1266
+ if (!Array.isArray(includes) || depth > 4) return table;
1267
+ let included = {};
1268
+ for (const entry of includes) {
1269
+ const path = typeof entry === "string" ? entry : entry?.path;
1270
+ if (typeof path === "string") included = merge(included, loadFile(isAbsolute(path) ? path : resolve5(dirname5(file), path), depth + 1));
1271
+ }
1272
+ return merge(included, table);
1273
+ }
1274
+ var CargoConfigReader = class {
1275
+ env;
1276
+ cache = /* @__PURE__ */ new Map();
1277
+ constructor(options = {}) {
1278
+ this.env = options.env ?? process.env;
1279
+ }
1280
+ /** The configuration as Cargo would merge it for `cwd`: nearest file first, CARGO_HOME last. */
1281
+ config(cwd) {
1282
+ const start = resolve5(cwd);
1283
+ let cached = this.cache.get(start);
1284
+ if (cached) return cached;
1285
+ const files = [];
1286
+ for (let dir = start; ; ) {
1287
+ for (const name of ["config.toml", "config"]) {
1288
+ const file = join10(dir, ".cargo", name);
1289
+ if (existsSync6(file)) {
1290
+ files.push(file);
1291
+ break;
1292
+ }
1293
+ }
1294
+ const parent = dirname5(dir);
1295
+ if (parent === dir) break;
1296
+ dir = parent;
1297
+ }
1298
+ const home2 = this.env.CARGO_HOME ?? join10(homedir(), ".cargo");
1299
+ for (const name of ["config.toml", "config"]) {
1300
+ const file = join10(home2, name);
1301
+ if (existsSync6(file) && !files.includes(file)) {
1302
+ files.push(file);
1303
+ break;
1304
+ }
1305
+ }
1306
+ cached = files.reduceRight((acc, file) => merge(acc, loadFile(file)), {});
1307
+ this.cache.set(start, cached);
1308
+ return cached;
1309
+ }
1310
+ /** `[source.*]` comes from files only: Cargo ignores `CARGO_SOURCE_*` variables (checked with Cargo 1.97). */
1311
+ sourceField(config, source, field) {
1312
+ const value = config.source?.[source]?.[field];
1313
+ return typeof value === "string" ? value : void 0;
1314
+ }
1315
+ /**
1316
+ * The index a lookup goes to. `registry` is the index URL of an alternative
1317
+ * registry as Cargo reports it, or undefined for crates.io.
1318
+ */
1319
+ async resolve(registry, cwd) {
1320
+ const config = this.config(cwd);
1321
+ let name = registry === void 0 ? "crates-io" : this.sourceNamed(config, registry);
1322
+ let index = registry ?? CRATES_IO;
1323
+ const seen = /* @__PURE__ */ new Set();
1324
+ while (name !== void 0) {
1325
+ if (seen.has(name)) throw new UnsupportedSourceError(`replacement cycle at '${name}'`);
1326
+ seen.add(name);
1327
+ const next = this.sourceField(config, name, "replace-with");
1328
+ if (next === void 0) break;
1329
+ for (const kind of ["directory", "local-registry", "git"]) {
1330
+ if (this.sourceField(config, next, kind) !== void 0) throw new UnsupportedSourceError(`'${next}' is a ${kind} source`);
1331
+ }
1332
+ const target = this.sourceField(config, next, "registry");
1333
+ if (target !== void 0) {
1334
+ index = target;
1335
+ name = next;
1336
+ if (this.sourceField(config, next, "replace-with") === void 0) break;
1337
+ continue;
1338
+ }
1339
+ if (this.sourceField(config, next, "replace-with") === void 0) {
1340
+ throw new UnsupportedSourceError(`'${next}' has no registry`);
1341
+ }
1342
+ name = next;
1343
+ }
1344
+ return { index, isPublic: isPublicIndex(index) };
1345
+ }
1346
+ /** Name of the `[source.*]` table whose registry is `url`, if any. */
1347
+ sourceNamed(config, url) {
1348
+ return Object.keys(config.source ?? {}).find((n) => this.sourceField(config, n, "registry") === url);
1349
+ }
1350
+ };
1351
+
1352
+ // src/version.ts
1353
+ import { createRequire } from "module";
1354
+ var VERSION = true ? "1.5.0" : fromPackage();
1355
+
1356
+ // src/adapters/cargo/cargo-index.ts
1357
+ var RegistryAuthRequiredError = class extends Error {
1358
+ constructor(index) {
1359
+ super(`The registry ${index} requires authentication. Elevate does not handle registry credentials.`);
1360
+ this.index = index;
1361
+ this.name = "RegistryAuthRequiredError";
1362
+ }
1363
+ index;
1364
+ };
1365
+ var CrateNotFoundError = class extends Error {
1366
+ constructor(crate, index) {
1367
+ super(`'${crate}' is not in the registry ${index}.`);
1368
+ this.crate = crate;
1369
+ this.index = index;
1370
+ this.name = "CrateNotFoundError";
1371
+ }
1372
+ crate;
1373
+ index;
1374
+ };
1375
+ var UnsupportedIndexError = class extends Error {
1376
+ constructor(index) {
1377
+ super(`The registry index ${index} is not a sparse index; Elevate cannot read it.`);
1378
+ this.index = index;
1379
+ this.name = "UnsupportedIndexError";
1380
+ }
1381
+ index;
1382
+ };
1383
+ var CRATES_IO_SPARSE = "https://index.crates.io/";
1384
+ var CRATES_IO_GIT = "registry+https://github.com/rust-lang/crates.io-index";
1385
+ function indexPath(name) {
1386
+ const n = name.toLowerCase();
1387
+ if (n.length <= 2) return `${n.length}/${n}`;
1388
+ if (n.length === 3) return `3/${n[0]}/${n}`;
1389
+ return `${n.slice(0, 2)}/${n.slice(2, 4)}/${n}`;
1390
+ }
1391
+ function parseIndex(text) {
1392
+ const versions = [];
1393
+ for (const raw of text.split(/\r?\n/)) {
1394
+ if (!raw.trim()) continue;
1395
+ try {
1396
+ const entry = JSON.parse(raw);
1397
+ if (typeof entry.vers !== "string") continue;
1398
+ versions.push({ version: entry.vers, yanked: entry.yanked === true, rustVersion: entry.rust_version });
1399
+ } catch {
1400
+ }
1401
+ }
1402
+ return versions;
1403
+ }
1404
+ function sparseRoot(index) {
1405
+ if (index === CRATES_IO_GIT) return CRATES_IO_SPARSE;
1406
+ if (index.startsWith("sparse+")) {
1407
+ const url = index.slice("sparse+".length);
1408
+ return url.endsWith("/") ? url : `${url}/`;
1409
+ }
1410
+ throw new UnsupportedIndexError(index);
1411
+ }
1412
+ var userAgent = () => `elevate/${VERSION} (+https://github.com/DennisRepa/Elevate)`;
1413
+ function networkReason(err) {
1414
+ const cause = err?.cause?.message;
1415
+ return cause ?? (err instanceof Error ? err.message : String(err));
1416
+ }
1417
+ var CargoIndexClient = class {
1418
+ constructor(fetchFn = fetch, env = process.env) {
1419
+ this.fetchFn = fetchFn;
1420
+ this.env = env;
1421
+ }
1422
+ fetchFn;
1423
+ env;
1424
+ memo = /* @__PURE__ */ new Map();
1425
+ /** All versions of a crate in oldest-first file order. Memoised per index and crate. */
1426
+ versions(index, crate) {
1427
+ const key = `${index}
1428
+ ${crate.toLowerCase()}`;
1429
+ let pending = this.memo.get(key);
1430
+ if (!pending) {
1431
+ pending = this.load(index, crate);
1432
+ this.memo.set(key, pending);
1433
+ }
1434
+ return pending;
1435
+ }
1436
+ async load(index, crate) {
1437
+ const url = `${sparseRoot(index)}${indexPath(crate)}`;
1438
+ let response;
1439
+ try {
1440
+ response = await this.fetchFn(url, { headers: { "user-agent": userAgent() } });
1441
+ } catch (err) {
1442
+ throw new Error(`Cannot reach ${url}: ${networkReason(err)}${this.proxyHint()}`);
1443
+ }
1444
+ if (response.status === 401 || response.status === 403) throw new RegistryAuthRequiredError(index);
1445
+ if (response.status === 404) throw new CrateNotFoundError(crate, index);
1446
+ if (!response.ok) throw new Error(`Registry answered ${response.status} for ${url}.`);
1447
+ return parseIndex(await response.text());
1448
+ }
1449
+ /**
1450
+ * Node's fetch ignores HTTP(S)_PROXY unless NODE_USE_ENV_PROXY=1 is set when
1451
+ * the process starts (measured with Node 22.23; it cannot be switched on later).
1452
+ */
1453
+ proxyHint() {
1454
+ const proxied = this.env.HTTPS_PROXY || this.env.https_proxy || this.env.HTTP_PROXY || this.env.http_proxy;
1455
+ if (!proxied || this.env.NODE_USE_ENV_PROXY === "1") return "";
1456
+ return " A proxy is configured, but Node does not use it for registry lookups unless you start Elevate with NODE_USE_ENV_PROXY=1.";
1457
+ }
1458
+ };
1459
+
1460
+ // src/adapters/cargo/cargo-registry.ts
1461
+ import semver6 from "semver";
1462
+
1463
+ // src/adapters/cargo/cargo-versions.ts
1464
+ import semver5 from "semver";
1465
+ var rust = (v) => semver5.coerce(v)?.version;
1466
+ function selectVersion(versions, options) {
1467
+ const candidates = versions.filter((v) => !v.yanked && semver5.valid(v.version)).filter((v) => options.channel === "all" || semver5.prerelease(v.version) === null).sort((a, b) => semver5.rcompare(a.version, b.version));
1468
+ const project = options.rustVersion ? rust(options.rustVersion) : void 0;
1469
+ let blocked;
1470
+ for (const candidate of candidates) {
1471
+ const needs = candidate.rustVersion ? rust(candidate.rustVersion) : void 0;
1472
+ if (project && needs && semver5.gt(needs, project)) {
1473
+ blocked ??= { version: candidate.version, requires: candidate.rustVersion };
1474
+ continue;
1475
+ }
1476
+ return { version: candidate.version, ...blocked ? { blockedByMsrv: blocked } : {} };
1477
+ }
1478
+ return { version: void 0, ...blocked ? { blockedByMsrv: blocked } : {} };
1479
+ }
1480
+
1481
+ // src/adapters/cargo/cargo-registry.ts
1482
+ var CargoRegistryAdapter = class {
1483
+ constructor(deps) {
1484
+ this.deps = deps;
1485
+ }
1486
+ deps;
1487
+ async resolveEndpoint(coordinate, cwd) {
1488
+ const { index, isPublic } = await this.deps.endpoint(coordinate.registry, cwd);
1489
+ return { url: index, isPublic };
1490
+ }
1491
+ async getLatestVersion(coordinate, channel = "stable", cwd = process.cwd()) {
1492
+ return selectVersion(await this.load(coordinate, cwd), { channel }).version ?? null;
1493
+ }
1494
+ /** Published, not yanked, newest first. Lookup errors propagate so they are not mistaken for "no versions". */
1495
+ async getAllVersions(coordinate, cwd = process.cwd()) {
1496
+ return (await this.load(coordinate, cwd)).filter((v) => !v.yanked && semver6.valid(v.version)).map((v) => v.version).sort(semver6.rcompare);
1497
+ }
1498
+ async load(coordinate, cwd) {
1499
+ const { index } = await this.deps.endpoint(coordinate.registry, cwd);
1500
+ return this.deps.index.versions(index, coordinate.artifact);
1501
+ }
1502
+ };
1503
+
1504
+ // src/adapters/cargo/cargo-requirement.ts
1505
+ import semver7 from "semver";
1506
+ var SIMPLE = /^(\^|~|=)?\s*(\d+(?:\.\d+){0,2})$/;
1507
+ function toSemverRange(requirement) {
1508
+ const parts = requirement.split(",").map((p) => p.trim());
1509
+ if (parts.some((p) => p === "")) return null;
1510
+ const converted = parts.map((part) => {
1511
+ const bare = part.match(/^\d/);
1512
+ return bare ? `^${part}` : part;
1513
+ });
1514
+ return semver7.validRange(converted.join(" ")) ? converted.join(" ") : null;
1515
+ }
1516
+ function satisfiesRequirement(requirement, version) {
1517
+ const range = toSemverRange(requirement);
1518
+ return range !== null && semver7.satisfies(version, range, { includePrerelease: false });
1519
+ }
1520
+ function isUpgradableRequirement(requirement) {
1521
+ return SIMPLE.test(requirement.trim());
1522
+ }
1523
+ function requirementFloor(requirement) {
1524
+ const range = toSemverRange(requirement);
1525
+ return range ? semver7.minVersion(range)?.version ?? null : null;
1526
+ }
1527
+ function rewriteRequirement(current, target) {
1528
+ const match = current.trim().match(SIMPLE);
1529
+ if (!match) return target;
1530
+ if (semver7.prerelease(target)) return `${match[1] ?? ""}${target}`;
1531
+ const precision = match[2].split(".").length;
1532
+ const [major, minor, patch] = target.split(/[-+]/)[0].split(".");
1533
+ const written = [major, minor, patch].slice(0, precision).join(".");
1534
+ return `${match[1] ?? ""}${written}`;
1535
+ }
1536
+ function cargoDiff(from, to) {
1537
+ const a = semver7.parse(from);
1538
+ const b = semver7.parse(to);
1539
+ if (!a || !b) return "minor";
1540
+ if (a.major > 0 || b.major > a.major) {
1541
+ if (b.major !== a.major) return "major";
1542
+ return b.minor !== a.minor ? "minor" : "patch";
1543
+ }
1544
+ if (a.minor > 0) {
1545
+ if (b.minor !== a.minor) return "major";
1546
+ return "minor";
1547
+ }
1548
+ return "major";
1549
+ }
1550
+
1551
+ // src/adapters/cargo/cargo-scanner.ts
1552
+ import { readFileSync as readFileSync10 } from "fs";
1553
+ import { join as join11, relative as relative4 } from "path";
1554
+ import semver8 from "semver";
1555
+
1556
+ // src/adapters/cargo/cargo-toml.ts
1557
+ import { parseTOML as parseTOML2 } from "toml-eslint-parser";
1558
+ var SECTIONS2 = {
1559
+ dependencies: "dependencies",
1560
+ "dev-dependencies": "dev-dependencies",
1561
+ dev_dependencies: "dev-dependencies",
1562
+ "build-dependencies": "build-dependencies",
1563
+ build_dependencies: "build-dependencies"
1564
+ };
1565
+ function parseCargoManifest(source) {
1566
+ const root = parseTOML2(source).body[0];
1567
+ const leaves = [];
1568
+ const keyPath = (key) => key.keys.map((k) => k.type === "TOMLBare" ? k.name : k.value);
1569
+ const visitPair = (prefix, pair) => {
1570
+ const path = [...prefix, ...keyPath(pair.key)];
1571
+ leaves.push({ path, node: pair.value });
1572
+ if (pair.value.type === "TOMLInlineTable") for (const inner of pair.value.body) visitPair(path, inner);
1573
+ };
1574
+ for (const element of root.body) {
1575
+ if (element.type === "TOMLKeyValue") visitPair([], element);
1576
+ else if (element.type === "TOMLTable") {
1577
+ const header = keyPath(element.key);
1578
+ for (const pair of element.body) visitPair(header, pair);
1579
+ leaves.push({ path: header, node: element });
1580
+ }
1581
+ }
1582
+ const byKey = /* @__PURE__ */ new Map();
1583
+ for (const { path, node } of leaves) {
1584
+ const located = locateSection(path);
1585
+ if (!located || located.rest.length === 0) continue;
1586
+ const [key, field] = located.rest;
1587
+ const id = `${located.inWorkspace}|${located.target ?? ""}|${located.section}|${key}`;
1588
+ let decl = byKey.get(id);
1589
+ if (!decl) {
1590
+ decl = { key, section: located.section, target: located.target, inWorkspace: located.inWorkspace, inherited: false };
1591
+ byKey.set(id, decl);
1592
+ }
1593
+ if (located.rest.length === 1 && node.type === "TOMLValue" && node.kind === "string") {
1594
+ decl.version = literalOf(node);
1595
+ } else if (field === "version" && located.rest.length === 2 && node.type === "TOMLValue" && node.kind === "string") {
1596
+ decl.version = literalOf(node);
1597
+ } else if (field === "workspace" && located.rest.length === 2 && node.value === true) {
1598
+ decl.inherited = true;
1599
+ } else if (field === "package" && located.rest.length === 2 && node.type === "TOMLValue") {
1600
+ decl.package = String(node.value);
1601
+ }
1602
+ }
1603
+ return [...byKey.values()];
1604
+ }
1605
+ function literalOf(node) {
1606
+ return { value: node.value, start: node.range[0] + 1, end: node.range[1] - 1 };
1607
+ }
1608
+ function locateSection(path) {
1609
+ let i = 0;
1610
+ let target;
1611
+ let inWorkspace = false;
1612
+ if (path[0] === "workspace" && path[1] === "dependencies") {
1613
+ return { section: "dependencies", inWorkspace: true, target, rest: path.slice(2) };
1614
+ }
1615
+ if (path[0] === "target" && path.length >= 3) {
1616
+ target = path[1];
1617
+ i = 2;
1618
+ }
1619
+ const section = SECTIONS2[path[i] ?? ""];
1620
+ if (!section) return void 0;
1621
+ return { section, inWorkspace, target, rest: path.slice(i + 1) };
1622
+ }
1623
+
1624
+ // src/adapters/cargo/cargo-scanner.ts
1625
+ var CONCURRENCY2 = 6;
1626
+ var DIFF_ORDER2 = { major: 0, minor: 1, patch: 2 };
1627
+ var CargoDependencyAdapter = class {
1628
+ constructor(deps) {
1629
+ this.deps = deps;
1630
+ }
1631
+ deps;
1632
+ async scan(module, context) {
1633
+ const metadata = await this.deps.loadMetadata(module.aggregatorDir ?? module.path);
1634
+ const pkg = metadata.packages.find((p) => samePath(join11(p.manifest_path, ".."), module.path));
1635
+ if (!pkg) return { candidates: [], skipped: [] };
1636
+ const own = readDeclarations(pkg.manifest_path);
1637
+ const rootManifest = join11(metadata.workspace_root, "Cargo.toml");
1638
+ const root = readDeclarations(rootManifest);
1639
+ const groups = groupDependencies(pkg.dependencies);
1640
+ const outcomes = await mapPooled(groups, CONCURRENCY2, async (group) => {
1641
+ const primary = group.find((d) => d.kind === null) ?? group[0];
1642
+ const key = primary.rename ?? primary.name;
1643
+ const decl = findDeclaration(own, key, primary);
1644
+ const located = locate(decl, root, key, context.rootDir, pkg.manifest_path, rootManifest);
1645
+ const scope = group.every((d) => d.kind === "dev") ? "dev" : "prod";
1646
+ const qualifier = primary.target ?? (primary.kind === "build" ? "build" : void 0);
1647
+ const common = { scope, qualifier };
1648
+ if (primary.source === null && primary.path) {
1649
+ const target = [...context.workspace.values()].find((m) => samePath(m.path, primary.path));
1650
+ return target ? alignment(primary, target, located, common) : null;
1651
+ }
1652
+ if (!primary.source || primary.source.startsWith("git+")) return null;
1653
+ const outcome = await this.registryOutcome(primary, located, pkg, metadata, common, context);
1654
+ const other = group.find((d) => d !== primary && !sameRequirement(d.req, primary.req));
1655
+ if (outcome && "candidate" in outcome && other) {
1656
+ outcome.note = {
1657
+ identifier: primary.name,
1658
+ origin: outcome.candidate.origin.kind,
1659
+ reason: "declaration-mismatch",
1660
+ detail: `${sectionOf(other)} requires ${other.req}; it is not changed`
1661
+ };
1662
+ }
1663
+ return outcome;
1664
+ });
1665
+ const candidates = [];
1666
+ const skipped = [];
1667
+ for (const outcome of outcomes) {
1668
+ if (!outcome) continue;
1669
+ if ("candidate" in outcome) {
1670
+ candidates.push(outcome.candidate);
1671
+ if (outcome.note) skipped.push(outcome.note);
1672
+ } else skipped.push(outcome.skipped);
1673
+ }
1674
+ candidates.sort((a, b) => (DIFF_ORDER2[a.diff] ?? 3) - (DIFF_ORDER2[b.diff] ?? 3));
1675
+ return { candidates, skipped };
1676
+ }
1677
+ async registryOutcome(dep, located, pkg, metadata, common, context) {
1678
+ const literal = located?.version.value ?? dep.req;
1679
+ if (!isUpgradableRequirement(literal)) return null;
1680
+ if (!located || !sameRequirement(located.version.value, dep.req)) {
1681
+ return { skipped: { identifier: dep.name, origin: "public", reason: "declaration-mismatch", detail: `${literal} in the manifest, ${dep.req} for Cargo` } };
697
1682
  }
698
- return {
699
- status: "warn",
700
- details: result.timedOut ? "Verification timed out." : tail(result.stderr || result.stdout),
701
- label
1683
+ let origin = { kind: "public" };
1684
+ if (dep.registry) origin = { kind: "private", matchedBy: "scoped-registry", registry: dep.registry };
1685
+ else if (matchesInternalScope(dep.name, "cargo", context.internalScopes)) origin = { kind: "private", matchedBy: "internal-scope" };
1686
+ const endpoint = await this.deps.endpoint(dep.registry ?? void 0, context.rootDir);
1687
+ if (origin.kind === "private" && endpoint.isPublic) {
1688
+ return { skipped: { identifier: dep.name, origin: "private", reason: "private-on-public-registry", detail: endpoint.index } };
1689
+ }
1690
+ let versions;
1691
+ try {
1692
+ versions = await this.deps.index.versions(endpoint.index, dep.name);
1693
+ } catch (err) {
1694
+ return { skipped: { identifier: dep.name, origin: origin.kind, reason: "lookup-failed", detail: err instanceof Error ? err.message : String(err) } };
1695
+ }
1696
+ const chosen = selectVersion(versions, { channel: context.channel, rustVersion: pkg.rust_version ?? void 0 });
1697
+ const floor = requirementFloor(literal);
1698
+ if (!chosen.version) {
1699
+ if (chosen.blockedByMsrv && floor) {
1700
+ const { version, requires } = chosen.blockedByMsrv;
1701
+ return { skipped: { identifier: dep.name, origin: origin.kind, reason: "incompatible-rust-version", detail: ` (${version} needs Rust ${requires})` } };
1702
+ }
1703
+ return { skipped: { identifier: dep.name, origin: origin.kind, reason: "lookup-failed", detail: "no usable version in the registry" } };
1704
+ }
1705
+ const latest = chosen.version;
1706
+ if (!floor || !semver8.gt(latest, floor)) {
1707
+ const held = chosen.blockedByMsrv;
1708
+ if (floor && held && semver8.gt(held.version, floor)) {
1709
+ return { skipped: { identifier: dep.name, origin: origin.kind, reason: "incompatible-rust-version", detail: ` (${held.version} needs Rust ${held.requires})` } };
1710
+ }
1711
+ return null;
1712
+ }
1713
+ const newRange = rewriteRequirement(literal, latest);
1714
+ if (newRange === literal) return null;
1715
+ const tag = semver8.prerelease(latest) ? extractPreReleaseTag(latest) ?? "PRE" : void 0;
1716
+ const diff = cargoDiff(floor, latest);
1717
+ const candidate = {
1718
+ coordinate: { identifier: dep.name, artifact: dep.name, ecosystem: "cargo", ...dep.registry ? { registry: dep.registry } : {} },
1719
+ currentRange: literal,
1720
+ currentClean: floor,
1721
+ latest,
1722
+ newRange,
1723
+ diff,
1724
+ scope: common.scope,
1725
+ selected: diff !== "major",
1726
+ origin,
1727
+ action: "update",
1728
+ declaration: located.declaration,
1729
+ isPreRelease: tag !== void 0,
1730
+ preReleaseTag: tag,
1731
+ ...common.qualifier ? { qualifier: common.qualifier } : {},
1732
+ ...located.inherited ? { sharedWith: sharersOf(metadata, pkg, dep.rename ?? dep.name) } : {}
702
1733
  };
1734
+ return { candidate };
703
1735
  }
704
1736
  };
705
-
706
- // src/adapters/npm/npm-strategy.ts
707
- var NpmEcosystemStrategy = class {
708
- ecosystem = "npm";
709
- displayName = "Node / npm";
710
- icon = "\u{1F4E6}";
711
- manifestFile = "package.json";
712
- discovery = new NpmModuleDiscoveryAdapter();
713
- registry = new NpmRegistryAdapter();
714
- reader = new NpmDependencyAdapter(this.registry, this.registry.config);
715
- updater = new NpmUpdaterAdapter();
716
- verifier = new NpmVerificationAdapter();
717
- getTabLabels(counts, t) {
1737
+ function alignment(dep, target, located, common) {
1738
+ const local = target.version;
1739
+ if (!located || !local || !semver8.valid(local)) return null;
1740
+ const literal = located.version.value;
1741
+ if (!isUpgradableRequirement(literal) || satisfiesRequirement(literal, local)) return null;
1742
+ const floor = requirementFloor(literal) ?? "0.0.0";
1743
+ const diff = cargoDiff(floor, local);
1744
+ const tag = semver8.prerelease(local) ? extractPreReleaseTag(local) ?? "PRE" : void 0;
1745
+ return {
1746
+ candidate: {
1747
+ coordinate: { identifier: dep.name, artifact: dep.name, ecosystem: "cargo" },
1748
+ currentRange: literal,
1749
+ currentClean: floor,
1750
+ latest: local,
1751
+ newRange: rewriteRequirement(literal, local),
1752
+ diff,
1753
+ scope: common.scope,
1754
+ selected: diff !== "major",
1755
+ origin: { kind: "workspace", moduleId: target.id, moduleRelPath: target.relPath },
1756
+ action: "align",
1757
+ declaration: located.declaration,
1758
+ availableVersions: [local],
1759
+ isPreRelease: tag !== void 0,
1760
+ preReleaseTag: tag,
1761
+ ...common.qualifier ? { qualifier: common.qualifier } : {}
1762
+ }
1763
+ };
1764
+ }
1765
+ function groupDependencies(deps) {
1766
+ const groups = /* @__PURE__ */ new Map();
1767
+ for (const dep of deps) {
1768
+ const id = `${dep.rename ?? dep.name}`;
1769
+ groups.set(id, [...groups.get(id) ?? [], dep]);
1770
+ }
1771
+ return [...groups.values()];
1772
+ }
1773
+ function readDeclarations(manifestPath) {
1774
+ try {
1775
+ return parseCargoManifest(readFileSync10(manifestPath, "utf8"));
1776
+ } catch {
1777
+ return [];
1778
+ }
1779
+ }
1780
+ var sectionOf = (dep) => dep.kind === "dev" ? "dev-dependencies" : dep.kind === "build" ? "build-dependencies" : "dependencies";
1781
+ function findDeclaration(decls, key, dep) {
1782
+ const candidates = decls.filter((d) => !d.inWorkspace && d.key === key);
1783
+ return candidates.find((d) => d.section === sectionOf(dep) && (d.target ?? null) === dep.target) ?? candidates[0];
1784
+ }
1785
+ function locate(decl, root, key, repoRoot, manifestPath, rootManifest) {
1786
+ if (!decl) return void 0;
1787
+ if (decl.inherited) {
1788
+ const shared = root.find((d) => d.inWorkspace && d.key === key);
1789
+ if (!shared?.version) return void 0;
718
1790
  return {
719
- all: t.tabs.all(counts.total),
720
- prod: t.tabs.dependencies(counts.prodCount),
721
- dev: t.tabs.devDependencies(counts.devCount)
1791
+ version: shared.version,
1792
+ inherited: true,
1793
+ declaration: { file: rootManifest, kind: "dependency-management", displayPath: display(repoRoot, rootManifest) }
722
1794
  };
723
1795
  }
724
- };
725
-
726
- // src/adapters/maven/maven-discovery.ts
727
- import { existsSync as existsSync6 } from "fs";
728
- import { dirname as dirname5, join as join9, relative as relative3, resolve as resolve6 } from "path";
729
-
730
- // src/adapters/maven/maven-project.ts
731
- import { existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
732
- import { join as join8, resolve as resolve5 } from "path";
1796
+ if (!decl.version) return void 0;
1797
+ return {
1798
+ version: decl.version,
1799
+ inherited: false,
1800
+ declaration: { file: manifestPath, kind: "dependency", displayPath: display(repoRoot, manifestPath) }
1801
+ };
1802
+ }
1803
+ function sharersOf(metadata, self, key) {
1804
+ return metadata.packages.filter((p) => p !== self).filter((p) => readDeclarations(p.manifest_path).some((d) => d.key === key && d.inherited)).map((p) => p.name);
1805
+ }
1806
+ var display = (repoRoot, file) => relative4(repoRoot, file).replace(/\\/g, "/");
1807
+ var samePath = (a, b) => relative4(a, b) === "";
1808
+ function sameRequirement(literal, req) {
1809
+ const a = toSemverRange(literal);
1810
+ const b = toSemverRange(req);
1811
+ return a !== null && b !== null && semver8.validRange(a) === semver8.validRange(b);
1812
+ }
733
1813
 
734
- // src/adapters/maven/maven-pom.ts
735
- import { existsSync as existsSync4, readFileSync as readFileSync6, statSync } from "fs";
736
- import { dirname as dirname4, join as join7, resolve as resolve4 } from "path";
1814
+ // src/adapters/cargo/cargo-updater.ts
1815
+ import { existsSync as existsSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync2 } from "fs";
1816
+ import { join as join12, resolve as resolve6 } from "path";
1817
+ import semver9 from "semver";
737
1818
 
738
1819
  // src/adapters/shared/xml.ts
739
1820
  import { SaxesParser } from "saxes";
@@ -866,9 +1947,226 @@ function localName(qualified) {
866
1947
  return colon >= 0 ? qualified.slice(colon + 1) : qualified;
867
1948
  }
868
1949
 
1950
+ // src/adapters/shared/text-edit.ts
1951
+ function applyRangeEdits(source, edits) {
1952
+ for (const edit of edits) {
1953
+ const found = source.slice(edit.start, edit.end);
1954
+ if (found !== edit.expected) {
1955
+ throw new StaleEditError(`Found '${found}', expected '${edit.expected}'. The file changed since it was scanned.`);
1956
+ }
1957
+ }
1958
+ const ordered = [...edits].sort((a, b) => b.start - a.start);
1959
+ for (let i = 1; i < ordered.length; i++) {
1960
+ if (ordered[i].end > ordered[i - 1].start) throw new StaleEditError("Overlapping edits on the same text.");
1961
+ }
1962
+ let result = source;
1963
+ for (const edit of ordered) result = result.slice(0, edit.start) + edit.replacement + result.slice(edit.end);
1964
+ return result;
1965
+ }
1966
+
1967
+ // src/adapters/cargo/cargo-lockfile.ts
1968
+ import { existsSync as existsSync7, readFileSync as readFileSync11 } from "fs";
1969
+ import { getStaticTOMLValue as getStaticTOMLValue2, parseTOML as parseTOML3 } from "toml-eslint-parser";
1970
+ function readLockedVersions(lockFile) {
1971
+ const locked = /* @__PURE__ */ new Map();
1972
+ if (!existsSync7(lockFile)) return locked;
1973
+ try {
1974
+ const data = getStaticTOMLValue2(parseTOML3(readFileSync11(lockFile, "utf8")));
1975
+ for (const pkg of data.package ?? []) locked.set(pkg.name, [...locked.get(pkg.name) ?? [], pkg.version]);
1976
+ } catch {
1977
+ }
1978
+ return locked;
1979
+ }
1980
+
1981
+ // src/adapters/cargo/cargo-updater.ts
1982
+ var UPDATE_TIMEOUT_MS = 10 * 6e4;
1983
+ var workspaceDir = (module, rootDir) => module.aggregatorDir ?? module.path ?? rootDir;
1984
+ var CargoUpdaterAdapter = class {
1985
+ affectedFiles(module, rootDir, updates) {
1986
+ const files = new Set(updates.map((u) => resolve6(u.declaration?.file ?? join12(module.path, "Cargo.toml"))));
1987
+ files.add(join12(workspaceDir(module, rootDir), "Cargo.lock"));
1988
+ return [...files];
1989
+ }
1990
+ async applyUpdates(module, rootDir, updates, onProgress) {
1991
+ const dir = workspaceDir(module, rootDir);
1992
+ const lockFile = join12(dir, "Cargo.lock");
1993
+ const lockedBefore = readLockedVersions(lockFile);
1994
+ const byFile = /* @__PURE__ */ new Map();
1995
+ for (const update of updates) {
1996
+ const file = resolve6(update.declaration?.file ?? join12(module.path, "Cargo.toml"));
1997
+ byFile.set(file, [...byFile.get(file) ?? [], update]);
1998
+ }
1999
+ const writes = [];
2000
+ for (const [file, fileUpdates] of byFile) {
2001
+ onProgress(`Preparing ${fileUpdates.length} change(s) in ${file}\u2026`);
2002
+ const source = readFileSync12(file, "utf8");
2003
+ const decls = parseCargoManifest(source);
2004
+ const edits = [];
2005
+ for (const update of fileUpdates) {
2006
+ const inWorkspace = update.declaration?.kind === "dependency-management";
2007
+ const name = update.coordinate.identifier;
2008
+ const found = decls.filter(
2009
+ (d) => d.inWorkspace === inWorkspace && (d.package ?? d.key) === name && d.version?.value === update.currentRange
2010
+ );
2011
+ if (found.length === 0) {
2012
+ throw new InstallError(`${file}: cannot find the version of ${name} (${update.currentRange}) any more; rescan the module.`);
2013
+ }
2014
+ for (const d of found) {
2015
+ edits.push({ start: d.version.start, end: d.version.end, expected: d.version.value, replacement: update.newRange });
2016
+ }
2017
+ }
2018
+ try {
2019
+ writes.push({ file, content: applyRangeEdits(source, edits) });
2020
+ } catch (err) {
2021
+ if (err instanceof StaleEditError) throw new InstallError(`${file}: ${err.message}`);
2022
+ throw err;
2023
+ }
2024
+ }
2025
+ onProgress("Writing Cargo.toml files\u2026");
2026
+ for (const write of writes) writeFileSync2(write.file, write.content, "utf8");
2027
+ const registryUpdates = updates.filter((u) => u.action === "update");
2028
+ if (existsSync8(lockFile) && registryUpdates.length > 0) {
2029
+ await this.refreshLock(dir, lockFile, lockedBefore, registryUpdates, onProgress);
2030
+ }
2031
+ return {
2032
+ updatedCount: updates.length,
2033
+ auditMessage: `Updated ${updates.length} requirement(s) in ${writes.length} Cargo.toml file(s)${existsSync8(lockFile) ? "; Cargo.lock refreshed" : ""}.`,
2034
+ auditSeverity: "clean",
2035
+ integrityViolations: []
2036
+ };
2037
+ }
2038
+ async resync() {
2039
+ }
2040
+ async refreshLock(dir, lockFile, before, updates, onProgress) {
2041
+ const specs = [];
2042
+ for (const update of updates) {
2043
+ const name = update.coordinate.identifier;
2044
+ const versions = before.get(name) ?? [];
2045
+ const old = versions.find((v) => satisfiesRequirement(update.currentRange, v)) ?? versions[0];
2046
+ if (old) specs.push(`${name}@${old}`);
2047
+ }
2048
+ if (specs.length === 0) return;
2049
+ onProgress("Updating Cargo.lock\u2026");
2050
+ await this.cargoUpdate(dir, specs.flatMap((s) => ["-p", s]));
2051
+ const after = readLockedVersions(lockFile);
2052
+ for (const update of updates) {
2053
+ const name = update.coordinate.identifier;
2054
+ const locked = (after.get(name) ?? []).find((v) => satisfiesRequirement(update.newRange, v));
2055
+ if (locked && semver9.valid(update.latest) && locked !== update.latest) {
2056
+ onProgress(`Pinning ${name} to ${update.latest}\u2026`);
2057
+ await this.cargoUpdate(dir, ["-p", `${name}@${locked}`, "--precise", update.latest]);
2058
+ }
2059
+ }
2060
+ }
2061
+ async cargoUpdate(dir, args) {
2062
+ const result = await runCommand("cargo", ["update", ...args], { cwd: dir, timeoutMs: UPDATE_TIMEOUT_MS });
2063
+ if (result.exitCode !== 0) {
2064
+ const reason = result.timedOut ? "cargo update timed out" : "cargo update failed";
2065
+ throw new InstallError(reason, tail(result.stderr || result.stdout));
2066
+ }
2067
+ }
2068
+ };
2069
+
2070
+ // src/adapters/cargo/cargo-verifier.ts
2071
+ import { existsSync as existsSync9 } from "fs";
2072
+ import { join as join13, relative as relative5, resolve as resolve7 } from "path";
2073
+ var VERIFY_TIMEOUT_MS2 = 30 * 6e4;
2074
+ var same = (a, b) => relative5(resolve7(a), resolve7(b)) === "";
2075
+ function planCheck(metadata, module, changedFiles, locked) {
2076
+ const args = ["check", "--all-targets", ...locked ? ["--locked"] : []];
2077
+ const rootManifest = join13(metadata.workspace_root, "Cargo.toml");
2078
+ const memberManifests = new Set(metadata.packages.map((p) => resolve7(p.manifest_path)));
2079
+ if (changedFiles.some((f) => same(f, rootManifest))) {
2080
+ const isVirtualRoot = !memberManifests.has(resolve7(rootManifest));
2081
+ if (isVirtualRoot || metadata.packages.length > 1) return [...args, "--workspace"];
2082
+ }
2083
+ const changed = new Set(
2084
+ metadata.packages.filter((p) => changedFiles.some((f) => same(f, p.manifest_path))).map((p) => p.name)
2085
+ );
2086
+ if (changed.size === 0) changed.add(module.id);
2087
+ for (let grew = true; grew; ) {
2088
+ grew = false;
2089
+ for (const p of metadata.packages) {
2090
+ if (changed.has(p.name)) continue;
2091
+ if (p.dependencies.some((d) => d.source === null && d.path && changed.has(d.name))) {
2092
+ changed.add(p.name);
2093
+ grew = true;
2094
+ }
2095
+ }
2096
+ }
2097
+ const ordered = metadata.packages.filter((p) => changed.has(p.name)).map((p) => p.name);
2098
+ return [...args, ...ordered.flatMap((name) => ["-p", name])];
2099
+ }
2100
+ var CargoVerificationAdapter = class {
2101
+ constructor(deps = { loadMetadata, hasLock: (dir) => existsSync9(join13(dir, "Cargo.lock")) }) {
2102
+ this.deps = deps;
2103
+ }
2104
+ deps;
2105
+ async verify(module, rootDir, options = {}) {
2106
+ const dir = module.aggregatorDir ?? module.path ?? rootDir;
2107
+ if (options.customScript) {
2108
+ const label2 = options.customLabel || options.customScript;
2109
+ const result2 = await runShellScript(options.customScript, { cwd: dir, timeoutMs: VERIFY_TIMEOUT_MS2 });
2110
+ return result2.exitCode === 0 ? { status: "clean", details: "Verification script succeeded.", label: label2 } : { status: "warn", details: tail(result2.stderr || result2.stdout), label: label2 };
2111
+ }
2112
+ const metadata = await this.deps.loadMetadata(dir);
2113
+ const args = planCheck(metadata, module, options.changedFiles ?? [], this.deps.hasLock(dir));
2114
+ const label = `Cargo build (cargo ${args.join(" ")})`;
2115
+ const result = await runCargo(args, { cwd: dir, timeoutMs: VERIFY_TIMEOUT_MS2 });
2116
+ if (result.exitCode === 0) return { status: "clean", details: "All affected crates type-check, including tests.", label };
2117
+ return {
2118
+ status: "warn",
2119
+ details: result.timedOut ? "cargo check timed out." : cargoErrors(`${result.stderr}
2120
+ ${result.stdout}`),
2121
+ label
2122
+ };
2123
+ }
2124
+ };
2125
+
2126
+ // src/adapters/cargo/cargo-strategy.ts
2127
+ var cargoVersionPolicy = {
2128
+ normalize: (v) => semver10.valid(v),
2129
+ rewrite: rewriteRequirement,
2130
+ diff: (current, target) => semver10.valid(current) && semver10.valid(target) ? semver10.eq(current, target) ? "patch" : cargoDiff(current, target) : "minor",
2131
+ // Updates are written where the scan found the declaration.
2132
+ allowsManualTargets: false
2133
+ };
2134
+ var CargoEcosystemStrategy = class {
2135
+ ecosystem = "cargo";
2136
+ displayName = "Rust / Cargo";
2137
+ icon = "\u{1F980}";
2138
+ manifestFile = "Cargo.toml";
2139
+ versioning = cargoVersionPolicy;
2140
+ config = new CargoConfigReader();
2141
+ index = new CargoIndexClient();
2142
+ endpoint = (registry, cwd) => this.config.resolve(registry, cwd);
2143
+ discovery = new CargoModuleDiscoveryAdapter();
2144
+ registry = new CargoRegistryAdapter({ index: this.index, endpoint: this.endpoint });
2145
+ reader = new CargoDependencyAdapter({ loadMetadata, index: this.index, endpoint: this.endpoint });
2146
+ updater = new CargoUpdaterAdapter();
2147
+ verifier = new CargoVerificationAdapter();
2148
+ getTabLabels(counts, t) {
2149
+ return {
2150
+ all: t.tabs.all(counts.total),
2151
+ prod: t.tabs.dependencies(counts.prodCount),
2152
+ dev: t.tabs.devDependencies(counts.devCount)
2153
+ };
2154
+ }
2155
+ };
2156
+
2157
+ // src/adapters/maven/maven-discovery.ts
2158
+ import { existsSync as existsSync12 } from "fs";
2159
+ import { dirname as dirname7, join as join16, relative as relative6, resolve as resolve10 } from "path";
2160
+
2161
+ // src/adapters/maven/maven-project.ts
2162
+ import { existsSync as existsSync11, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
2163
+ import { join as join15, resolve as resolve9 } from "path";
2164
+
869
2165
  // src/adapters/maven/maven-pom.ts
2166
+ import { existsSync as existsSync10, readFileSync as readFileSync13, statSync } from "fs";
2167
+ import { dirname as dirname6, join as join14, resolve as resolve8 } from "path";
870
2168
  function readPom(file) {
871
- const source = readFileSync6(file, "utf8");
2169
+ const source = readFileSync13(file, "utf8");
872
2170
  return parsePom(source, file);
873
2171
  }
874
2172
  function parsePom(source, file) {
@@ -887,7 +2185,7 @@ function parsePom(source, file) {
887
2185
  }
888
2186
  return {
889
2187
  file,
890
- dir: dirname4(file),
2188
+ dir: dirname6(file),
891
2189
  groupId: childText(project, "groupId"),
892
2190
  artifactId: childText(project, "artifactId") ?? "unknown",
893
2191
  version: childText(project, "version"),
@@ -916,9 +2214,9 @@ function parentPomFile(pom) {
916
2214
  if (!pom.parent) return void 0;
917
2215
  const relativePath = pom.parent.relativePath ?? "../pom.xml";
918
2216
  if (relativePath === "") return void 0;
919
- let candidate = resolve4(pom.dir, relativePath);
920
- if (existsSync4(candidate) && statSync(candidate).isDirectory()) candidate = join7(candidate, "pom.xml");
921
- return existsSync4(candidate) ? candidate : void 0;
2217
+ let candidate = resolve8(pom.dir, relativePath);
2218
+ if (existsSync10(candidate) && statSync(candidate).isDirectory()) candidate = join14(candidate, "pom.xml");
2219
+ return existsSync10(candidate) ? candidate : void 0;
922
2220
  }
923
2221
  function builtinProperty(name, module) {
924
2222
  switch (name) {
@@ -993,9 +2291,9 @@ var MavenProject = class _MavenProject {
993
2291
  }
994
2292
  poms.push(pom);
995
2293
  for (const entry of pom.modules) {
996
- const target = resolve5(pom.dir, entry);
997
- const moduleFile = target.endsWith(".xml") ? target : join8(target, "pom.xml");
998
- if (!seen.has(moduleFile) && existsSync5(moduleFile)) {
2294
+ const target = resolve9(pom.dir, entry);
2295
+ const moduleFile = target.endsWith(".xml") ? target : join15(target, "pom.xml");
2296
+ if (!seen.has(moduleFile) && existsSync11(moduleFile)) {
999
2297
  seen.add(moduleFile);
1000
2298
  queue.push(moduleFile);
1001
2299
  }
@@ -1004,7 +2302,7 @@ var MavenProject = class _MavenProject {
1004
2302
  return new _MavenProject(rootDir, poms);
1005
2303
  }
1006
2304
  pomAt(file) {
1007
- return this.byFile.get(resolve5(file));
2305
+ return this.byFile.get(resolve9(file));
1008
2306
  }
1009
2307
  /** The project followed by its local parents, stopping at the first external parent. */
1010
2308
  chainOf(pom) {
@@ -1047,7 +2345,7 @@ var MavenProject = class _MavenProject {
1047
2345
  return groupId === parent.groupId && candidate.artifactId === parent.artifactId && (version.includes("${") || expected.includes("${") || version === expected);
1048
2346
  };
1049
2347
  const file = parentPomFile(pom);
1050
- const byPath = file ? this.byFile.get(resolve5(file)) : void 0;
2348
+ const byPath = file ? this.byFile.get(resolve9(file)) : void 0;
1051
2349
  return matches(byPath) ? byPath : void 0;
1052
2350
  }
1053
2351
  /**
@@ -1064,8 +2362,8 @@ var MavenProject = class _MavenProject {
1064
2362
  if (this.aggregators.has(pom.file)) continue;
1065
2363
  this.aggregators.set(pom.file, aggregator.dir);
1066
2364
  for (const entry of pom.modules) {
1067
- const target = resolve5(pom.dir, entry);
1068
- const file = target.endsWith(".xml") ? target : join8(target, "pom.xml");
2365
+ const target = resolve9(pom.dir, entry);
2366
+ const file = target.endsWith(".xml") ? target : join15(target, "pom.xml");
1069
2367
  const modulePom = this.byFile.get(file);
1070
2368
  if (modulePom) queue.push(modulePom);
1071
2369
  }
@@ -1075,23 +2373,23 @@ var MavenProject = class _MavenProject {
1075
2373
  };
1076
2374
  function findPomFiles(rootDir) {
1077
2375
  const results = [];
1078
- const rootPom = join8(rootDir, "pom.xml");
1079
- if (existsSync5(rootPom)) results.push(resolve5(rootPom));
2376
+ const rootPom = join15(rootDir, "pom.xml");
2377
+ if (existsSync11(rootPom)) results.push(resolve9(rootPom));
1080
2378
  const walk = (dir, depth) => {
1081
2379
  if (depth > MAX_DEPTH) return;
1082
- let entries;
2380
+ let entries2;
1083
2381
  try {
1084
- entries = readdirSync(dir, { withFileTypes: true });
2382
+ entries2 = readdirSync2(dir, { withFileTypes: true });
1085
2383
  } catch {
1086
2384
  return;
1087
2385
  }
1088
- const isProject = existsSync5(join8(dir, "pom.xml"));
1089
- for (const entry of entries) {
2386
+ const isProject = existsSync11(join15(dir, "pom.xml"));
2387
+ for (const entry of entries2) {
1090
2388
  if (!entry.isDirectory() || entry.name.startsWith(".") || SKIPPED_DIRS.has(entry.name)) continue;
1091
2389
  if (isProject && entry.name === "src") continue;
1092
- const subDir = join8(dir, entry.name);
1093
- const pom = join8(subDir, "pom.xml");
1094
- if (existsSync5(pom) && statSync2(pom).isFile()) results.push(resolve5(pom));
2390
+ const subDir = join15(dir, entry.name);
2391
+ const pom = join15(subDir, "pom.xml");
2392
+ if (existsSync11(pom) && statSync2(pom).isFile()) results.push(resolve9(pom));
1095
2393
  walk(subDir, depth + 1);
1096
2394
  }
1097
2395
  };
@@ -1106,9 +2404,9 @@ var MavenModuleDiscoveryAdapter = class {
1106
2404
  * the POM above until there is none: the top of the connected POM chain.
1107
2405
  */
1108
2406
  findRoot(startDir, boundaryDir) {
1109
- let dir = resolve6(startDir);
1110
- if (!existsSync6(join9(dir, "pom.xml"))) return void 0;
1111
- const boundary = boundaryDir ? resolve6(boundaryDir) : void 0;
2407
+ let dir = resolve10(startDir);
2408
+ if (!existsSync12(join16(dir, "pom.xml"))) return void 0;
2409
+ const boundary = boundaryDir ? resolve10(boundaryDir) : void 0;
1112
2410
  for (; ; ) {
1113
2411
  const above = pomAbove(dir, boundary);
1114
2412
  if (!above) return dir;
@@ -1119,7 +2417,7 @@ var MavenModuleDiscoveryAdapter = class {
1119
2417
  const project = MavenProject.load(rootDir);
1120
2418
  const modules = project.poms.map((pom) => {
1121
2419
  const coordinates = project.coordinatesOf(pom);
1122
- const relPath = relative3(rootDir, pom.dir).replace(/\\/g, "/");
2420
+ const relPath = relative6(rootDir, pom.dir).replace(/\\/g, "/");
1123
2421
  return {
1124
2422
  id: coordinates.groupId ? `${coordinates.groupId}:${coordinates.artifactId}` : coordinates.artifactId,
1125
2423
  name: pom.name ? interpolate(pom.name, project.chainOf(pom).poms) : pom.artifactId,
@@ -1146,7 +2444,7 @@ function ancestorsWithin(dir, boundary) {
1146
2444
  const result = [];
1147
2445
  let current = dir;
1148
2446
  while (current !== boundary) {
1149
- const parent = dirname5(current);
2447
+ const parent = dirname7(current);
1150
2448
  if (parent === current) break;
1151
2449
  result.push(parent);
1152
2450
  current = parent;
@@ -1154,22 +2452,22 @@ function ancestorsWithin(dir, boundary) {
1154
2452
  return result;
1155
2453
  }
1156
2454
  function pomAbove(dir, boundary) {
1157
- const pom = tryReadPom(join9(dir, "pom.xml"));
2455
+ const pom = tryReadPom(join16(dir, "pom.xml"));
1158
2456
  if (!pom) return void 0;
1159
2457
  const ancestors = ancestorsWithin(dir, boundary);
1160
2458
  const found = [];
1161
2459
  const parentFile = parentPomFile(pom);
1162
- if (parentFile && pom.parent && ancestors.includes(dirname5(parentFile))) {
2460
+ if (parentFile && pom.parent && ancestors.includes(dirname7(parentFile))) {
1163
2461
  const parent = tryReadPom(parentFile);
1164
2462
  const groupId = parent?.groupId ?? parent?.parent?.groupId;
1165
2463
  if (parent && parent.artifactId === pom.parent.artifactId && groupId === pom.parent.groupId) {
1166
- found.push(dirname5(parentFile));
2464
+ found.push(dirname7(parentFile));
1167
2465
  }
1168
2466
  }
1169
- const ownFile = join9(dir, "pom.xml");
2467
+ const ownFile = join16(dir, "pom.xml");
1170
2468
  const aggregator = ancestors.find(
1171
- (ancestor) => tryReadPom(join9(ancestor, "pom.xml"))?.modules.some((entry) => {
1172
- const target = resolve6(ancestor, entry);
2469
+ (ancestor) => tryReadPom(join16(ancestor, "pom.xml"))?.modules.some((entry) => {
2470
+ const target = resolve10(ancestor, entry);
1173
2471
  return target === dir || target === ownFile;
1174
2472
  })
1175
2473
  );
@@ -1178,12 +2476,12 @@ function pomAbove(dir, boundary) {
1178
2476
  }
1179
2477
 
1180
2478
  // src/adapters/maven/maven-registry.ts
1181
- import { existsSync as existsSync9 } from "fs";
1182
- import { join as join12 } from "path";
2479
+ import { existsSync as existsSync15 } from "fs";
2480
+ import { join as join19 } from "path";
1183
2481
 
1184
2482
  // src/adapters/maven/maven-command.ts
1185
- import { existsSync as existsSync7 } from "fs";
1186
- import { dirname as dirname6, isAbsolute, join as join10, relative as relative4, resolve as resolve7, sep } from "path";
2483
+ import { existsSync as existsSync13 } from "fs";
2484
+ import { dirname as dirname8, isAbsolute as isAbsolute2, join as join17, relative as relative7, resolve as resolve11, sep } from "path";
1187
2485
  var MavenUnavailableError = class extends Error {
1188
2486
  constructor() {
1189
2487
  super("Maven is not available: no Maven Wrapper (mvnw) in the project and no `mvn` on the PATH.");
@@ -1203,27 +2501,27 @@ var WRAPPER = IS_WINDOWS2 ? "mvnw.cmd" : "mvnw";
1203
2501
  var BATCH_ARGS = ["-B", "--no-transfer-progress"];
1204
2502
  var mvnOnPath;
1205
2503
  function findMavenWrapper(fromDir, rootDir) {
1206
- let dir = resolve7(fromDir);
1207
- const root = resolve7(rootDir);
2504
+ let dir = resolve11(fromDir);
2505
+ const root = resolve11(rootDir);
1208
2506
  for (; ; ) {
1209
- const candidate = join10(dir, WRAPPER);
1210
- if (existsSync7(candidate)) return candidate;
1211
- if (dir === root || relative4(root, dir).startsWith("..")) return void 0;
1212
- const parent = dirname6(dir);
2507
+ const candidate = join17(dir, WRAPPER);
2508
+ if (existsSync13(candidate)) return candidate;
2509
+ if (dir === root || relative7(root, dir).startsWith("..")) return void 0;
2510
+ const parent = dirname8(dir);
1213
2511
  if (parent === dir) return void 0;
1214
2512
  dir = parent;
1215
2513
  }
1216
2514
  }
1217
2515
  function relativeToCwd(cwd, file) {
1218
- const path = relative4(cwd, file);
1219
- return isAbsolute(path) ? file : path || ".";
2516
+ const path = relative7(cwd, file);
2517
+ return isAbsolute2(path) ? file : path || ".";
1220
2518
  }
1221
2519
  async function runMaven(args, options) {
1222
2520
  const wrapper = findMavenWrapper(options.cwd, options.rootDir);
1223
2521
  const run = { cwd: options.cwd, timeoutMs: options.timeoutMs };
1224
2522
  if (wrapper) {
1225
2523
  const relativeWrapper = relativeToCwd(options.cwd, wrapper);
1226
- const path = isAbsolute(relativeWrapper) || relativeWrapper.includes(sep) ? relativeWrapper : `.${sep}${relativeWrapper}`;
2524
+ const path = isAbsolute2(relativeWrapper) || relativeWrapper.includes(sep) ? relativeWrapper : `.${sep}${relativeWrapper}`;
1227
2525
  return IS_WINDOWS2 ? runCommand(path, [...BATCH_ARGS, ...args], run) : runCommand("sh", [path, ...BATCH_ARGS, ...args], run);
1228
2526
  }
1229
2527
  mvnOnPath ??= runCommand("mvn", ["-v"], { cwd: options.cwd, timeoutMs: 3e4 }).then(
@@ -1240,8 +2538,8 @@ function mavenErrors(output) {
1240
2538
 
1241
2539
  // src/adapters/maven/maven-resolution.ts
1242
2540
  import { randomUUID } from "crypto";
1243
- import { existsSync as existsSync8, mkdirSync, readFileSync as readFileSync7, readdirSync as readdirSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
1244
- import { join as join11 } from "path";
2541
+ import { existsSync as existsSync14, mkdirSync, readFileSync as readFileSync14, readdirSync as readdirSync3, rmSync, writeFileSync as writeFileSync3 } from "fs";
2542
+ import { join as join18 } from "path";
1245
2543
  var DEFAULT_MAVEN_PLUGINS = { help: "3.5.2", versions: "2.22.0" };
1246
2544
  var HELP_PLUGIN_ID = "org.apache.maven.plugins:maven-help-plugin";
1247
2545
  var VERSIONS_PLUGIN_ID = "org.codehaus.mojo:versions-maven-plugin";
@@ -1273,21 +2571,21 @@ var ScratchDirectory = class {
1273
2571
  target;
1274
2572
  createdTarget;
1275
2573
  constructor(moduleDir) {
1276
- this.target = join11(moduleDir, "target");
1277
- this.createdTarget = !existsSync8(this.target);
1278
- this.path = join11(this.target, `.elevate-${randomUUID().slice(0, 8)}`);
2574
+ this.target = join18(moduleDir, "target");
2575
+ this.createdTarget = !existsSync14(this.target);
2576
+ this.path = join18(this.target, `.elevate-${randomUUID().slice(0, 8)}`);
1279
2577
  mkdirSync(this.path, { recursive: true });
1280
2578
  }
1281
2579
  dispose() {
1282
2580
  rmSync(this.path, { recursive: true, force: true });
1283
2581
  try {
1284
- if (this.createdTarget && readdirSync2(this.target).length === 0) rmSync(this.target, { recursive: true });
2582
+ if (this.createdTarget && readdirSync3(this.target).length === 0) rmSync(this.target, { recursive: true });
1285
2583
  } catch {
1286
2584
  }
1287
2585
  }
1288
2586
  };
1289
2587
  async function readEffectivePoms(pomFile, moduleDir, rootDir, scratch, plugins, recursive = false) {
1290
- const output = join11(scratch.path, "effective-pom.xml");
2588
+ const output = join18(scratch.path, "effective-pom.xml");
1291
2589
  const args = [
1292
2590
  "-f",
1293
2591
  relativeToCwd(moduleDir, pomFile),
@@ -1299,7 +2597,7 @@ async function readEffectivePoms(pomFile, moduleDir, rootDir, scratch, plugins,
1299
2597
  rootDir,
1300
2598
  timeoutMs: MAVEN_TIMEOUT_MS
1301
2599
  });
1302
- if (result.exitCode !== 0 || !existsSync8(output)) {
2600
+ if (result.exitCode !== 0 || !existsSync14(output)) {
1303
2601
  const text = `${result.stdout}
1304
2602
  ${result.stderr}`;
1305
2603
  if (isPluginUnavailable(text, HELP_PLUGIN_ID)) {
@@ -1311,7 +2609,7 @@ ${result.stderr}`;
1311
2609
  throw new MavenCommandError(`Maven could not build the effective POM:
1312
2610
  ${mavenErrors(text)}`, text);
1313
2611
  }
1314
- return parseEffectivePoms(readFileSync7(output, "utf8"));
2612
+ return parseEffectivePoms(readFileSync14(output, "utf8"));
1315
2613
  }
1316
2614
  function parseEffectivePoms(source) {
1317
2615
  const root = parseXml(source);
@@ -1323,7 +2621,7 @@ function parseEffectivePoms(source) {
1323
2621
  return models;
1324
2622
  }
1325
2623
  function mergeRepositories(models) {
1326
- const merge = (name, pick) => {
2624
+ const merge2 = (name, pick) => {
1327
2625
  const byId = /* @__PURE__ */ new Map();
1328
2626
  for (const element of models.map(pick)) {
1329
2627
  for (const repository of element?.children ?? []) {
@@ -1336,8 +2634,8 @@ function mergeRepositories(models) {
1336
2634
  return {
1337
2635
  dependencies: [],
1338
2636
  managedDependencies: [],
1339
- repositories: merge("repositories", (m) => m.repositories),
1340
- pluginRepositories: merge("pluginRepositories", (m) => m.pluginRepositories)
2637
+ repositories: merge2("repositories", (m) => m.repositories),
2638
+ pluginRepositories: merge2("pluginRepositories", (m) => m.pluginRepositories)
1341
2639
  };
1342
2640
  }
1343
2641
  function toEffectiveModel(project) {
@@ -1360,7 +2658,7 @@ function readEffectiveDependencies(container) {
1360
2658
  }
1361
2659
  function buildProbePom(artifacts, model) {
1362
2660
  const seen = /* @__PURE__ */ new Set();
1363
- const entries = [];
2661
+ const entries2 = [];
1364
2662
  for (const artifact of artifacts) {
1365
2663
  const key = `${artifact.groupId}:${artifact.artifactId}:${artifact.type ?? "jar"}:${artifact.classifier ?? ""}`;
1366
2664
  if (seen.has(key)) continue;
@@ -1372,7 +2670,7 @@ function buildProbePom(artifacts, model) {
1372
2670
  artifact.type && artifact.type !== "jar" ? `<type>${escapeXml(artifact.type)}</type>` : "",
1373
2671
  artifact.classifier ? `<classifier>${escapeXml(artifact.classifier)}</classifier>` : ""
1374
2672
  ].filter(Boolean);
1375
- entries.push(` <dependency>${fields.join("")}</dependency>`);
2673
+ entries2.push(` <dependency>${fields.join("")}</dependency>`);
1376
2674
  }
1377
2675
  const repositories = [model.repositories, model.pluginRepositories].filter((element) => element !== void 0).map((element) => serializeXml(element, " ")).join("\n");
1378
2676
  return [
@@ -1386,7 +2684,7 @@ function buildProbePom(artifacts, model) {
1386
2684
  repositories,
1387
2685
  " <dependencyManagement>",
1388
2686
  " <dependencies>",
1389
- ...entries,
2687
+ ...entries2,
1390
2688
  " </dependencies>",
1391
2689
  " </dependencyManagement>",
1392
2690
  "</project>",
@@ -1395,10 +2693,10 @@ function buildProbePom(artifacts, model) {
1395
2693
  }
1396
2694
  async function queryNewerVersions(artifacts, model, moduleDir, rootDir, scratch, plugins) {
1397
2695
  if (artifacts.length === 0) return { newerVersions: /* @__PURE__ */ new Map(), failedLookups: /* @__PURE__ */ new Set() };
1398
- const probeDir = join11(scratch.path, "probe");
2696
+ const probeDir = join18(scratch.path, "probe");
1399
2697
  mkdirSync(probeDir, { recursive: true });
1400
- const probePom = join11(probeDir, "pom.xml");
1401
- writeFileSync2(probePom, buildProbePom(artifacts, model), "utf8");
2698
+ const probePom = join18(probeDir, "pom.xml");
2699
+ writeFileSync3(probePom, buildProbePom(artifacts, model), "utf8");
1402
2700
  const result = await runMaven(
1403
2701
  [
1404
2702
  "-N",
@@ -1412,8 +2710,8 @@ async function queryNewerVersions(artifacts, model, moduleDir, rootDir, scratch,
1412
2710
  );
1413
2711
  const log = `${result.stdout}
1414
2712
  ${result.stderr}`;
1415
- const reportFile = join11(probeDir, "target", "dependency-updates-report.xml");
1416
- if (result.exitCode !== 0 || !existsSync8(reportFile)) {
2713
+ const reportFile = join18(probeDir, "target", "dependency-updates-report.xml");
2714
+ if (result.exitCode !== 0 || !existsSync14(reportFile)) {
1417
2715
  if (isPluginUnavailable(log, VERSIONS_PLUGIN_ID)) {
1418
2716
  throw new MavenPluginUnavailableError(
1419
2717
  { id: VERSIONS_PLUGIN_ID, version: plugins.versions, key: "versions", purpose: "look up newer versions" },
@@ -1424,7 +2722,7 @@ ${result.stderr}`;
1424
2722
  ${mavenErrors(log)}`, log);
1425
2723
  }
1426
2724
  return {
1427
- newerVersions: parseUpdatesReport(readFileSync7(reportFile, "utf8")),
2725
+ newerVersions: parseUpdatesReport(readFileSync14(reportFile, "utf8")),
1428
2726
  failedLookups: parseFailedLookups(log)
1429
2727
  };
1430
2728
  }
@@ -1509,18 +2807,18 @@ var MavenRegistryAdapter = class {
1509
2807
  }
1510
2808
  };
1511
2809
  async function projectRepositories(cwd, scratch, plugins) {
1512
- const rootPom = join12(cwd, "pom.xml");
1513
- if (!existsSync9(rootPom)) return NO_REPOSITORIES;
2810
+ const rootPom = join19(cwd, "pom.xml");
2811
+ if (!existsSync15(rootPom)) return NO_REPOSITORIES;
1514
2812
  const models = await readEffectivePoms(rootPom, cwd, cwd, scratch, plugins);
1515
2813
  return [...models.values()][0] ?? NO_REPOSITORIES;
1516
2814
  }
1517
2815
 
1518
2816
  // src/adapters/maven/maven-scanner.ts
1519
- import { join as join13 } from "path";
1520
- import semver4 from "semver";
2817
+ import { join as join20 } from "path";
2818
+ import semver11 from "semver";
1521
2819
 
1522
2820
  // src/adapters/maven/maven-locator.ts
1523
- import { relative as relative5 } from "path";
2821
+ import { relative as relative8 } from "path";
1524
2822
  function locateDependencyVersion(chain, target, rootDir) {
1525
2823
  for (const pom of chain) {
1526
2824
  const dependency = findDependency(pom.dependencies, target, chain);
@@ -1601,13 +2899,13 @@ function resolveLiteral(pom, kind, versionElement, chain, target, rootDir) {
1601
2899
  file,
1602
2900
  kind: propertyName ? "property" : kind,
1603
2901
  propertyName,
1604
- displayPath: relative5(rootDir, file).replace(/\\/g, "/")
2902
+ displayPath: relative8(rootDir, file).replace(/\\/g, "/")
1605
2903
  };
1606
2904
  return { found: true, declaration, element, value };
1607
2905
  }
1608
2906
 
1609
2907
  // src/adapters/maven/maven-scanner.ts
1610
- var DIFF_ORDER2 = { major: 0, minor: 1, patch: 2 };
2908
+ var DIFF_ORDER3 = { major: 0, minor: 1, patch: 2 };
1611
2909
  var MavenDependencyAdapter = class {
1612
2910
  constructor(plugins = DEFAULT_MAVEN_PLUGINS) {
1613
2911
  this.plugins = plugins;
@@ -1661,14 +2959,14 @@ function groupModules(project, modules) {
1661
2959
  if (module.aggregatorDir) reactorSizes.set(module.aggregatorDir, (reactorSizes.get(module.aggregatorDir) ?? 0) + 1);
1662
2960
  }
1663
2961
  for (const module of modules) {
1664
- const pom = project.pomAt(join13(module.path, "pom.xml"));
2962
+ const pom = project.pomAt(join20(module.path, "pom.xml"));
1665
2963
  if (!pom) continue;
1666
2964
  const reactor = module.aggregatorDir;
1667
2965
  const batched = reactor !== void 0 && (reactorSizes.get(reactor) ?? 0) > 1;
1668
2966
  const key = batched ? `reactor:${reactor}` : `module:${module.path}`;
1669
2967
  const group = groups.get(key) ?? {
1670
2968
  dir: batched ? reactor : module.path,
1671
- aggregatorPom: batched ? join13(reactor, "pom.xml") : void 0,
2969
+ aggregatorPom: batched ? join20(reactor, "pom.xml") : void 0,
1672
2970
  members: []
1673
2971
  };
1674
2972
  group.members.push({ module, pom });
@@ -1757,7 +3055,7 @@ function finishModule(plan, report, channel) {
1757
3055
  collect2(updateCandidate(subject, latest, newer, origin), candidates, skipped);
1758
3056
  }
1759
3057
  markSharedDeclarations(candidates);
1760
- candidates.sort((a, b) => (DIFF_ORDER2[a.diff] ?? 3) - (DIFF_ORDER2[b.diff] ?? 3));
3058
+ candidates.sort((a, b) => (DIFF_ORDER3[a.diff] ?? 3) - (DIFF_ORDER3[b.diff] ?? 3));
1761
3059
  return { candidates, skipped };
1762
3060
  }
1763
3061
  function newerThan(current, ascending) {
@@ -1788,7 +3086,7 @@ function buildCandidate(subject, target, origin, action) {
1788
3086
  }
1789
3087
  const currentClean = cleanJavaVersion(subject.version) ?? "0.0.0";
1790
3088
  const targetClean = cleanJavaVersion(target) ?? currentClean;
1791
- const diff = semver4.eq(currentClean, targetClean) ? "patch" : diffVersions(currentClean, targetClean);
3089
+ const diff = semver11.eq(currentClean, targetClean) ? "patch" : diffVersions(currentClean, targetClean);
1792
3090
  const isPreRelease = isPreReleaseVersion(target);
1793
3091
  return {
1794
3092
  candidate: {
@@ -1836,11 +3134,11 @@ function markSharedDeclarations(candidates) {
1836
3134
  }
1837
3135
 
1838
3136
  // src/adapters/maven/maven-updater.ts
1839
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "fs";
1840
- import { join as join14, resolve as resolve8 } from "path";
3137
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync4 } from "fs";
3138
+ import { join as join21, resolve as resolve12 } from "path";
1841
3139
  var MavenUpdaterAdapter = class {
1842
3140
  affectedFiles(module, _rootDir, updates) {
1843
- const files = new Set(updates.map((u) => resolve8(u.declaration?.file ?? join14(module.path, "pom.xml"))));
3141
+ const files = new Set(updates.map((u) => resolve12(u.declaration?.file ?? join21(module.path, "pom.xml"))));
1844
3142
  return [...files];
1845
3143
  }
1846
3144
  async applyUpdates(module, rootDir, updates, onProgress) {
@@ -1850,13 +3148,13 @@ var MavenUpdaterAdapter = class {
1850
3148
  if (!update.declaration) {
1851
3149
  throw new InstallError(`No version declaration known for ${update.coordinate.identifier}; rescan the module.`);
1852
3150
  }
1853
- const file = resolve8(update.declaration.file);
3151
+ const file = resolve12(update.declaration.file);
1854
3152
  byFile.set(file, [...byFile.get(file) ?? [], update]);
1855
3153
  }
1856
3154
  const writes = [];
1857
3155
  for (const [file, fileUpdates] of byFile) {
1858
3156
  onProgress(`Preparing ${fileUpdates.length} change(s) in ${fileUpdates[0].declaration.displayPath}\u2026`);
1859
- const source = readFileSync8(file, "utf8");
3157
+ const source = readFileSync15(file, "utf8");
1860
3158
  const pom = parsePom(source, file);
1861
3159
  const chain = project.pomAt(file) ? project.chainOf(project.pomAt(file)).poms : [pom];
1862
3160
  const edits = collectEdits(pom, [pom, ...chain.slice(1)], fileUpdates);
@@ -1868,7 +3166,7 @@ var MavenUpdaterAdapter = class {
1868
3166
  }
1869
3167
  }
1870
3168
  onProgress("Writing POM files\u2026");
1871
- for (const write of writes) writeFileSync3(write.file, write.content, "utf8");
3169
+ for (const write of writes) writeFileSync4(write.file, write.content, "utf8");
1872
3170
  return {
1873
3171
  updatedCount: updates.length,
1874
3172
  auditMessage: `Updated ${updates.length} version declaration(s) in ${writes.length} POM file(s).`,
@@ -1926,14 +3224,14 @@ function describe(declaration) {
1926
3224
  }
1927
3225
 
1928
3226
  // src/adapters/maven/maven-verifier.ts
1929
- import { dirname as dirname7, join as join15, relative as relative6, resolve as resolve9 } from "path";
1930
- var VERIFY_TIMEOUT_MS2 = 30 * 6e4;
3227
+ import { dirname as dirname9, join as join22, relative as relative9, resolve as resolve13 } from "path";
3228
+ var VERIFY_TIMEOUT_MS3 = 30 * 6e4;
1931
3229
  var MavenVerificationAdapter = class {
1932
3230
  async verify(module, rootDir, options = {}) {
1933
3231
  if (options.customScript) {
1934
3232
  const label2 = options.customLabel || options.customScript;
1935
- const result = await runShellScript(options.customScript, { cwd: module.path, timeoutMs: VERIFY_TIMEOUT_MS2 });
1936
- return result.exitCode === 0 ? { status: "clean", details: "Verification script succeeded.", label: label2 } : { status: "warn", details: failureDetails(result), label: label2 };
3233
+ const result = await runShellScript(options.customScript, { cwd: module.path, timeoutMs: VERIFY_TIMEOUT_MS3 });
3234
+ return result.exitCode === 0 ? { status: "clean", details: "Verification script succeeded.", label: label2 } : { status: "warn", details: failureDetails2(result), label: label2 };
1937
3235
  }
1938
3236
  const plan = planBuild(module, rootDir, options.changedFiles ?? []);
1939
3237
  const label = `Maven build (${["test-compile", ...plan.args].join(" ")})`;
@@ -1941,12 +3239,12 @@ var MavenVerificationAdapter = class {
1941
3239
  const result = await runMaven(["-q", "test-compile", ...plan.args], {
1942
3240
  cwd: plan.cwd,
1943
3241
  rootDir,
1944
- timeoutMs: VERIFY_TIMEOUT_MS2
3242
+ timeoutMs: VERIFY_TIMEOUT_MS3
1945
3243
  });
1946
3244
  if (result.exitCode === 0) {
1947
3245
  return { status: "clean", details: "All affected modules compile, including tests.", label };
1948
3246
  }
1949
- const details = result.timedOut ? "Maven build timed out." : failureDetails(result);
3247
+ const details = result.timedOut ? "Maven build timed out." : failureDetails2(result);
1950
3248
  return { status: "warn", details, label };
1951
3249
  } catch (err) {
1952
3250
  if (err instanceof MavenUnavailableError) return { status: "warn", details: err.message, label };
@@ -1954,7 +3252,7 @@ var MavenVerificationAdapter = class {
1954
3252
  }
1955
3253
  }
1956
3254
  };
1957
- function failureDetails(result) {
3255
+ function failureDetails2(result) {
1958
3256
  const output = `${result.stdout}
1959
3257
  ${result.stderr}`.trim();
1960
3258
  return output ? mavenErrors(output) : `Exited with code ${result.exitCode}.`;
@@ -1963,15 +3261,15 @@ function planBuild(module, rootDir, changedFiles) {
1963
3261
  const aggregatorDir = module.aggregatorDir;
1964
3262
  if (!aggregatorDir) return { cwd: module.path, args: [] };
1965
3263
  const project = MavenProject.load(rootDir);
1966
- const files = changedFiles.length > 0 ? changedFiles : [join15(module.path, "pom.xml")];
3264
+ const files = changedFiles.length > 0 ? changedFiles : [join22(module.path, "pom.xml")];
1967
3265
  const selected = /* @__PURE__ */ new Set();
1968
3266
  for (const file of files) {
1969
3267
  const pom = project.pomAt(file);
1970
- const dir = resolve9(dirname7(file));
1971
- if (!pom || project.aggregatorDirOf(pom) !== aggregatorDir || dir === resolve9(aggregatorDir)) {
3268
+ const dir = resolve13(dirname9(file));
3269
+ if (!pom || project.aggregatorDirOf(pom) !== aggregatorDir || dir === resolve13(aggregatorDir)) {
1972
3270
  return { cwd: aggregatorDir, args: [] };
1973
3271
  }
1974
- selected.add(relative6(aggregatorDir, dir).replace(/\\/g, "/"));
3272
+ selected.add(relative9(aggregatorDir, dir).replace(/\\/g, "/"));
1975
3273
  }
1976
3274
  return { cwd: aggregatorDir, args: ["-pl", [...selected].join(","), "-amd"] };
1977
3275
  }
@@ -1982,6 +3280,7 @@ var MavenEcosystemStrategy = class {
1982
3280
  displayName = "Java / Maven";
1983
3281
  icon = "\u2615";
1984
3282
  manifestFile = "pom.xml";
3283
+ versioning = mavenVersionPolicy;
1985
3284
  discovery = new MavenModuleDiscoveryAdapter();
1986
3285
  registry;
1987
3286
  reader;
@@ -2019,6 +3318,8 @@ var EcosystemFactory = class {
2019
3318
  if (!strategy) {
2020
3319
  if (ecosystem === "maven") {
2021
3320
  strategy = new MavenEcosystemStrategy(this.settings.mavenPlugins);
3321
+ } else if (ecosystem === "cargo") {
3322
+ strategy = new CargoEcosystemStrategy();
2022
3323
  } else {
2023
3324
  strategy = new NpmEcosystemStrategy();
2024
3325
  }
@@ -2028,18 +3329,23 @@ var EcosystemFactory = class {
2028
3329
  }
2029
3330
  /** All supported ecosystems. */
2030
3331
  static getAvailableEcosystems() {
2031
- return ["npm", "maven"];
3332
+ return ["npm", "maven", "cargo"];
3333
+ }
3334
+ /** The ecosystem after `current`, for the dashboard's ecosystem switch. */
3335
+ static next(current) {
3336
+ const all = this.getAvailableEcosystems();
3337
+ return all[(all.indexOf(current) + 1) % all.length];
2032
3338
  }
2033
3339
  };
2034
3340
 
2035
3341
  // src/config.ts
2036
3342
  function findRepositoryRoot(startDir) {
2037
- const start = resolve10(startDir);
3343
+ const start = resolve14(startDir);
2038
3344
  const boundary = findCheckoutBoundary(start);
2039
3345
  let dir = start;
2040
3346
  for (; ; ) {
2041
- if (existsSync10(join16(dir, "elevate.config.json"))) return dir;
2042
- const parent = dirname8(dir);
3347
+ if (existsSync16(join23(dir, "elevate.config.json"))) return dir;
3348
+ const parent = dirname10(dir);
2043
3349
  if (dir === boundary || parent === dir) break;
2044
3350
  dir = parent;
2045
3351
  }
@@ -2104,15 +3410,15 @@ function resolveConfig(rootDir, file, source = "elevate.config.json") {
2104
3410
  }
2105
3411
  function loadConfig2(startDir = process.cwd()) {
2106
3412
  const rootDir = findRepositoryRoot(startDir);
2107
- const configPath = join16(rootDir, "elevate.config.json");
3413
+ const configPath = join23(rootDir, "elevate.config.json");
2108
3414
  let file = {};
2109
- if (existsSync10(configPath)) {
3415
+ if (existsSync16(configPath)) {
2110
3416
  const unreadable = (detail) => new ConfigError(
2111
3417
  `Cannot read elevate.config.json: ${detail} (${configPath}). Fix the file or remove it; ${NO_CONTINUE}`
2112
3418
  );
2113
3419
  let parsed;
2114
3420
  try {
2115
- parsed = JSON.parse(readFileSync9(configPath, "utf8").replace(/^/, ""));
3421
+ parsed = JSON.parse(readFileSync16(configPath, "utf8").replace(/^/, ""));
2116
3422
  } catch (err) {
2117
3423
  throw unreadable(err instanceof Error ? err.message : String(err));
2118
3424
  }
@@ -2161,7 +3467,9 @@ var de = {
2161
3467
  "lookup-failed": "Abfrage der Registry fehlgeschlagen",
2162
3468
  "managed-externally": "Version wird au\xDFerhalb dieses Repos verwaltet",
2163
3469
  "declaration-mismatch": "Ort der Versionsangabe ist nicht eindeutig",
2164
- "invalid-name": "ung\xFCltiger Paketname"
3470
+ "invalid-name": "ung\xFCltiger Paketname",
3471
+ "not-linked": "Workspace-Paket, das pnpm ohne das workspace:-Protokoll nicht verlinkt",
3472
+ "catalog-unresolved": "Catalog-Eintrag konnte nicht aufgel\xF6st werden"
2165
3473
  })[reason]
2166
3474
  },
2167
3475
  badges: {
@@ -2171,7 +3479,8 @@ var de = {
2171
3479
  dev: "(dev/test)",
2172
3480
  prod: "(prod/compile)",
2173
3481
  align: "[Angleichen]",
2174
- internal: "[Intern]"
3482
+ internal: "[Intern]",
3483
+ shared: (count) => `[+${count} Crates]`
2175
3484
  },
2176
3485
  status: {
2177
3486
  selectedOf: (selected, total) => `${selected} von ${total}`,
@@ -2200,6 +3509,7 @@ var de = {
2200
3509
  hint: "[\u2191/\u2193/j/k] Navigieren \u2502 [Enter] Ausw\xE4hlen \u2502 [Esc] Abbrechen \u2502 [Backspace] Filter l\xF6schen",
2201
3510
  latestBadge: "LATEST",
2202
3511
  currentBadge: "AKTUELL",
3512
+ alsoChanges: (names) => `\xC4ndert auch: ${names.join(", ")}`,
2203
3513
  customBadge: "MANUELL"
2204
3514
  },
2205
3515
  updating: {
@@ -2228,6 +3538,7 @@ var de = {
2228
3538
  scanningChirps: ["*such*", "pr\xFCfe...", "oh, Updates?", "einen Moment!"],
2229
3539
  updatingChirps: ["*schuft*", "baue Code...", "gleich fertig!", "arbeite emsig..."],
2230
3540
  successChirps: ["hurra! \u{1F389}", "alles aktuell!", "top gepflegt! \u{1FAB6}", "super gemacht!"],
3541
+ cargoChirps: ["*krabbel* \u{1F980}", "cargo check l\xE4uft durch!", "Borrow Checker nickt!", "furchtlose Updates!"],
2231
3542
  mavenChirps: ["*kaffee schl\xFCrf* \u2615", "Spring Boot l\xE4uft!", "Java 25 rockt!", "sauber gebaut!"]
2232
3543
  },
2233
3544
  splash: {
@@ -2267,7 +3578,9 @@ var en = {
2267
3578
  "lookup-failed": "registry lookup failed",
2268
3579
  "managed-externally": "version is managed outside this repository",
2269
3580
  "declaration-mismatch": "version location is ambiguous",
2270
- "invalid-name": "invalid package name"
3581
+ "invalid-name": "invalid package name",
3582
+ "not-linked": "workspace package that pnpm does not link without the workspace: protocol",
3583
+ "catalog-unresolved": "catalog entry could not be resolved"
2271
3584
  })[reason]
2272
3585
  },
2273
3586
  badges: {
@@ -2277,7 +3590,8 @@ var en = {
2277
3590
  dev: "(dev/test)",
2278
3591
  prod: "(prod/compile)",
2279
3592
  align: "[Align]",
2280
- internal: "[Internal]"
3593
+ internal: "[Internal]",
3594
+ shared: (count) => `[+${count} crates]`
2281
3595
  },
2282
3596
  status: {
2283
3597
  selectedOf: (selected, total) => `${selected} of ${total}`,
@@ -2306,6 +3620,7 @@ var en = {
2306
3620
  hint: "[\u2191/\u2193/j/k] Navigate \u2502 [Enter] Select \u2502 [Esc] Cancel \u2502 [Backspace] Clear filter",
2307
3621
  latestBadge: "LATEST",
2308
3622
  currentBadge: "CURRENT",
3623
+ alsoChanges: (names) => `Also changes: ${names.join(", ")}`,
2309
3624
  customBadge: "MANUAL"
2310
3625
  },
2311
3626
  updating: {
@@ -2334,6 +3649,7 @@ var en = {
2334
3649
  scanningChirps: ["*peeking*", "checking...", "ooh, updates?", "just a sec!"],
2335
3650
  updatingChirps: ["*working hard*", "building...", "almost there!", "packing modules..."],
2336
3651
  successChirps: ["hooray! \u{1F389}", "all updated!", "looking sharp! \u{1FAB6}", "great job!"],
3652
+ cargoChirps: ["*crab walk* \u{1F980}", "cargo check passes!", "borrow checker approves!", "fearless updates!"],
2337
3653
  mavenChirps: ["*sipping coffee* \u2615", "Spring Boot rocks!", "Java 25 is fly!", "clean build!"]
2338
3654
  },
2339
3655
  splash: {
@@ -2370,48 +3686,12 @@ function useI18n(initialLocale = "de") {
2370
3686
 
2371
3687
  // src/hooks/use-workspaces.ts
2372
3688
  import { useState as useState2, useEffect, useCallback as useCallback2 } from "react";
2373
- function useWorkspaces(strategy, rootDir) {
2374
- const [modules, setModules] = useState2([]);
2375
- const [loading, setLoading] = useState2(true);
2376
- const [activeIndex, setActiveIndex] = useState2(0);
2377
- const reload = useCallback2(async () => {
2378
- setLoading(true);
2379
- setModules([]);
2380
- setActiveIndex(0);
2381
- try {
2382
- setModules(await strategy.discovery.discover(rootDir));
2383
- } catch {
2384
- setModules([]);
2385
- } finally {
2386
- setLoading(false);
2387
- }
2388
- }, [strategy, rootDir]);
2389
- useEffect(() => {
2390
- reload();
2391
- }, [reload]);
2392
- const active = modules[activeIndex] || {
2393
- id: "empty",
2394
- name: "No module found",
2395
- path: rootDir,
2396
- relPath: "Root",
2397
- ecosystem: strategy.ecosystem,
2398
- isRoot: true
2399
- };
2400
- return {
2401
- modules,
2402
- active,
2403
- activeIndex,
2404
- setActiveIndex,
2405
- loading,
2406
- reload
2407
- };
2408
- }
2409
-
2410
- // src/hooks/use-packages.ts
2411
- import { useState as useState3, useEffect as useEffect2, useMemo as useMemo2, useCallback as useCallback3, useRef } from "react";
2412
- import semver5 from "semver";
2413
3689
 
2414
3690
  // src/application/scan.ts
3691
+ async function discoverModules(strategy, rootDir) {
3692
+ const modules = await strategy.discovery.discover(rootDir);
3693
+ return { modules, diagnostics: strategy.discovery.diagnostics?.() ?? [] };
3694
+ }
2415
3695
  function createScanContext(modules, config, channel) {
2416
3696
  return {
2417
3697
  rootDir: config.rootDir,
@@ -2461,12 +3741,62 @@ function candidateToJson(u) {
2461
3741
  scope: u.scope,
2462
3742
  isPreRelease: u.isPreRelease ?? false,
2463
3743
  preReleaseTag: u.preReleaseTag,
2464
- declaredIn: u.declaration ? { file: u.declaration.displayPath, kind: u.declaration.kind, property: u.declaration.propertyName } : void 0,
3744
+ declaredIn: u.declaration ? {
3745
+ file: u.declaration.displayPath,
3746
+ kind: u.declaration.kind,
3747
+ property: u.declaration.propertyName,
3748
+ catalog: u.declaration.catalogName
3749
+ } : void 0,
2465
3750
  sharedWith: u.sharedWith
2466
3751
  };
2467
3752
  }
2468
3753
 
3754
+ // src/hooks/use-workspaces.ts
3755
+ function useWorkspaces(strategy, rootDir) {
3756
+ const [modules, setModules] = useState2([]);
3757
+ const [loading, setLoading] = useState2(true);
3758
+ const [activeIndex, setActiveIndex] = useState2(0);
3759
+ const [diagnostics, setDiagnostics] = useState2([]);
3760
+ const reload = useCallback2(async () => {
3761
+ setLoading(true);
3762
+ setModules([]);
3763
+ setActiveIndex(0);
3764
+ setDiagnostics([]);
3765
+ try {
3766
+ const found = await discoverModules(strategy, rootDir);
3767
+ setModules(found.modules);
3768
+ setDiagnostics(found.diagnostics);
3769
+ } catch (err) {
3770
+ setModules([]);
3771
+ setDiagnostics([err instanceof Error ? err.message : String(err)]);
3772
+ } finally {
3773
+ setLoading(false);
3774
+ }
3775
+ }, [strategy, rootDir]);
3776
+ useEffect(() => {
3777
+ reload();
3778
+ }, [reload]);
3779
+ const active = modules[activeIndex] || {
3780
+ id: "empty",
3781
+ name: "No module found",
3782
+ path: rootDir,
3783
+ relPath: "Root",
3784
+ ecosystem: strategy.ecosystem,
3785
+ isRoot: true
3786
+ };
3787
+ return {
3788
+ modules,
3789
+ active,
3790
+ activeIndex,
3791
+ setActiveIndex,
3792
+ loading,
3793
+ diagnostics,
3794
+ reload
3795
+ };
3796
+ }
3797
+
2469
3798
  // src/hooks/use-packages.ts
3799
+ import { useState as useState3, useEffect as useEffect2, useMemo as useMemo2, useCallback as useCallback3, useRef } from "react";
2470
3800
  function usePackages(strategy, module, modules, config, channel = "stable") {
2471
3801
  const [candidates, setCandidates] = useState3([]);
2472
3802
  const [skipped, setSkipped] = useState3([]);
@@ -2533,15 +3863,12 @@ function usePackages(strategy, module, modules, config, channel = "stable") {
2533
3863
  setCandidates(
2534
3864
  (prev) => prev.map((c) => {
2535
3865
  if (c.coordinate.identifier !== identifier) return c;
2536
- const target = c.coordinate.ecosystem === "maven" ? cleanJavaVersion(chosenVersion) : semver5.valid(chosenVersion);
2537
- let diff = "minor";
2538
- if (c.currentClean && target && semver5.valid(c.currentClean)) {
2539
- diff = semver5.eq(c.currentClean, target) ? "patch" : diffVersions(c.currentClean, target);
2540
- }
3866
+ const policy = strategy.versioning;
3867
+ const diff = c.currentClean ? policy.diff(c.currentClean, chosenVersion) : "minor";
2541
3868
  const isPre = isPreReleaseVersion(chosenVersion);
2542
3869
  return {
2543
3870
  ...c,
2544
- newRange: c.coordinate.ecosystem === "npm" ? `${rangePrefix(c.currentRange)}${chosenVersion}` : chosenVersion,
3871
+ newRange: policy.rewrite(c.currentRange, chosenVersion),
2545
3872
  latest: chosenVersion,
2546
3873
  diff,
2547
3874
  selected: true,
@@ -2551,7 +3878,7 @@ function usePackages(strategy, module, modules, config, channel = "stable") {
2551
3878
  };
2552
3879
  })
2553
3880
  );
2554
- }, []);
3881
+ }, [strategy]);
2555
3882
  const switchTab = useCallback3((tab) => {
2556
3883
  setActiveTab(tab);
2557
3884
  setCursor(0);
@@ -2583,24 +3910,24 @@ function usePackages(strategy, module, modules, config, channel = "stable") {
2583
3910
  import { useState as useState4, useCallback as useCallback4 } from "react";
2584
3911
 
2585
3912
  // src/application/update-workflow.ts
2586
- import { existsSync as existsSync12 } from "fs";
2587
- import { relative as relative7 } from "path";
3913
+ import { existsSync as existsSync18 } from "fs";
3914
+ import { relative as relative10 } from "path";
2588
3915
 
2589
3916
  // src/adapters/shared/file-snapshot.ts
2590
- import { existsSync as existsSync11, readFileSync as readFileSync10, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
3917
+ import { existsSync as existsSync17, readFileSync as readFileSync17, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
2591
3918
  var FileSnapshot = class _FileSnapshot {
2592
- constructor(entries) {
2593
- this.entries = entries;
3919
+ constructor(entries2) {
3920
+ this.entries = entries2;
2594
3921
  }
2595
3922
  entries;
2596
3923
  /** Records the current content (or absence) of each file. */
2597
3924
  static capture(files) {
2598
- const entries = /* @__PURE__ */ new Map();
3925
+ const entries2 = /* @__PURE__ */ new Map();
2599
3926
  for (const file of files) {
2600
- if (entries.has(file)) continue;
2601
- entries.set(file, existsSync11(file) ? readFileSync10(file) : null);
3927
+ if (entries2.has(file)) continue;
3928
+ entries2.set(file, existsSync17(file) ? readFileSync17(file) : null);
2602
3929
  }
2603
- return new _FileSnapshot(entries);
3930
+ return new _FileSnapshot(entries2);
2604
3931
  }
2605
3932
  /** The files covered by this snapshot. */
2606
3933
  get files() {
@@ -2610,9 +3937,9 @@ var FileSnapshot = class _FileSnapshot {
2610
3937
  changedFiles() {
2611
3938
  return this.files.filter((file) => {
2612
3939
  const before = this.entries.get(file) ?? null;
2613
- const exists = existsSync11(file);
3940
+ const exists = existsSync17(file);
2614
3941
  if (before === null) return exists;
2615
- return !exists || !readFileSync10(file).equals(before);
3942
+ return !exists || !readFileSync17(file).equals(before);
2616
3943
  });
2617
3944
  }
2618
3945
  /** Writes every file back to its captured state; removes files that did not exist. */
@@ -2621,7 +3948,7 @@ var FileSnapshot = class _FileSnapshot {
2621
3948
  if (content === null) {
2622
3949
  rmSync2(file, { force: true });
2623
3950
  } else {
2624
- writeFileSync4(file, content);
3951
+ writeFileSync5(file, content);
2625
3952
  }
2626
3953
  }
2627
3954
  }
@@ -2660,7 +3987,7 @@ Restoring the installed state failed as well: ${message(err)}`;
2660
3987
  let baseline;
2661
3988
  if (!options.skipVerification) {
2662
3989
  progress("Running verification on the current state\u2026");
2663
- baseline = await verify(snapshot.files.filter((file) => existsSync12(file)));
3990
+ baseline = await verify(snapshot.files.filter((file) => existsSync18(file)));
2664
3991
  }
2665
3992
  let applied;
2666
3993
  try {
@@ -2706,7 +4033,7 @@ function message(err) {
2706
4033
  return err instanceof Error ? err.message : String(err);
2707
4034
  }
2708
4035
  function displayPaths(files, rootDir) {
2709
- return files.map((file) => relative7(rootDir, file).replace(/\\/g, "/"));
4036
+ return files.map((file) => relative10(rootDir, file).replace(/\\/g, "/"));
2710
4037
  }
2711
4038
 
2712
4039
  // src/hooks/use-updater.ts
@@ -2864,7 +4191,8 @@ var Header = ({
2864
4191
  currentLocale,
2865
4192
  strategy,
2866
4193
  nextStrategyName,
2867
- channel
4194
+ channel,
4195
+ rootDir
2868
4196
  }) => /* @__PURE__ */ jsxs2(
2869
4197
  Box2,
2870
4198
  {
@@ -2885,7 +4213,7 @@ var Header = ({
2885
4213
  /* @__PURE__ */ jsxs2(Text2, { color: theme.colors.brandLight, bold: true, children: [
2886
4214
  strategy.icon,
2887
4215
  " ",
2888
- strategy.displayName
4216
+ rootDir && strategy.displayNameFor?.(rootDir) || strategy.displayName
2889
4217
  ] }),
2890
4218
  /* @__PURE__ */ jsxs2(Text2, { color: theme.colors.muted, children: [
2891
4219
  " [",
@@ -2919,33 +4247,39 @@ var Header = ({
2919
4247
  // src/components/workspace-bar.tsx
2920
4248
  import { Box as Box3, Text as Text3 } from "ink";
2921
4249
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2922
- var WorkspaceBar = ({ module, internalScopes, strategy, t }) => {
4250
+ var WorkspaceBar = ({ module, internalScopes, strategy, t, diagnostics = [] }) => {
2923
4251
  const scopeHint = internalScopes.length > 0 ? t.status.internalScopes(internalScopes) : t.status.noInternalScopes;
2924
- return /* @__PURE__ */ jsxs3(Box3, { marginY: 1, justifyContent: "space-between", alignItems: "center", children: [
2925
- /* @__PURE__ */ jsxs3(Box3, { alignItems: "center", flexShrink: 1, children: [
2926
- /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
2927
- "\u{1F4CD} ",
2928
- strategy.icon,
2929
- " ",
2930
- t.workspace.label,
2931
- ": "
4252
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
4253
+ /* @__PURE__ */ jsxs3(Box3, { marginY: 1, justifyContent: "space-between", alignItems: "center", children: [
4254
+ /* @__PURE__ */ jsxs3(Box3, { alignItems: "center", flexShrink: 1, children: [
4255
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
4256
+ "\u{1F4CD} ",
4257
+ strategy.icon,
4258
+ " ",
4259
+ t.workspace.label,
4260
+ ": "
4261
+ ] }),
4262
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.colors.brandLight, children: module.name }),
4263
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.colors.muted, children: [
4264
+ " (",
4265
+ module.relPath,
4266
+ ")"
4267
+ ] })
2932
4268
  ] }),
2933
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.colors.brandLight, children: module.name }),
2934
- /* @__PURE__ */ jsxs3(Text3, { color: theme.colors.muted, children: [
2935
- " (",
2936
- module.relPath,
2937
- ")"
4269
+ /* @__PURE__ */ jsxs3(Box3, { alignItems: "center", marginLeft: 2, children: [
4270
+ /* @__PURE__ */ jsx3(Text3, { color: theme.colors.muted, children: scopeHint }),
4271
+ /* @__PURE__ */ jsx3(Text3, { color: theme.colors.muted, children: " \u2502 " }),
4272
+ /* @__PURE__ */ jsx3(Text3, { color: theme.colors.brandLight, bold: true, children: "[W]" }),
4273
+ /* @__PURE__ */ jsxs3(Text3, { color: theme.colors.muted, children: [
4274
+ " ",
4275
+ t.workspace.switchHint.replace("[W]", "").trim()
4276
+ ] })
2938
4277
  ] })
2939
4278
  ] }),
2940
- /* @__PURE__ */ jsxs3(Box3, { alignItems: "center", marginLeft: 2, children: [
2941
- /* @__PURE__ */ jsx3(Text3, { color: theme.colors.muted, children: scopeHint }),
2942
- /* @__PURE__ */ jsx3(Text3, { color: theme.colors.muted, children: " \u2502 " }),
2943
- /* @__PURE__ */ jsx3(Text3, { color: theme.colors.brandLight, bold: true, children: "[W]" }),
2944
- /* @__PURE__ */ jsxs3(Text3, { color: theme.colors.muted, children: [
2945
- " ",
2946
- t.workspace.switchHint.replace("[W]", "").trim()
2947
- ] })
2948
- ] })
4279
+ diagnostics.slice(0, 2).map((d) => /* @__PURE__ */ jsxs3(Text3, { color: theme.colors.danger, wrap: "truncate-end", children: [
4280
+ "\u26A0 ",
4281
+ d.split("\n")[0]
4282
+ ] }, d))
2949
4283
  ] });
2950
4284
  };
2951
4285
 
@@ -2995,7 +4329,9 @@ import { Box as Box5, Text as Text5 } from "ink";
2995
4329
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2996
4330
  var PackageRow = ({ item, isFocused, t }) => {
2997
4331
  const badge = item.diff === "major" ? /* @__PURE__ */ jsx5(Text5, { bold: true, color: theme.colors.danger, children: t.badges.major }) : item.diff === "minor" ? /* @__PURE__ */ jsx5(Text5, { color: theme.colors.warning, children: t.badges.minor }) : /* @__PURE__ */ jsx5(Text5, { color: theme.colors.success, children: t.badges.patch });
2998
- const displayName = item.coordinate.identifier;
4332
+ const shared = item.sharedWith && item.sharedWith.length > 0 ? ` ${t.badges.shared(item.sharedWith.length)}` : "";
4333
+ const label = item.qualifier ? `${item.coordinate.identifier} (${item.qualifier})` : item.coordinate.identifier;
4334
+ const displayName = label.slice(0, 42 - shared.length) + shared;
2999
4335
  const isDevOrTest = item.scope === "dev" || item.scope === "test";
3000
4336
  const property = item.declaration?.kind === "property" ? `\${${item.declaration.propertyName}}` : void 0;
3001
4337
  return /* @__PURE__ */ jsxs5(Box5, { justifyContent: "space-between", children: [
@@ -3174,7 +4510,7 @@ var ChickenMascot = ({ state = "idle", t, ecosystem = "npm" }) => {
3174
4510
  chickFace = cur.face;
3175
4511
  wingLeft = cur.l;
3176
4512
  wingRight = cur.r;
3177
- const chirpsPool = ecosystem === "maven" ? [...t.mascot.idleChirps, ...t.mascot.mavenChirps] : t.mascot.idleChirps;
4513
+ const chirpsPool = ecosystem === "maven" ? [...t.mascot.idleChirps, ...t.mascot.mavenChirps] : ecosystem === "cargo" ? [...t.mascot.idleChirps, ...t.mascot.cargoChirps] : t.mascot.idleChirps;
3178
4514
  currentChirp = chirpsPool[chirpIndex % chirpsPool.length];
3179
4515
  }
3180
4516
  return /* @__PURE__ */ jsxs7(Box7, { alignItems: "center", children: [
@@ -3452,6 +4788,7 @@ var VersionModal = ({
3452
4788
  ] }),
3453
4789
  /* @__PURE__ */ jsx11(Text11, { color: theme.colors.muted, children: t.versionModal.current(candidate.currentRange) })
3454
4790
  ] }),
4791
+ candidate.sharedWith && candidate.sharedWith.length > 0 && /* @__PURE__ */ jsx11(Box11, { marginBottom: 1, children: /* @__PURE__ */ jsx11(Text11, { color: theme.colors.warning, children: t.versionModal.alsoChanges(candidate.sharedWith) }) }),
3455
4792
  /* @__PURE__ */ jsxs11(
3456
4793
  Box11,
3457
4794
  {
@@ -3626,10 +4963,10 @@ var App = ({ config }) => {
3626
4963
  const [versionPickerCandidate, setVersionPickerCandidate] = useState8(null);
3627
4964
  const [ecosystem, setEcosystem] = useState8("npm");
3628
4965
  const strategy = useMemo4(() => EcosystemFactory.getStrategy(ecosystem), [ecosystem]);
3629
- const nextEcosystem = ecosystem === "npm" ? "maven" : "npm";
4966
+ const nextEcosystem = EcosystemFactory.next(ecosystem);
3630
4967
  const nextStrategy = useMemo4(() => EcosystemFactory.getStrategy(nextEcosystem), [nextEcosystem]);
3631
4968
  const toggleEcosystem = useCallback5(() => {
3632
- setEcosystem((prev) => prev === "npm" ? "maven" : "npm");
4969
+ setEcosystem((prev) => EcosystemFactory.next(prev));
3633
4970
  }, []);
3634
4971
  const { locale, toggleLocale, t } = useI18n(resolveInitialLocale(config.locale));
3635
4972
  const ws = useWorkspaces(strategy, config.rootDir);
@@ -3733,7 +5070,8 @@ var App = ({ config }) => {
3733
5070
  currentLocale: locale,
3734
5071
  strategy,
3735
5072
  nextStrategyName: `${nextStrategy.icon} ${nextStrategy.displayName}`,
3736
- channel: config.channel
5073
+ channel: config.channel,
5074
+ rootDir: config.rootDir
3737
5075
  }
3738
5076
  ),
3739
5077
  view === "workspace_modal" && /* @__PURE__ */ jsx14(
@@ -3780,7 +5118,8 @@ var App = ({ config }) => {
3780
5118
  module: ws.active,
3781
5119
  internalScopes: config.internalScopes,
3782
5120
  strategy,
3783
- t
5121
+ t,
5122
+ diagnostics: ws.diagnostics
3784
5123
  }
3785
5124
  ),
3786
5125
  /* @__PURE__ */ jsx14(
@@ -3820,6 +5159,7 @@ var App = ({ config }) => {
3820
5159
  };
3821
5160
 
3822
5161
  // src/cli/parser.ts
5162
+ var isEcosystem = (v) => EcosystemFactory.getAvailableEcosystems().includes(v);
3823
5163
  function parseCliArgs(args) {
3824
5164
  const [first, ...rest] = args;
3825
5165
  if (!first || first === "--help" || first === "-h" || first === "help") {
@@ -3857,10 +5197,10 @@ function parseCliArgs(args) {
3857
5197
  result.help = true;
3858
5198
  } else if (arg.startsWith("--ecosystem=")) {
3859
5199
  const val = arg.split("=")[1];
3860
- if (val === "npm" || val === "maven") result.ecosystem = val;
5200
+ if (isEcosystem(val)) result.ecosystem = val;
3861
5201
  } else if (arg === "-e" || arg === "--ecosystem") {
3862
5202
  const val = remaining[++i];
3863
- if (val === "npm" || val === "maven") result.ecosystem = val;
5203
+ if (isEcosystem(val)) result.ecosystem = val;
3864
5204
  } else if (arg.startsWith("--module=")) {
3865
5205
  result.module = arg.split("=")[1];
3866
5206
  } else if (arg === "-m" || arg === "--module") {
@@ -3902,7 +5242,7 @@ SUBCOMMANDS:
3902
5242
  mcp Start stdio-based MCP server
3903
5243
 
3904
5244
  FLAGS:
3905
- -e, --ecosystem <npm|maven> Target ecosystem (default: npm)
5245
+ -e, --ecosystem <npm|maven|cargo> Target ecosystem (default: npm)
3906
5246
  -m, --module <name|path> Target module ID or relative path (default: root)
3907
5247
  -c, --channel <stable|all> Release channel (default: stable)
3908
5248
  -p, --packages <pkg1,pkg2@1.2.3> Comma-separated list of packages to update
@@ -3930,19 +5270,22 @@ EXAMPLES:
3930
5270
  async function handleModulesCommand(options, config) {
3931
5271
  const ecosystem = options.ecosystem ?? "npm";
3932
5272
  const strategy = EcosystemFactory.getStrategy(ecosystem);
3933
- const modules = await strategy.discovery.discover(config.rootDir);
5273
+ const { modules, diagnostics } = await discoverModules(strategy, config.rootDir);
5274
+ const displayName = strategy.displayNameFor?.(config.rootDir) ?? strategy.displayName;
5275
+ const packageManager = strategy.packageManagerFor?.(config.rootDir);
3934
5276
  if (options.json) {
3935
- console.log(JSON.stringify({ ecosystem, count: modules.length, modules }, null, 2));
5277
+ console.log(JSON.stringify({ ecosystem, ...packageManager ? { packageManager } : {}, count: modules.length, modules, diagnostics }, null, 2));
3936
5278
  return 0;
3937
5279
  }
3938
5280
  console.log(`
3939
- \u{1FAB6} Discovered ${strategy.icon} ${strategy.displayName} Modules (${modules.length}):
5281
+ \u{1FAB6} Discovered ${strategy.icon} ${displayName} Modules (${modules.length}):
3940
5282
  `);
3941
5283
  for (const m of modules) {
3942
5284
  const isRootBadge = m.isRoot ? " [ROOT]" : "";
3943
5285
  console.log(` \u2022 ${m.name.padEnd(45)} (${m.relPath})${isRootBadge}`);
3944
5286
  }
3945
5287
  console.log("");
5288
+ for (const problem of diagnostics) console.error(`\u26A0\uFE0F ${problem}`);
3946
5289
  return 0;
3947
5290
  }
3948
5291
 
@@ -3960,6 +5303,12 @@ function describeSkip(skipped) {
3960
5303
  return `version location is ambiguous${detail}`;
3961
5304
  case "invalid-name":
3962
5305
  return "not a valid package name";
5306
+ case "not-linked":
5307
+ return `workspace package that pnpm does not link unless it is declared with the workspace: protocol${detail}`;
5308
+ case "catalog-unresolved":
5309
+ return `catalog entry could not be resolved${detail}`;
5310
+ case "incompatible-rust-version":
5311
+ return `newer versions need a newer Rust than the project's rust-version${detail}`;
3963
5312
  }
3964
5313
  }
3965
5314
 
@@ -3968,7 +5317,8 @@ async function handleScanCommand(options, config) {
3968
5317
  const ecosystem = options.ecosystem ?? "npm";
3969
5318
  const channel = options.channel ?? config.channel ?? "stable";
3970
5319
  const strategy = EcosystemFactory.getStrategy(ecosystem);
3971
- const modules = await strategy.discovery.discover(config.rootDir);
5320
+ const { modules, diagnostics } = await discoverModules(strategy, config.rootDir);
5321
+ for (const problem of diagnostics) console.error(`\u26A0\uFE0F ${problem}`);
3972
5322
  if (modules.length === 0) {
3973
5323
  if (options.json) {
3974
5324
  console.log(JSON.stringify({ error: `No ${strategy.displayName} modules found.`, ecosystem }, null, 2));
@@ -4118,7 +5468,7 @@ async function handleVersionsCommand(options, config) {
4118
5468
  }
4119
5469
 
4120
5470
  // src/application/update-selection.ts
4121
- import semver6 from "semver";
5471
+ import semver12 from "semver";
4122
5472
  var UpdateSelectionError = class extends Error {
4123
5473
  constructor(message2) {
4124
5474
  super(message2);
@@ -4143,9 +5493,9 @@ function selectRequested(scan, requests, options) {
4143
5493
  `'${request.identifier}' has no pending update. Specify a target version (e.g. ${request.identifier}@1.2.3).`
4144
5494
  );
4145
5495
  }
4146
- if (options.ecosystem === "maven") {
5496
+ if (!policyOf(options).allowsManualTargets) {
4147
5497
  throw new UpdateSelectionError(
4148
- `'${request.identifier}' has no pending update. Maven updates are written where the scan located the version; a dependency without an offered update cannot be retargeted.`
5498
+ `'${request.identifier}' has no pending update. Updates are written where the scan located the version; a dependency without an offered update cannot be retargeted.`
4149
5499
  );
4150
5500
  }
4151
5501
  if (options.originOf(request.identifier) !== "public") {
@@ -4170,12 +5520,12 @@ function retarget(found, target, options) {
4170
5520
  `'${found.coordinate.identifier}' is a workspace module; it can only be aligned to its local version ${found.latest}.`
4171
5521
  );
4172
5522
  }
4173
- if (options.ecosystem === "maven" && found.availableVersions && !found.availableVersions.includes(target)) {
5523
+ if (!policyOf(options).allowsManualTargets && found.availableVersions && !found.availableVersions.includes(target)) {
4174
5524
  throw new UpdateSelectionError(
4175
5525
  `'${found.coordinate.identifier}' has no version ${target} in the configured repositories. Available: ${found.availableVersions.slice(0, 5).join(", ")}${found.availableVersions.length > 5 ? ", \u2026" : ""}.`
4176
5526
  );
4177
5527
  }
4178
- const diff = computeDiff(found.currentClean, target, options.ecosystem);
5528
+ const diff = policyOf(options).diff(found.currentClean, target);
4179
5529
  if (diff === "major" && !options.allowMajor) {
4180
5530
  throw new UpdateSelectionError(
4181
5531
  `'${found.coordinate.identifier}' requires a major upgrade to ${target}. Pass --allow-major / allowMajor.`
@@ -4184,7 +5534,7 @@ function retarget(found, target, options) {
4184
5534
  const isPreRelease = isPreReleaseVersion(target);
4185
5535
  return {
4186
5536
  ...found,
4187
- newRange: options.ecosystem === "npm" ? `${rangePrefix(found.currentRange)}${target}` : target,
5537
+ newRange: policyOf(options).rewrite(found.currentRange, target),
4188
5538
  diff,
4189
5539
  selected: true,
4190
5540
  isCustomVersion: true,
@@ -4193,7 +5543,7 @@ function retarget(found, target, options) {
4193
5543
  };
4194
5544
  }
4195
5545
  function manualNpmCandidate(identifier, target, options) {
4196
- if (!semver6.valid(target)) {
5546
+ if (!semver12.valid(target)) {
4197
5547
  throw new UpdateSelectionError(`'${target}' is not a valid version for '${identifier}'.`);
4198
5548
  }
4199
5549
  return {
@@ -4210,10 +5560,8 @@ function manualNpmCandidate(identifier, target, options) {
4210
5560
  isCustomVersion: true
4211
5561
  };
4212
5562
  }
4213
- function computeDiff(current, target, ecosystem) {
4214
- const to = ecosystem === "maven" ? cleanJavaVersion(target) : semver6.valid(target);
4215
- if (!to || !semver6.valid(current)) return "minor";
4216
- return semver6.eq(current, to) ? "patch" : diffVersions(current, to);
5563
+ function policyOf(options) {
5564
+ return EcosystemFactory.getStrategy(options.ecosystem).versioning;
4217
5565
  }
4218
5566
 
4219
5567
  // src/cli/command-update.ts
@@ -4225,7 +5573,8 @@ async function handleUpdateCommand(options, config) {
4225
5573
  else console.error(`\u274C ${message2}`);
4226
5574
  return code;
4227
5575
  };
4228
- const modules = await strategy.discovery.discover(config.rootDir);
5576
+ const { modules, diagnostics } = await discoverModules(strategy, config.rootDir);
5577
+ for (const problem of diagnostics) console.error(`\u26A0\uFE0F ${problem}`);
4229
5578
  if (modules.length === 0) return fail(`No ${strategy.displayName} modules found.`);
4230
5579
  const targetModule = options.module ? findModule(modules, options.module) : modules[0];
4231
5580
  if (!targetModule) return fail(`Module '${options.module}' not found.`);
@@ -4366,9 +5715,9 @@ function parsePackageArgument(item) {
4366
5715
  }
4367
5716
 
4368
5717
  // src/cli/command-init.ts
4369
- import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2 } from "fs";
4370
- import { homedir, platform } from "os";
4371
- import { join as join17, resolve as resolve11 } from "path";
5718
+ import { existsSync as existsSync19, readFileSync as readFileSync18, writeFileSync as writeFileSync6, mkdirSync as mkdirSync2 } from "fs";
5719
+ import { homedir as homedir2, platform } from "os";
5720
+ import { join as join24, resolve as resolve15 } from "path";
4372
5721
  function mcpServerEntry() {
4373
5722
  return {
4374
5723
  command: "npx",
@@ -4411,10 +5760,10 @@ automatic rollback.
4411
5760
  requested). A failed install or verification is rolled back automatically.
4412
5761
  4. \`elevate_verify\` \u2014 Verify build integrity when needed
4413
5762
  `;
4414
- var home = homedir();
5763
+ var home = homedir2();
4415
5764
  var isWin = platform() === "win32";
4416
5765
  function appData() {
4417
- return process.env.APPDATA || join17(home, "AppData", "Roaming");
5766
+ return process.env.APPDATA || join24(home, "AppData", "Roaming");
4418
5767
  }
4419
5768
  function detectCursor(cwd) {
4420
5769
  const env = {
@@ -4426,25 +5775,25 @@ function detectCursor(cwd) {
4426
5775
  mcpConfigured: false,
4427
5776
  skillConfigured: false
4428
5777
  };
4429
- const workspaceCursorDir = join17(cwd, ".cursor");
4430
- const globalCursorDir = join17(home, ".cursor");
4431
- if (existsSync13(workspaceCursorDir) || existsSync13(globalCursorDir)) {
5778
+ const workspaceCursorDir = join24(cwd, ".cursor");
5779
+ const globalCursorDir = join24(home, ".cursor");
5780
+ if (existsSync19(workspaceCursorDir) || existsSync19(globalCursorDir)) {
4432
5781
  env.detected = true;
4433
- env.reason = existsSync13(workspaceCursorDir) ? `Found .cursor/ in workspace` : `Found ~/.cursor/ global config`;
5782
+ env.reason = existsSync19(workspaceCursorDir) ? `Found .cursor/ in workspace` : `Found ~/.cursor/ global config`;
4434
5783
  }
4435
- const mcpPath = join17(cwd, ".cursor", "mcp.json");
4436
- if (existsSync13(mcpPath)) {
5784
+ const mcpPath = join24(cwd, ".cursor", "mcp.json");
5785
+ if (existsSync19(mcpPath)) {
4437
5786
  try {
4438
- const content = JSON.parse(readFileSync11(mcpPath, "utf8"));
5787
+ const content = JSON.parse(readFileSync18(mcpPath, "utf8"));
4439
5788
  if (content?.mcpServers?.elevate) {
4440
5789
  env.mcpConfigured = true;
4441
5790
  }
4442
5791
  } catch {
4443
5792
  }
4444
5793
  }
4445
- const rulesPath = join17(cwd, ".cursorrules");
4446
- if (existsSync13(rulesPath)) {
4447
- const content = readFileSync11(rulesPath, "utf8");
5794
+ const rulesPath = join24(cwd, ".cursorrules");
5795
+ if (existsSync19(rulesPath)) {
5796
+ const content = readFileSync18(rulesPath, "utf8");
4448
5797
  if (content.includes("elevate") || content.includes("Elevate")) {
4449
5798
  env.skillConfigured = true;
4450
5799
  }
@@ -4461,12 +5810,12 @@ function detectClaudeDesktop() {
4461
5810
  mcpConfigured: false,
4462
5811
  skillConfigured: false
4463
5812
  };
4464
- const configPath = isWin ? join17(appData(), "Claude", "claude_desktop_config.json") : join17(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
4465
- if (existsSync13(configPath)) {
5813
+ const configPath = isWin ? join24(appData(), "Claude", "claude_desktop_config.json") : join24(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
5814
+ if (existsSync19(configPath)) {
4466
5815
  env.detected = true;
4467
5816
  env.reason = `Found ${configPath}`;
4468
5817
  try {
4469
- const content = JSON.parse(readFileSync11(configPath, "utf8"));
5818
+ const content = JSON.parse(readFileSync18(configPath, "utf8"));
4470
5819
  if (content?.mcpServers?.elevate) {
4471
5820
  env.mcpConfigured = true;
4472
5821
  }
@@ -4485,23 +5834,23 @@ function detectClaudeCode(cwd) {
4485
5834
  mcpConfigured: false,
4486
5835
  skillConfigured: false
4487
5836
  };
4488
- const mcpJsonPath = join17(cwd, ".mcp.json");
4489
- const claudeMdPath = join17(cwd, "CLAUDE.md");
4490
- if (existsSync13(mcpJsonPath) || existsSync13(claudeMdPath)) {
5837
+ const mcpJsonPath = join24(cwd, ".mcp.json");
5838
+ const claudeMdPath = join24(cwd, "CLAUDE.md");
5839
+ if (existsSync19(mcpJsonPath) || existsSync19(claudeMdPath)) {
4491
5840
  env.detected = true;
4492
- env.reason = existsSync13(mcpJsonPath) ? `Found .mcp.json in workspace` : `Found CLAUDE.md in workspace`;
5841
+ env.reason = existsSync19(mcpJsonPath) ? `Found .mcp.json in workspace` : `Found CLAUDE.md in workspace`;
4493
5842
  }
4494
- if (existsSync13(mcpJsonPath)) {
5843
+ if (existsSync19(mcpJsonPath)) {
4495
5844
  try {
4496
- const content = JSON.parse(readFileSync11(mcpJsonPath, "utf8"));
5845
+ const content = JSON.parse(readFileSync18(mcpJsonPath, "utf8"));
4497
5846
  if (content?.mcpServers?.elevate) {
4498
5847
  env.mcpConfigured = true;
4499
5848
  }
4500
5849
  } catch {
4501
5850
  }
4502
5851
  }
4503
- if (existsSync13(claudeMdPath)) {
4504
- const content = readFileSync11(claudeMdPath, "utf8");
5852
+ if (existsSync19(claudeMdPath)) {
5853
+ const content = readFileSync18(claudeMdPath, "utf8");
4505
5854
  if (content.includes("elevate") || content.includes("Elevate")) {
4506
5855
  env.skillConfigured = true;
4507
5856
  }
@@ -4518,20 +5867,20 @@ function detectAntigravity(cwd) {
4518
5867
  mcpConfigured: false,
4519
5868
  skillConfigured: false
4520
5869
  };
4521
- const globalGeminiDir = join17(home, ".gemini");
4522
- const workspaceAgentsDir = join17(cwd, ".agents");
4523
- if (existsSync13(globalGeminiDir) || existsSync13(workspaceAgentsDir)) {
5870
+ const globalGeminiDir = join24(home, ".gemini");
5871
+ const workspaceAgentsDir = join24(cwd, ".agents");
5872
+ if (existsSync19(globalGeminiDir) || existsSync19(workspaceAgentsDir)) {
4524
5873
  env.detected = true;
4525
- env.reason = existsSync13(workspaceAgentsDir) ? `Found .agents/ in workspace` : `Found ~/.gemini/ global config`;
5874
+ env.reason = existsSync19(workspaceAgentsDir) ? `Found .agents/ in workspace` : `Found ~/.gemini/ global config`;
4526
5875
  }
4527
5876
  const mcpPaths = [
4528
- join17(cwd, ".agents", "mcp_config.json"),
4529
- join17(home, ".gemini", "config", "mcp_config.json")
5877
+ join24(cwd, ".agents", "mcp_config.json"),
5878
+ join24(home, ".gemini", "config", "mcp_config.json")
4530
5879
  ];
4531
5880
  for (const p of mcpPaths) {
4532
- if (existsSync13(p)) {
5881
+ if (existsSync19(p)) {
4533
5882
  try {
4534
- const content = JSON.parse(readFileSync11(p, "utf8"));
5883
+ const content = JSON.parse(readFileSync18(p, "utf8"));
4535
5884
  if (content?.mcpServers?.elevate) {
4536
5885
  env.mcpConfigured = true;
4537
5886
  break;
@@ -4552,24 +5901,24 @@ function detectVSCode(cwd) {
4552
5901
  mcpConfigured: false,
4553
5902
  skillConfigured: false
4554
5903
  };
4555
- const vscodeDir = join17(cwd, ".vscode");
4556
- if (existsSync13(vscodeDir)) {
5904
+ const vscodeDir = join24(cwd, ".vscode");
5905
+ if (existsSync19(vscodeDir)) {
4557
5906
  env.detected = true;
4558
5907
  env.reason = `Found .vscode/ in workspace`;
4559
5908
  }
4560
- const mcpPath = join17(cwd, ".vscode", "mcp.json");
4561
- if (existsSync13(mcpPath)) {
5909
+ const mcpPath = join24(cwd, ".vscode", "mcp.json");
5910
+ if (existsSync19(mcpPath)) {
4562
5911
  try {
4563
- const content = JSON.parse(readFileSync11(mcpPath, "utf8"));
5912
+ const content = JSON.parse(readFileSync18(mcpPath, "utf8"));
4564
5913
  if (content?.servers?.elevate) {
4565
5914
  env.mcpConfigured = true;
4566
5915
  }
4567
5916
  } catch {
4568
5917
  }
4569
5918
  }
4570
- const copilotInstructions = join17(cwd, ".github", "copilot-instructions.md");
4571
- if (existsSync13(copilotInstructions)) {
4572
- const content = readFileSync11(copilotInstructions, "utf8");
5919
+ const copilotInstructions = join24(cwd, ".github", "copilot-instructions.md");
5920
+ if (existsSync19(copilotInstructions)) {
5921
+ const content = readFileSync18(copilotInstructions, "utf8");
4573
5922
  if (content.includes("elevate") || content.includes("Elevate")) {
4574
5923
  env.skillConfigured = true;
4575
5924
  }
@@ -4586,14 +5935,14 @@ function detectWindsurf() {
4586
5935
  mcpConfigured: false,
4587
5936
  skillConfigured: false
4588
5937
  };
4589
- const configPath = isWin ? join17(appData(), "Codeium", "Windsurf", "mcp_config.json") : join17(home, ".codeium", "windsurf", "mcp_config.json");
4590
- const altPath = join17(home, ".codeium", "windsurf", "mcp_config.json");
4591
- if (existsSync13(configPath) || existsSync13(altPath)) {
5938
+ const configPath = isWin ? join24(appData(), "Codeium", "Windsurf", "mcp_config.json") : join24(home, ".codeium", "windsurf", "mcp_config.json");
5939
+ const altPath = join24(home, ".codeium", "windsurf", "mcp_config.json");
5940
+ if (existsSync19(configPath) || existsSync19(altPath)) {
4592
5941
  env.detected = true;
4593
5942
  env.reason = `Found Windsurf config`;
4594
- const path = existsSync13(configPath) ? configPath : altPath;
5943
+ const path = existsSync19(configPath) ? configPath : altPath;
4595
5944
  try {
4596
- const content = JSON.parse(readFileSync11(path, "utf8"));
5945
+ const content = JSON.parse(readFileSync18(path, "utf8"));
4597
5946
  if (content?.mcpServers?.elevate) {
4598
5947
  env.mcpConfigured = true;
4599
5948
  }
@@ -4604,9 +5953,9 @@ function detectWindsurf() {
4604
5953
  }
4605
5954
  function mergeJsonMcpServer(filePath, serverKey, topLevelKey) {
4606
5955
  let existing = {};
4607
- if (existsSync13(filePath)) {
5956
+ if (existsSync19(filePath)) {
4608
5957
  try {
4609
- existing = JSON.parse(readFileSync11(filePath, "utf8"));
5958
+ existing = JSON.parse(readFileSync18(filePath, "utf8"));
4610
5959
  } catch {
4611
5960
  existing = {};
4612
5961
  }
@@ -4618,69 +5967,69 @@ function mergeJsonMcpServer(filePath, serverKey, topLevelKey) {
4618
5967
  servers[serverKey] = mcpServerEntry();
4619
5968
  existing[topLevelKey] = servers;
4620
5969
  const dir = filePath.substring(0, filePath.lastIndexOf(isWin ? "\\" : "/"));
4621
- if (dir && !existsSync13(dir)) {
5970
+ if (dir && !existsSync19(dir)) {
4622
5971
  mkdirSync2(dir, { recursive: true });
4623
5972
  }
4624
- writeFileSync5(filePath, JSON.stringify(existing, null, 2) + "\n", "utf8");
5973
+ writeFileSync6(filePath, JSON.stringify(existing, null, 2) + "\n", "utf8");
4625
5974
  return { written: true, message: `${filePath}: elevate MCP server registered` };
4626
5975
  }
4627
5976
  function appendSkillToFile(filePath, content, marker) {
4628
- if (existsSync13(filePath)) {
4629
- const existing = readFileSync11(filePath, "utf8");
5977
+ if (existsSync19(filePath)) {
5978
+ const existing = readFileSync18(filePath, "utf8");
4630
5979
  if (existing.includes(marker)) {
4631
5980
  return { written: false, message: `${filePath}: skill already present` };
4632
5981
  }
4633
- writeFileSync5(filePath, existing + "\n\n" + content, "utf8");
5982
+ writeFileSync6(filePath, existing + "\n\n" + content, "utf8");
4634
5983
  return { written: true, message: `${filePath}: skill appended` };
4635
5984
  }
4636
5985
  const dir = filePath.substring(0, filePath.lastIndexOf(isWin ? "\\" : "/"));
4637
- if (dir && !existsSync13(dir)) {
5986
+ if (dir && !existsSync19(dir)) {
4638
5987
  mkdirSync2(dir, { recursive: true });
4639
5988
  }
4640
- writeFileSync5(filePath, content, "utf8");
5989
+ writeFileSync6(filePath, content, "utf8");
4641
5990
  return { written: true, message: `${filePath}: skill created` };
4642
5991
  }
4643
5992
  function configureCursor(cwd, result) {
4644
- const mcpPath = join17(cwd, ".cursor", "mcp.json");
5993
+ const mcpPath = join24(cwd, ".cursor", "mcp.json");
4645
5994
  const mcp = mergeJsonMcpServer(mcpPath, "elevate", "mcpServers");
4646
5995
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Cursor MCP: ${mcp.message}`);
4647
- const rulesPath = join17(cwd, ".cursorrules");
5996
+ const rulesPath = join24(cwd, ".cursorrules");
4648
5997
  const skill = appendSkillToFile(rulesPath, ELEVATE_SKILL_CONTENT, "elevate_scan");
4649
5998
  result.actions.push(`${skill.written ? "\u2705" : "\u23ED\uFE0F"} Cursor Rules: ${skill.message}`);
4650
5999
  }
4651
6000
  function configureClaudeDesktop(result) {
4652
- const configPath = isWin ? join17(appData(), "Claude", "claude_desktop_config.json") : join17(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
6001
+ const configPath = isWin ? join24(appData(), "Claude", "claude_desktop_config.json") : join24(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
4653
6002
  const mcp = mergeJsonMcpServer(configPath, "elevate", "mcpServers");
4654
6003
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Claude Desktop MCP: ${mcp.message}`);
4655
6004
  }
4656
6005
  function configureClaudeCode(cwd, result) {
4657
- const mcpPath = join17(cwd, ".mcp.json");
6006
+ const mcpPath = join24(cwd, ".mcp.json");
4658
6007
  const mcp = mergeJsonMcpServer(mcpPath, "elevate", "mcpServers");
4659
6008
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Claude Code MCP: ${mcp.message}`);
4660
- const claudeMdPath = join17(cwd, "CLAUDE.md");
6009
+ const claudeMdPath = join24(cwd, "CLAUDE.md");
4661
6010
  const skill = appendSkillToFile(claudeMdPath, ELEVATE_SKILL_CONTENT, "elevate_scan");
4662
6011
  result.actions.push(`${skill.written ? "\u2705" : "\u23ED\uFE0F"} Claude Code Skill: ${skill.message}`);
4663
6012
  }
4664
6013
  function configureAntigravity(cwd, result) {
4665
- const mcpPath = join17(cwd, ".agents", "mcp_config.json");
6014
+ const mcpPath = join24(cwd, ".agents", "mcp_config.json");
4666
6015
  const mcp = mergeJsonMcpServer(mcpPath, "elevate", "mcpServers");
4667
6016
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Antigravity MCP: ${mcp.message}`);
4668
6017
  }
4669
6018
  function configureVSCode(cwd, result) {
4670
- const mcpPath = join17(cwd, ".vscode", "mcp.json");
6019
+ const mcpPath = join24(cwd, ".vscode", "mcp.json");
4671
6020
  const mcp = mergeJsonMcpServer(mcpPath, "elevate", "servers");
4672
6021
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} VS Code MCP: ${mcp.message}`);
4673
- const instructionsPath = join17(cwd, ".github", "copilot-instructions.md");
6022
+ const instructionsPath = join24(cwd, ".github", "copilot-instructions.md");
4674
6023
  const skill = appendSkillToFile(instructionsPath, ELEVATE_SKILL_CONTENT, "elevate_scan");
4675
6024
  result.actions.push(`${skill.written ? "\u2705" : "\u23ED\uFE0F"} Copilot Instructions: ${skill.message}`);
4676
6025
  }
4677
6026
  function configureWindsurf(result) {
4678
- const configPath = isWin ? join17(appData(), "Codeium", "Windsurf", "mcp_config.json") : join17(home, ".codeium", "windsurf", "mcp_config.json");
6027
+ const configPath = isWin ? join24(appData(), "Codeium", "Windsurf", "mcp_config.json") : join24(home, ".codeium", "windsurf", "mcp_config.json");
4679
6028
  const mcp = mergeJsonMcpServer(configPath, "elevate", "mcpServers");
4680
6029
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Windsurf MCP: ${mcp.message}`);
4681
6030
  }
4682
6031
  async function handleInitCommand(options) {
4683
- const cwd = resolve11(process.cwd());
6032
+ const cwd = resolve15(process.cwd());
4684
6033
  const result = {
4685
6034
  cwd,
4686
6035
  environments: [],
@@ -4708,7 +6057,7 @@ async function handleInitCommand(options) {
4708
6057
  console.log(` Creating portable .mcp.json for broad compatibility.
4709
6058
  `);
4710
6059
  }
4711
- const mcpPath = join17(cwd, ".mcp.json");
6060
+ const mcpPath = join24(cwd, ".mcp.json");
4712
6061
  const mcp = mergeJsonMcpServer(mcpPath, "elevate", "mcpServers");
4713
6062
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Generic MCP: ${mcp.message}`);
4714
6063
  } else {
@@ -4775,14 +6124,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4775
6124
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4776
6125
  var ECOSYSTEM_PROPERTY = {
4777
6126
  type: "string",
4778
- enum: ["npm", "maven"],
6127
+ enum: EcosystemFactory.getAvailableEcosystems(),
4779
6128
  description: "Target ecosystem (default: npm)"
4780
6129
  };
4781
6130
  async function startMcpServer(config) {
4782
6131
  const server = new Server(
4783
6132
  {
4784
6133
  name: "elevate-mcp",
4785
- version: "1.2.0"
6134
+ version: VERSION
4786
6135
  },
4787
6136
  {
4788
6137
  capabilities: {
@@ -4795,7 +6144,7 @@ async function startMcpServer(config) {
4795
6144
  tools: [
4796
6145
  {
4797
6146
  name: "elevate_discover_modules",
4798
- description: "Discovers all repository modules and workspaces for Node/npm (package.json) or Java/Maven (pom.xml).",
6147
+ description: "Discovers all repository modules and workspaces for Node/npm (package.json), Java/Maven (pom.xml) or Rust/Cargo (Cargo.toml).",
4799
6148
  inputSchema: {
4800
6149
  type: "object",
4801
6150
  properties: {
@@ -4835,7 +6184,7 @@ async function startMcpServer(config) {
4835
6184
  ecosystem: ECOSYSTEM_PROPERTY,
4836
6185
  identifier: {
4837
6186
  type: "string",
4838
- description: "Package identifier (npm: chalk, maven: org.slf4j:slf4j-api)"
6187
+ description: "Package identifier (npm: chalk, maven: org.slf4j:slf4j-api, cargo: serde)"
4839
6188
  }
4840
6189
  },
4841
6190
  required: ["identifier"]
@@ -4916,10 +6265,13 @@ async function startMcpServer(config) {
4916
6265
  };
4917
6266
  try {
4918
6267
  if (name === "elevate_discover_modules") {
4919
- const modules = await strategy.discovery.discover(config.rootDir);
6268
+ const { modules, diagnostics } = await discoverModules(strategy, config.rootDir);
6269
+ const packageManager = strategy.packageManagerFor?.(config.rootDir);
4920
6270
  return reply({
4921
6271
  ecosystem,
6272
+ ...packageManager ? { packageManager } : {},
4922
6273
  count: modules.length,
6274
+ diagnostics,
4923
6275
  modules: modules.map((m) => ({
4924
6276
  id: m.id,
4925
6277
  name: m.name,