@dennisrepa/elevate 1.4.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,8 +4,65 @@
4
4
  import { render } from "ink";
5
5
 
6
6
  // src/config.ts
7
- import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
8
- import { dirname as dirname8, join as join18, 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
+
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);
18
+ }
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 ?? ""}`;
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
65
+ };
9
66
 
10
67
  // src/adapters/npm/npm-package-manager.ts
11
68
  import { existsSync as existsSync2, readFileSync } from "fs";
@@ -37,7 +94,7 @@ var UnsafeArgumentError = class extends Error {
37
94
  var IS_WINDOWS = process.platform === "win32";
38
95
  var WINDOWS_UNSAFE = /["%!^&|<>\r\n]/;
39
96
  function runCommand(command, args, options) {
40
- return new Promise((resolve12, reject) => {
97
+ return new Promise((resolve16, reject) => {
41
98
  const spawnOptions = { cwd: options.cwd, windowsHide: true, env: process.env };
42
99
  let child2;
43
100
  if (IS_WINDOWS) {
@@ -67,19 +124,19 @@ function runCommand(command, args, options) {
67
124
  });
68
125
  child2.on("close", (code) => {
69
126
  if (timer) clearTimeout(timer);
70
- resolve12({ exitCode: code ?? 1, stdout, stderr, timedOut });
127
+ resolve16({ exitCode: code ?? 1, stdout, stderr, timedOut });
71
128
  });
72
129
  });
73
130
  }
74
131
  function runShellScript(script, options) {
75
- return new Promise((resolve12) => {
132
+ return new Promise((resolve16) => {
76
133
  exec(
77
134
  script,
78
135
  { cwd: options.cwd, timeout: options.timeoutMs, maxBuffer: 64 * 1024 * 1024, windowsHide: true },
79
136
  (error, stdout, stderr) => {
80
137
  const timedOut = Boolean(error?.killed);
81
138
  const exitCode = error ? typeof error.code === "number" ? error.code : 1 : 0;
82
- resolve12({ exitCode, stdout, stderr, timedOut });
139
+ resolve16({ exitCode, stdout, stderr, timedOut });
83
140
  }
84
141
  );
85
142
  });
@@ -304,7 +361,7 @@ var NpmModuleDiscoveryAdapter = class {
304
361
  };
305
362
 
306
363
  // src/adapters/npm/npm-registry.ts
307
- import semver from "semver";
364
+ import semver3 from "semver";
308
365
 
309
366
  // src/domain/origin.ts
310
367
  function buildWorkspaceIndex(modules) {
@@ -434,13 +491,13 @@ var NpmRegistryAdapter = class {
434
491
  if (!isValidPackageName(coordinate.identifier)) return null;
435
492
  if (channel === "stable") {
436
493
  const latest = await this.view(coordinate.identifier, "dist-tags.latest", cwd);
437
- if (typeof latest === "string" && semver.valid(latest) && semver.prerelease(latest) === null) {
494
+ if (typeof latest === "string" && semver3.valid(latest) && semver3.prerelease(latest) === null) {
438
495
  return latest;
439
496
  }
440
497
  const releases = (await this.getAllVersions(coordinate, cwd)).filter(
441
- (v) => semver.valid(v) && semver.prerelease(v) === null
498
+ (v) => semver3.valid(v) && semver3.prerelease(v) === null
442
499
  );
443
- return releases.length > 0 ? semver.rsort(releases)[0] : null;
500
+ return releases.length > 0 ? semver3.rsort(releases)[0] : null;
444
501
  }
445
502
  const version = await this.view(coordinate.identifier, "version", cwd);
446
503
  return typeof version === "string" && version ? version : null;
@@ -470,42 +527,7 @@ var NpmRegistryAdapter = class {
470
527
  // src/adapters/npm/npm-scanner.ts
471
528
  import { readFileSync as readFileSync4 } from "fs";
472
529
  import { join as join5 } from "path";
473
- import semver3 from "semver";
474
-
475
- // src/domain/versions.ts
476
- import semver2 from "semver";
477
- var PRE_RELEASE = /(?:^|[.\-_+]|(?<=\d))(alpha|beta|rc|cr|milestone|preview|snapshot|ea|dev|pre|(?:a|b|m)(?=\d))(?=$|[.\-_+]|\d)/i;
478
- function isPreReleaseVersion(version) {
479
- return PRE_RELEASE.test(version);
480
- }
481
- function extractPreReleaseTag(version) {
482
- const match = version.match(PRE_RELEASE);
483
- if (!match) return void 0;
484
- const token = match[1].toLowerCase();
485
- if (token === "a") return "ALPHA";
486
- if (token === "b") return "BETA";
487
- if (token === "m") {
488
- const number = version.slice((match.index ?? 0) + match[0].length).match(/^\d+/)?.[0];
489
- return `M${number ?? ""}`;
490
- }
491
- return token.toUpperCase();
492
- }
493
- function cleanJavaVersion(version) {
494
- const match = version.match(/^v?(\d+(?:\.\d+){0,2})/);
495
- if (!match) return null;
496
- return semver2.coerce(match[1])?.version ?? null;
497
- }
498
- function diffVersions(from, to) {
499
- const kind = semver2.diff(from, to);
500
- if (kind === "major" || kind === "premajor") return "major";
501
- if (kind === "minor" || kind === "preminor") return "minor";
502
- return "patch";
503
- }
504
- function rangePrefix(range) {
505
- if (range.startsWith("^")) return "^";
506
- if (range.startsWith("~")) return "~";
507
- return "";
508
- }
530
+ import semver4 from "semver";
509
531
 
510
532
  // src/adapters/shared/pool.ts
511
533
  async function mapPooled(items, concurrency, fn) {
@@ -636,9 +658,9 @@ var NpmDependencyAdapter = class {
636
658
  if (!latest) {
637
659
  return { skipped: { identifier: dep.name, origin: origin.kind, reason: "lookup-failed" } };
638
660
  }
639
- const currentClean = semver3.minVersion(dep.range)?.version;
640
- if (!currentClean || !semver3.valid(latest) || !semver3.gt(latest, currentClean)) return null;
641
- 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;
642
664
  const diff = diffVersions(currentClean, latest);
643
665
  return {
644
666
  candidate: {
@@ -673,18 +695,18 @@ function declaredDependencies(pkg) {
673
695
  function isUpgradableRange(range) {
674
696
  const trimmed = range.trim();
675
697
  if (trimmed === "" || trimmed === "*" || trimmed.toLowerCase() === "x") return false;
676
- return semver3.validRange(trimmed) !== null;
698
+ return semver4.validRange(trimmed) !== null;
677
699
  }
678
700
  function alignmentCandidate(dep, target, origin, pnpm = false) {
679
701
  const split = pnpm ? splitRangeProtocol(dep.range) : { protocol: void 0, rest: dep.range };
680
702
  const range = split.protocol === "workspace" ? split.rest : dep.range;
681
703
  const protocol = split.protocol === "workspace" ? "workspace:" : "";
682
704
  const local = target.version;
683
- if (!local || !semver3.valid(local) || !isUpgradableRange(range)) return null;
684
- if (semver3.satisfies(local, range, { includePrerelease: true })) return null;
685
- const currentClean = semver3.minVersion(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";
686
708
  const diff = diffVersions(currentClean, local);
687
- const tag = semver3.prerelease(local) ? extractPreReleaseTag(local) ?? "PRE" : void 0;
709
+ const tag = semver4.prerelease(local) ? extractPreReleaseTag(local) ?? "PRE" : void 0;
688
710
  return {
689
711
  coordinate: { identifier: dep.name, artifact: dep.name, ecosystem: "npm" },
690
712
  currentRange: dep.range,
@@ -1035,6 +1057,7 @@ var NpmEcosystemStrategy = class {
1035
1057
  }
1036
1058
  icon = "\u{1F4E6}";
1037
1059
  manifestFile = "package.json";
1060
+ versioning = npmVersionPolicy;
1038
1061
  discovery = new NpmModuleDiscoveryAdapter();
1039
1062
  registry = new NpmRegistryAdapter();
1040
1063
  reader = new NpmDependencyAdapter(this.registry, this.registry.config);
@@ -1049,17 +1072,749 @@ var NpmEcosystemStrategy = class {
1049
1072
  }
1050
1073
  };
1051
1074
 
1052
- // src/adapters/maven/maven-discovery.ts
1053
- import { existsSync as existsSync7 } from "fs";
1054
- import { dirname as dirname5, join as join11, relative as relative3, resolve as resolve6 } from "path";
1075
+ // src/adapters/cargo/cargo-strategy.ts
1076
+ import semver10 from "semver";
1055
1077
 
1056
- // src/adapters/maven/maven-project.ts
1057
- import { existsSync as existsSync6, readdirSync, statSync as statSync2 } from "fs";
1058
- import { join as join10, resolve as resolve5 } from "path";
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";
1059
1081
 
1060
- // src/adapters/maven/maven-pom.ts
1061
- import { existsSync as existsSync5, readFileSync as readFileSync8, statSync } from "fs";
1062
- import { dirname as dirname4, join as join9, resolve as resolve4 } from "path";
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` } };
1682
+ }
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) } : {}
1733
+ };
1734
+ return { candidate };
1735
+ }
1736
+ };
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;
1790
+ return {
1791
+ version: shared.version,
1792
+ inherited: true,
1793
+ declaration: { file: rootManifest, kind: "dependency-management", displayPath: display(repoRoot, rootManifest) }
1794
+ };
1795
+ }
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
+ }
1813
+
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";
1063
1818
 
1064
1819
  // src/adapters/shared/xml.ts
1065
1820
  import { SaxesParser } from "saxes";
@@ -1192,9 +1947,226 @@ function localName(qualified) {
1192
1947
  return colon >= 0 ? qualified.slice(colon + 1) : qualified;
1193
1948
  }
1194
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
+
1195
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";
1196
2168
  function readPom(file) {
1197
- const source = readFileSync8(file, "utf8");
2169
+ const source = readFileSync13(file, "utf8");
1198
2170
  return parsePom(source, file);
1199
2171
  }
1200
2172
  function parsePom(source, file) {
@@ -1213,7 +2185,7 @@ function parsePom(source, file) {
1213
2185
  }
1214
2186
  return {
1215
2187
  file,
1216
- dir: dirname4(file),
2188
+ dir: dirname6(file),
1217
2189
  groupId: childText(project, "groupId"),
1218
2190
  artifactId: childText(project, "artifactId") ?? "unknown",
1219
2191
  version: childText(project, "version"),
@@ -1242,9 +2214,9 @@ function parentPomFile(pom) {
1242
2214
  if (!pom.parent) return void 0;
1243
2215
  const relativePath = pom.parent.relativePath ?? "../pom.xml";
1244
2216
  if (relativePath === "") return void 0;
1245
- let candidate = resolve4(pom.dir, relativePath);
1246
- if (existsSync5(candidate) && statSync(candidate).isDirectory()) candidate = join9(candidate, "pom.xml");
1247
- return existsSync5(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;
1248
2220
  }
1249
2221
  function builtinProperty(name, module) {
1250
2222
  switch (name) {
@@ -1319,9 +2291,9 @@ var MavenProject = class _MavenProject {
1319
2291
  }
1320
2292
  poms.push(pom);
1321
2293
  for (const entry of pom.modules) {
1322
- const target = resolve5(pom.dir, entry);
1323
- const moduleFile = target.endsWith(".xml") ? target : join10(target, "pom.xml");
1324
- if (!seen.has(moduleFile) && existsSync6(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)) {
1325
2297
  seen.add(moduleFile);
1326
2298
  queue.push(moduleFile);
1327
2299
  }
@@ -1330,7 +2302,7 @@ var MavenProject = class _MavenProject {
1330
2302
  return new _MavenProject(rootDir, poms);
1331
2303
  }
1332
2304
  pomAt(file) {
1333
- return this.byFile.get(resolve5(file));
2305
+ return this.byFile.get(resolve9(file));
1334
2306
  }
1335
2307
  /** The project followed by its local parents, stopping at the first external parent. */
1336
2308
  chainOf(pom) {
@@ -1373,7 +2345,7 @@ var MavenProject = class _MavenProject {
1373
2345
  return groupId === parent.groupId && candidate.artifactId === parent.artifactId && (version.includes("${") || expected.includes("${") || version === expected);
1374
2346
  };
1375
2347
  const file = parentPomFile(pom);
1376
- const byPath = file ? this.byFile.get(resolve5(file)) : void 0;
2348
+ const byPath = file ? this.byFile.get(resolve9(file)) : void 0;
1377
2349
  return matches(byPath) ? byPath : void 0;
1378
2350
  }
1379
2351
  /**
@@ -1390,8 +2362,8 @@ var MavenProject = class _MavenProject {
1390
2362
  if (this.aggregators.has(pom.file)) continue;
1391
2363
  this.aggregators.set(pom.file, aggregator.dir);
1392
2364
  for (const entry of pom.modules) {
1393
- const target = resolve5(pom.dir, entry);
1394
- const file = target.endsWith(".xml") ? target : join10(target, "pom.xml");
2365
+ const target = resolve9(pom.dir, entry);
2366
+ const file = target.endsWith(".xml") ? target : join15(target, "pom.xml");
1395
2367
  const modulePom = this.byFile.get(file);
1396
2368
  if (modulePom) queue.push(modulePom);
1397
2369
  }
@@ -1401,23 +2373,23 @@ var MavenProject = class _MavenProject {
1401
2373
  };
1402
2374
  function findPomFiles(rootDir) {
1403
2375
  const results = [];
1404
- const rootPom = join10(rootDir, "pom.xml");
1405
- if (existsSync6(rootPom)) results.push(resolve5(rootPom));
2376
+ const rootPom = join15(rootDir, "pom.xml");
2377
+ if (existsSync11(rootPom)) results.push(resolve9(rootPom));
1406
2378
  const walk = (dir, depth) => {
1407
2379
  if (depth > MAX_DEPTH) return;
1408
2380
  let entries2;
1409
2381
  try {
1410
- entries2 = readdirSync(dir, { withFileTypes: true });
2382
+ entries2 = readdirSync2(dir, { withFileTypes: true });
1411
2383
  } catch {
1412
2384
  return;
1413
2385
  }
1414
- const isProject = existsSync6(join10(dir, "pom.xml"));
2386
+ const isProject = existsSync11(join15(dir, "pom.xml"));
1415
2387
  for (const entry of entries2) {
1416
2388
  if (!entry.isDirectory() || entry.name.startsWith(".") || SKIPPED_DIRS.has(entry.name)) continue;
1417
2389
  if (isProject && entry.name === "src") continue;
1418
- const subDir = join10(dir, entry.name);
1419
- const pom = join10(subDir, "pom.xml");
1420
- if (existsSync6(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));
1421
2393
  walk(subDir, depth + 1);
1422
2394
  }
1423
2395
  };
@@ -1432,9 +2404,9 @@ var MavenModuleDiscoveryAdapter = class {
1432
2404
  * the POM above until there is none: the top of the connected POM chain.
1433
2405
  */
1434
2406
  findRoot(startDir, boundaryDir) {
1435
- let dir = resolve6(startDir);
1436
- if (!existsSync7(join11(dir, "pom.xml"))) return void 0;
1437
- 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;
1438
2410
  for (; ; ) {
1439
2411
  const above = pomAbove(dir, boundary);
1440
2412
  if (!above) return dir;
@@ -1445,7 +2417,7 @@ var MavenModuleDiscoveryAdapter = class {
1445
2417
  const project = MavenProject.load(rootDir);
1446
2418
  const modules = project.poms.map((pom) => {
1447
2419
  const coordinates = project.coordinatesOf(pom);
1448
- const relPath = relative3(rootDir, pom.dir).replace(/\\/g, "/");
2420
+ const relPath = relative6(rootDir, pom.dir).replace(/\\/g, "/");
1449
2421
  return {
1450
2422
  id: coordinates.groupId ? `${coordinates.groupId}:${coordinates.artifactId}` : coordinates.artifactId,
1451
2423
  name: pom.name ? interpolate(pom.name, project.chainOf(pom).poms) : pom.artifactId,
@@ -1472,7 +2444,7 @@ function ancestorsWithin(dir, boundary) {
1472
2444
  const result = [];
1473
2445
  let current = dir;
1474
2446
  while (current !== boundary) {
1475
- const parent = dirname5(current);
2447
+ const parent = dirname7(current);
1476
2448
  if (parent === current) break;
1477
2449
  result.push(parent);
1478
2450
  current = parent;
@@ -1480,22 +2452,22 @@ function ancestorsWithin(dir, boundary) {
1480
2452
  return result;
1481
2453
  }
1482
2454
  function pomAbove(dir, boundary) {
1483
- const pom = tryReadPom(join11(dir, "pom.xml"));
2455
+ const pom = tryReadPom(join16(dir, "pom.xml"));
1484
2456
  if (!pom) return void 0;
1485
2457
  const ancestors = ancestorsWithin(dir, boundary);
1486
2458
  const found = [];
1487
2459
  const parentFile = parentPomFile(pom);
1488
- if (parentFile && pom.parent && ancestors.includes(dirname5(parentFile))) {
2460
+ if (parentFile && pom.parent && ancestors.includes(dirname7(parentFile))) {
1489
2461
  const parent = tryReadPom(parentFile);
1490
2462
  const groupId = parent?.groupId ?? parent?.parent?.groupId;
1491
2463
  if (parent && parent.artifactId === pom.parent.artifactId && groupId === pom.parent.groupId) {
1492
- found.push(dirname5(parentFile));
2464
+ found.push(dirname7(parentFile));
1493
2465
  }
1494
2466
  }
1495
- const ownFile = join11(dir, "pom.xml");
2467
+ const ownFile = join16(dir, "pom.xml");
1496
2468
  const aggregator = ancestors.find(
1497
- (ancestor) => tryReadPom(join11(ancestor, "pom.xml"))?.modules.some((entry) => {
1498
- const target = resolve6(ancestor, entry);
2469
+ (ancestor) => tryReadPom(join16(ancestor, "pom.xml"))?.modules.some((entry) => {
2470
+ const target = resolve10(ancestor, entry);
1499
2471
  return target === dir || target === ownFile;
1500
2472
  })
1501
2473
  );
@@ -1504,12 +2476,12 @@ function pomAbove(dir, boundary) {
1504
2476
  }
1505
2477
 
1506
2478
  // src/adapters/maven/maven-registry.ts
1507
- import { existsSync as existsSync10 } from "fs";
1508
- import { join as join14 } from "path";
2479
+ import { existsSync as existsSync15 } from "fs";
2480
+ import { join as join19 } from "path";
1509
2481
 
1510
2482
  // src/adapters/maven/maven-command.ts
1511
- import { existsSync as existsSync8 } from "fs";
1512
- import { dirname as dirname6, isAbsolute, join as join12, 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";
1513
2485
  var MavenUnavailableError = class extends Error {
1514
2486
  constructor() {
1515
2487
  super("Maven is not available: no Maven Wrapper (mvnw) in the project and no `mvn` on the PATH.");
@@ -1529,27 +2501,27 @@ var WRAPPER = IS_WINDOWS2 ? "mvnw.cmd" : "mvnw";
1529
2501
  var BATCH_ARGS = ["-B", "--no-transfer-progress"];
1530
2502
  var mvnOnPath;
1531
2503
  function findMavenWrapper(fromDir, rootDir) {
1532
- let dir = resolve7(fromDir);
1533
- const root = resolve7(rootDir);
2504
+ let dir = resolve11(fromDir);
2505
+ const root = resolve11(rootDir);
1534
2506
  for (; ; ) {
1535
- const candidate = join12(dir, WRAPPER);
1536
- if (existsSync8(candidate)) return candidate;
1537
- if (dir === root || relative4(root, dir).startsWith("..")) return void 0;
1538
- 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);
1539
2511
  if (parent === dir) return void 0;
1540
2512
  dir = parent;
1541
2513
  }
1542
2514
  }
1543
2515
  function relativeToCwd(cwd, file) {
1544
- const path = relative4(cwd, file);
1545
- return isAbsolute(path) ? file : path || ".";
2516
+ const path = relative7(cwd, file);
2517
+ return isAbsolute2(path) ? file : path || ".";
1546
2518
  }
1547
2519
  async function runMaven(args, options) {
1548
2520
  const wrapper = findMavenWrapper(options.cwd, options.rootDir);
1549
2521
  const run = { cwd: options.cwd, timeoutMs: options.timeoutMs };
1550
2522
  if (wrapper) {
1551
2523
  const relativeWrapper = relativeToCwd(options.cwd, wrapper);
1552
- const path = isAbsolute(relativeWrapper) || relativeWrapper.includes(sep) ? relativeWrapper : `.${sep}${relativeWrapper}`;
2524
+ const path = isAbsolute2(relativeWrapper) || relativeWrapper.includes(sep) ? relativeWrapper : `.${sep}${relativeWrapper}`;
1553
2525
  return IS_WINDOWS2 ? runCommand(path, [...BATCH_ARGS, ...args], run) : runCommand("sh", [path, ...BATCH_ARGS, ...args], run);
1554
2526
  }
1555
2527
  mvnOnPath ??= runCommand("mvn", ["-v"], { cwd: options.cwd, timeoutMs: 3e4 }).then(
@@ -1566,8 +2538,8 @@ function mavenErrors(output) {
1566
2538
 
1567
2539
  // src/adapters/maven/maven-resolution.ts
1568
2540
  import { randomUUID } from "crypto";
1569
- import { existsSync as existsSync9, mkdirSync, readFileSync as readFileSync9, readdirSync as readdirSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
1570
- import { join as join13 } 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";
1571
2543
  var DEFAULT_MAVEN_PLUGINS = { help: "3.5.2", versions: "2.22.0" };
1572
2544
  var HELP_PLUGIN_ID = "org.apache.maven.plugins:maven-help-plugin";
1573
2545
  var VERSIONS_PLUGIN_ID = "org.codehaus.mojo:versions-maven-plugin";
@@ -1599,21 +2571,21 @@ var ScratchDirectory = class {
1599
2571
  target;
1600
2572
  createdTarget;
1601
2573
  constructor(moduleDir) {
1602
- this.target = join13(moduleDir, "target");
1603
- this.createdTarget = !existsSync9(this.target);
1604
- this.path = join13(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)}`);
1605
2577
  mkdirSync(this.path, { recursive: true });
1606
2578
  }
1607
2579
  dispose() {
1608
2580
  rmSync(this.path, { recursive: true, force: true });
1609
2581
  try {
1610
- 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 });
1611
2583
  } catch {
1612
2584
  }
1613
2585
  }
1614
2586
  };
1615
2587
  async function readEffectivePoms(pomFile, moduleDir, rootDir, scratch, plugins, recursive = false) {
1616
- const output = join13(scratch.path, "effective-pom.xml");
2588
+ const output = join18(scratch.path, "effective-pom.xml");
1617
2589
  const args = [
1618
2590
  "-f",
1619
2591
  relativeToCwd(moduleDir, pomFile),
@@ -1625,7 +2597,7 @@ async function readEffectivePoms(pomFile, moduleDir, rootDir, scratch, plugins,
1625
2597
  rootDir,
1626
2598
  timeoutMs: MAVEN_TIMEOUT_MS
1627
2599
  });
1628
- if (result.exitCode !== 0 || !existsSync9(output)) {
2600
+ if (result.exitCode !== 0 || !existsSync14(output)) {
1629
2601
  const text = `${result.stdout}
1630
2602
  ${result.stderr}`;
1631
2603
  if (isPluginUnavailable(text, HELP_PLUGIN_ID)) {
@@ -1637,7 +2609,7 @@ ${result.stderr}`;
1637
2609
  throw new MavenCommandError(`Maven could not build the effective POM:
1638
2610
  ${mavenErrors(text)}`, text);
1639
2611
  }
1640
- return parseEffectivePoms(readFileSync9(output, "utf8"));
2612
+ return parseEffectivePoms(readFileSync14(output, "utf8"));
1641
2613
  }
1642
2614
  function parseEffectivePoms(source) {
1643
2615
  const root = parseXml(source);
@@ -1649,7 +2621,7 @@ function parseEffectivePoms(source) {
1649
2621
  return models;
1650
2622
  }
1651
2623
  function mergeRepositories(models) {
1652
- const merge = (name, pick) => {
2624
+ const merge2 = (name, pick) => {
1653
2625
  const byId = /* @__PURE__ */ new Map();
1654
2626
  for (const element of models.map(pick)) {
1655
2627
  for (const repository of element?.children ?? []) {
@@ -1662,8 +2634,8 @@ function mergeRepositories(models) {
1662
2634
  return {
1663
2635
  dependencies: [],
1664
2636
  managedDependencies: [],
1665
- repositories: merge("repositories", (m) => m.repositories),
1666
- pluginRepositories: merge("pluginRepositories", (m) => m.pluginRepositories)
2637
+ repositories: merge2("repositories", (m) => m.repositories),
2638
+ pluginRepositories: merge2("pluginRepositories", (m) => m.pluginRepositories)
1667
2639
  };
1668
2640
  }
1669
2641
  function toEffectiveModel(project) {
@@ -1721,10 +2693,10 @@ function buildProbePom(artifacts, model) {
1721
2693
  }
1722
2694
  async function queryNewerVersions(artifacts, model, moduleDir, rootDir, scratch, plugins) {
1723
2695
  if (artifacts.length === 0) return { newerVersions: /* @__PURE__ */ new Map(), failedLookups: /* @__PURE__ */ new Set() };
1724
- const probeDir = join13(scratch.path, "probe");
2696
+ const probeDir = join18(scratch.path, "probe");
1725
2697
  mkdirSync(probeDir, { recursive: true });
1726
- const probePom = join13(probeDir, "pom.xml");
1727
- writeFileSync2(probePom, buildProbePom(artifacts, model), "utf8");
2698
+ const probePom = join18(probeDir, "pom.xml");
2699
+ writeFileSync3(probePom, buildProbePom(artifacts, model), "utf8");
1728
2700
  const result = await runMaven(
1729
2701
  [
1730
2702
  "-N",
@@ -1738,8 +2710,8 @@ async function queryNewerVersions(artifacts, model, moduleDir, rootDir, scratch,
1738
2710
  );
1739
2711
  const log = `${result.stdout}
1740
2712
  ${result.stderr}`;
1741
- const reportFile = join13(probeDir, "target", "dependency-updates-report.xml");
1742
- if (result.exitCode !== 0 || !existsSync9(reportFile)) {
2713
+ const reportFile = join18(probeDir, "target", "dependency-updates-report.xml");
2714
+ if (result.exitCode !== 0 || !existsSync14(reportFile)) {
1743
2715
  if (isPluginUnavailable(log, VERSIONS_PLUGIN_ID)) {
1744
2716
  throw new MavenPluginUnavailableError(
1745
2717
  { id: VERSIONS_PLUGIN_ID, version: plugins.versions, key: "versions", purpose: "look up newer versions" },
@@ -1750,7 +2722,7 @@ ${result.stderr}`;
1750
2722
  ${mavenErrors(log)}`, log);
1751
2723
  }
1752
2724
  return {
1753
- newerVersions: parseUpdatesReport(readFileSync9(reportFile, "utf8")),
2725
+ newerVersions: parseUpdatesReport(readFileSync14(reportFile, "utf8")),
1754
2726
  failedLookups: parseFailedLookups(log)
1755
2727
  };
1756
2728
  }
@@ -1835,18 +2807,18 @@ var MavenRegistryAdapter = class {
1835
2807
  }
1836
2808
  };
1837
2809
  async function projectRepositories(cwd, scratch, plugins) {
1838
- const rootPom = join14(cwd, "pom.xml");
1839
- if (!existsSync10(rootPom)) return NO_REPOSITORIES;
2810
+ const rootPom = join19(cwd, "pom.xml");
2811
+ if (!existsSync15(rootPom)) return NO_REPOSITORIES;
1840
2812
  const models = await readEffectivePoms(rootPom, cwd, cwd, scratch, plugins);
1841
2813
  return [...models.values()][0] ?? NO_REPOSITORIES;
1842
2814
  }
1843
2815
 
1844
2816
  // src/adapters/maven/maven-scanner.ts
1845
- import { join as join15 } from "path";
1846
- import semver4 from "semver";
2817
+ import { join as join20 } from "path";
2818
+ import semver11 from "semver";
1847
2819
 
1848
2820
  // src/adapters/maven/maven-locator.ts
1849
- import { relative as relative5 } from "path";
2821
+ import { relative as relative8 } from "path";
1850
2822
  function locateDependencyVersion(chain, target, rootDir) {
1851
2823
  for (const pom of chain) {
1852
2824
  const dependency = findDependency(pom.dependencies, target, chain);
@@ -1927,13 +2899,13 @@ function resolveLiteral(pom, kind, versionElement, chain, target, rootDir) {
1927
2899
  file,
1928
2900
  kind: propertyName ? "property" : kind,
1929
2901
  propertyName,
1930
- displayPath: relative5(rootDir, file).replace(/\\/g, "/")
2902
+ displayPath: relative8(rootDir, file).replace(/\\/g, "/")
1931
2903
  };
1932
2904
  return { found: true, declaration, element, value };
1933
2905
  }
1934
2906
 
1935
2907
  // src/adapters/maven/maven-scanner.ts
1936
- var DIFF_ORDER2 = { major: 0, minor: 1, patch: 2 };
2908
+ var DIFF_ORDER3 = { major: 0, minor: 1, patch: 2 };
1937
2909
  var MavenDependencyAdapter = class {
1938
2910
  constructor(plugins = DEFAULT_MAVEN_PLUGINS) {
1939
2911
  this.plugins = plugins;
@@ -1987,14 +2959,14 @@ function groupModules(project, modules) {
1987
2959
  if (module.aggregatorDir) reactorSizes.set(module.aggregatorDir, (reactorSizes.get(module.aggregatorDir) ?? 0) + 1);
1988
2960
  }
1989
2961
  for (const module of modules) {
1990
- const pom = project.pomAt(join15(module.path, "pom.xml"));
2962
+ const pom = project.pomAt(join20(module.path, "pom.xml"));
1991
2963
  if (!pom) continue;
1992
2964
  const reactor = module.aggregatorDir;
1993
2965
  const batched = reactor !== void 0 && (reactorSizes.get(reactor) ?? 0) > 1;
1994
2966
  const key = batched ? `reactor:${reactor}` : `module:${module.path}`;
1995
2967
  const group = groups.get(key) ?? {
1996
2968
  dir: batched ? reactor : module.path,
1997
- aggregatorPom: batched ? join15(reactor, "pom.xml") : void 0,
2969
+ aggregatorPom: batched ? join20(reactor, "pom.xml") : void 0,
1998
2970
  members: []
1999
2971
  };
2000
2972
  group.members.push({ module, pom });
@@ -2083,7 +3055,7 @@ function finishModule(plan, report, channel) {
2083
3055
  collect2(updateCandidate(subject, latest, newer, origin), candidates, skipped);
2084
3056
  }
2085
3057
  markSharedDeclarations(candidates);
2086
- 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));
2087
3059
  return { candidates, skipped };
2088
3060
  }
2089
3061
  function newerThan(current, ascending) {
@@ -2114,7 +3086,7 @@ function buildCandidate(subject, target, origin, action) {
2114
3086
  }
2115
3087
  const currentClean = cleanJavaVersion(subject.version) ?? "0.0.0";
2116
3088
  const targetClean = cleanJavaVersion(target) ?? currentClean;
2117
- const diff = semver4.eq(currentClean, targetClean) ? "patch" : diffVersions(currentClean, targetClean);
3089
+ const diff = semver11.eq(currentClean, targetClean) ? "patch" : diffVersions(currentClean, targetClean);
2118
3090
  const isPreRelease = isPreReleaseVersion(target);
2119
3091
  return {
2120
3092
  candidate: {
@@ -2162,11 +3134,11 @@ function markSharedDeclarations(candidates) {
2162
3134
  }
2163
3135
 
2164
3136
  // src/adapters/maven/maven-updater.ts
2165
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync3 } from "fs";
2166
- import { join as join16, 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";
2167
3139
  var MavenUpdaterAdapter = class {
2168
3140
  affectedFiles(module, _rootDir, updates) {
2169
- const files = new Set(updates.map((u) => resolve8(u.declaration?.file ?? join16(module.path, "pom.xml"))));
3141
+ const files = new Set(updates.map((u) => resolve12(u.declaration?.file ?? join21(module.path, "pom.xml"))));
2170
3142
  return [...files];
2171
3143
  }
2172
3144
  async applyUpdates(module, rootDir, updates, onProgress) {
@@ -2176,13 +3148,13 @@ var MavenUpdaterAdapter = class {
2176
3148
  if (!update.declaration) {
2177
3149
  throw new InstallError(`No version declaration known for ${update.coordinate.identifier}; rescan the module.`);
2178
3150
  }
2179
- const file = resolve8(update.declaration.file);
3151
+ const file = resolve12(update.declaration.file);
2180
3152
  byFile.set(file, [...byFile.get(file) ?? [], update]);
2181
3153
  }
2182
3154
  const writes = [];
2183
3155
  for (const [file, fileUpdates] of byFile) {
2184
3156
  onProgress(`Preparing ${fileUpdates.length} change(s) in ${fileUpdates[0].declaration.displayPath}\u2026`);
2185
- const source = readFileSync10(file, "utf8");
3157
+ const source = readFileSync15(file, "utf8");
2186
3158
  const pom = parsePom(source, file);
2187
3159
  const chain = project.pomAt(file) ? project.chainOf(project.pomAt(file)).poms : [pom];
2188
3160
  const edits = collectEdits(pom, [pom, ...chain.slice(1)], fileUpdates);
@@ -2194,7 +3166,7 @@ var MavenUpdaterAdapter = class {
2194
3166
  }
2195
3167
  }
2196
3168
  onProgress("Writing POM files\u2026");
2197
- for (const write of writes) writeFileSync3(write.file, write.content, "utf8");
3169
+ for (const write of writes) writeFileSync4(write.file, write.content, "utf8");
2198
3170
  return {
2199
3171
  updatedCount: updates.length,
2200
3172
  auditMessage: `Updated ${updates.length} version declaration(s) in ${writes.length} POM file(s).`,
@@ -2252,13 +3224,13 @@ function describe(declaration) {
2252
3224
  }
2253
3225
 
2254
3226
  // src/adapters/maven/maven-verifier.ts
2255
- import { dirname as dirname7, join as join17, relative as relative6, resolve as resolve9 } from "path";
2256
- 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;
2257
3229
  var MavenVerificationAdapter = class {
2258
3230
  async verify(module, rootDir, options = {}) {
2259
3231
  if (options.customScript) {
2260
3232
  const label2 = options.customLabel || options.customScript;
2261
- const result = await runShellScript(options.customScript, { cwd: module.path, timeoutMs: VERIFY_TIMEOUT_MS2 });
3233
+ const result = await runShellScript(options.customScript, { cwd: module.path, timeoutMs: VERIFY_TIMEOUT_MS3 });
2262
3234
  return result.exitCode === 0 ? { status: "clean", details: "Verification script succeeded.", label: label2 } : { status: "warn", details: failureDetails2(result), label: label2 };
2263
3235
  }
2264
3236
  const plan = planBuild(module, rootDir, options.changedFiles ?? []);
@@ -2267,7 +3239,7 @@ var MavenVerificationAdapter = class {
2267
3239
  const result = await runMaven(["-q", "test-compile", ...plan.args], {
2268
3240
  cwd: plan.cwd,
2269
3241
  rootDir,
2270
- timeoutMs: VERIFY_TIMEOUT_MS2
3242
+ timeoutMs: VERIFY_TIMEOUT_MS3
2271
3243
  });
2272
3244
  if (result.exitCode === 0) {
2273
3245
  return { status: "clean", details: "All affected modules compile, including tests.", label };
@@ -2289,15 +3261,15 @@ function planBuild(module, rootDir, changedFiles) {
2289
3261
  const aggregatorDir = module.aggregatorDir;
2290
3262
  if (!aggregatorDir) return { cwd: module.path, args: [] };
2291
3263
  const project = MavenProject.load(rootDir);
2292
- const files = changedFiles.length > 0 ? changedFiles : [join17(module.path, "pom.xml")];
3264
+ const files = changedFiles.length > 0 ? changedFiles : [join22(module.path, "pom.xml")];
2293
3265
  const selected = /* @__PURE__ */ new Set();
2294
3266
  for (const file of files) {
2295
3267
  const pom = project.pomAt(file);
2296
- const dir = resolve9(dirname7(file));
2297
- 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)) {
2298
3270
  return { cwd: aggregatorDir, args: [] };
2299
3271
  }
2300
- selected.add(relative6(aggregatorDir, dir).replace(/\\/g, "/"));
3272
+ selected.add(relative9(aggregatorDir, dir).replace(/\\/g, "/"));
2301
3273
  }
2302
3274
  return { cwd: aggregatorDir, args: ["-pl", [...selected].join(","), "-amd"] };
2303
3275
  }
@@ -2308,6 +3280,7 @@ var MavenEcosystemStrategy = class {
2308
3280
  displayName = "Java / Maven";
2309
3281
  icon = "\u2615";
2310
3282
  manifestFile = "pom.xml";
3283
+ versioning = mavenVersionPolicy;
2311
3284
  discovery = new MavenModuleDiscoveryAdapter();
2312
3285
  registry;
2313
3286
  reader;
@@ -2345,6 +3318,8 @@ var EcosystemFactory = class {
2345
3318
  if (!strategy) {
2346
3319
  if (ecosystem === "maven") {
2347
3320
  strategy = new MavenEcosystemStrategy(this.settings.mavenPlugins);
3321
+ } else if (ecosystem === "cargo") {
3322
+ strategy = new CargoEcosystemStrategy();
2348
3323
  } else {
2349
3324
  strategy = new NpmEcosystemStrategy();
2350
3325
  }
@@ -2354,18 +3329,23 @@ var EcosystemFactory = class {
2354
3329
  }
2355
3330
  /** All supported ecosystems. */
2356
3331
  static getAvailableEcosystems() {
2357
- 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];
2358
3338
  }
2359
3339
  };
2360
3340
 
2361
3341
  // src/config.ts
2362
3342
  function findRepositoryRoot(startDir) {
2363
- const start = resolve10(startDir);
3343
+ const start = resolve14(startDir);
2364
3344
  const boundary = findCheckoutBoundary(start);
2365
3345
  let dir = start;
2366
3346
  for (; ; ) {
2367
- if (existsSync11(join18(dir, "elevate.config.json"))) return dir;
2368
- const parent = dirname8(dir);
3347
+ if (existsSync16(join23(dir, "elevate.config.json"))) return dir;
3348
+ const parent = dirname10(dir);
2369
3349
  if (dir === boundary || parent === dir) break;
2370
3350
  dir = parent;
2371
3351
  }
@@ -2430,15 +3410,15 @@ function resolveConfig(rootDir, file, source = "elevate.config.json") {
2430
3410
  }
2431
3411
  function loadConfig2(startDir = process.cwd()) {
2432
3412
  const rootDir = findRepositoryRoot(startDir);
2433
- const configPath = join18(rootDir, "elevate.config.json");
3413
+ const configPath = join23(rootDir, "elevate.config.json");
2434
3414
  let file = {};
2435
- if (existsSync11(configPath)) {
3415
+ if (existsSync16(configPath)) {
2436
3416
  const unreadable = (detail) => new ConfigError(
2437
3417
  `Cannot read elevate.config.json: ${detail} (${configPath}). Fix the file or remove it; ${NO_CONTINUE}`
2438
3418
  );
2439
3419
  let parsed;
2440
3420
  try {
2441
- parsed = JSON.parse(readFileSync11(configPath, "utf8").replace(/^/, ""));
3421
+ parsed = JSON.parse(readFileSync16(configPath, "utf8").replace(/^/, ""));
2442
3422
  } catch (err) {
2443
3423
  throw unreadable(err instanceof Error ? err.message : String(err));
2444
3424
  }
@@ -2499,7 +3479,8 @@ var de = {
2499
3479
  dev: "(dev/test)",
2500
3480
  prod: "(prod/compile)",
2501
3481
  align: "[Angleichen]",
2502
- internal: "[Intern]"
3482
+ internal: "[Intern]",
3483
+ shared: (count) => `[+${count} Crates]`
2503
3484
  },
2504
3485
  status: {
2505
3486
  selectedOf: (selected, total) => `${selected} von ${total}`,
@@ -2528,6 +3509,7 @@ var de = {
2528
3509
  hint: "[\u2191/\u2193/j/k] Navigieren \u2502 [Enter] Ausw\xE4hlen \u2502 [Esc] Abbrechen \u2502 [Backspace] Filter l\xF6schen",
2529
3510
  latestBadge: "LATEST",
2530
3511
  currentBadge: "AKTUELL",
3512
+ alsoChanges: (names) => `\xC4ndert auch: ${names.join(", ")}`,
2531
3513
  customBadge: "MANUELL"
2532
3514
  },
2533
3515
  updating: {
@@ -2556,6 +3538,7 @@ var de = {
2556
3538
  scanningChirps: ["*such*", "pr\xFCfe...", "oh, Updates?", "einen Moment!"],
2557
3539
  updatingChirps: ["*schuft*", "baue Code...", "gleich fertig!", "arbeite emsig..."],
2558
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!"],
2559
3542
  mavenChirps: ["*kaffee schl\xFCrf* \u2615", "Spring Boot l\xE4uft!", "Java 25 rockt!", "sauber gebaut!"]
2560
3543
  },
2561
3544
  splash: {
@@ -2607,7 +3590,8 @@ var en = {
2607
3590
  dev: "(dev/test)",
2608
3591
  prod: "(prod/compile)",
2609
3592
  align: "[Align]",
2610
- internal: "[Internal]"
3593
+ internal: "[Internal]",
3594
+ shared: (count) => `[+${count} crates]`
2611
3595
  },
2612
3596
  status: {
2613
3597
  selectedOf: (selected, total) => `${selected} of ${total}`,
@@ -2636,6 +3620,7 @@ var en = {
2636
3620
  hint: "[\u2191/\u2193/j/k] Navigate \u2502 [Enter] Select \u2502 [Esc] Cancel \u2502 [Backspace] Clear filter",
2637
3621
  latestBadge: "LATEST",
2638
3622
  currentBadge: "CURRENT",
3623
+ alsoChanges: (names) => `Also changes: ${names.join(", ")}`,
2639
3624
  customBadge: "MANUAL"
2640
3625
  },
2641
3626
  updating: {
@@ -2664,6 +3649,7 @@ var en = {
2664
3649
  scanningChirps: ["*peeking*", "checking...", "ooh, updates?", "just a sec!"],
2665
3650
  updatingChirps: ["*working hard*", "building...", "almost there!", "packing modules..."],
2666
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!"],
2667
3653
  mavenChirps: ["*sipping coffee* \u2615", "Spring Boot rocks!", "Java 25 is fly!", "clean build!"]
2668
3654
  },
2669
3655
  splash: {
@@ -2700,48 +3686,12 @@ function useI18n(initialLocale = "de") {
2700
3686
 
2701
3687
  // src/hooks/use-workspaces.ts
2702
3688
  import { useState as useState2, useEffect, useCallback as useCallback2 } from "react";
2703
- function useWorkspaces(strategy, rootDir) {
2704
- const [modules, setModules] = useState2([]);
2705
- const [loading, setLoading] = useState2(true);
2706
- const [activeIndex, setActiveIndex] = useState2(0);
2707
- const reload = useCallback2(async () => {
2708
- setLoading(true);
2709
- setModules([]);
2710
- setActiveIndex(0);
2711
- try {
2712
- setModules(await strategy.discovery.discover(rootDir));
2713
- } catch {
2714
- setModules([]);
2715
- } finally {
2716
- setLoading(false);
2717
- }
2718
- }, [strategy, rootDir]);
2719
- useEffect(() => {
2720
- reload();
2721
- }, [reload]);
2722
- const active = modules[activeIndex] || {
2723
- id: "empty",
2724
- name: "No module found",
2725
- path: rootDir,
2726
- relPath: "Root",
2727
- ecosystem: strategy.ecosystem,
2728
- isRoot: true
2729
- };
2730
- return {
2731
- modules,
2732
- active,
2733
- activeIndex,
2734
- setActiveIndex,
2735
- loading,
2736
- reload
2737
- };
2738
- }
2739
-
2740
- // src/hooks/use-packages.ts
2741
- import { useState as useState3, useEffect as useEffect2, useMemo as useMemo2, useCallback as useCallback3, useRef } from "react";
2742
- import semver5 from "semver";
2743
3689
 
2744
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
+ }
2745
3695
  function createScanContext(modules, config, channel) {
2746
3696
  return {
2747
3697
  rootDir: config.rootDir,
@@ -2801,7 +3751,52 @@ function candidateToJson(u) {
2801
3751
  };
2802
3752
  }
2803
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
+
2804
3798
  // src/hooks/use-packages.ts
3799
+ import { useState as useState3, useEffect as useEffect2, useMemo as useMemo2, useCallback as useCallback3, useRef } from "react";
2805
3800
  function usePackages(strategy, module, modules, config, channel = "stable") {
2806
3801
  const [candidates, setCandidates] = useState3([]);
2807
3802
  const [skipped, setSkipped] = useState3([]);
@@ -2868,15 +3863,12 @@ function usePackages(strategy, module, modules, config, channel = "stable") {
2868
3863
  setCandidates(
2869
3864
  (prev) => prev.map((c) => {
2870
3865
  if (c.coordinate.identifier !== identifier) return c;
2871
- const target = c.coordinate.ecosystem === "maven" ? cleanJavaVersion(chosenVersion) : semver5.valid(chosenVersion);
2872
- let diff = "minor";
2873
- if (c.currentClean && target && semver5.valid(c.currentClean)) {
2874
- diff = semver5.eq(c.currentClean, target) ? "patch" : diffVersions(c.currentClean, target);
2875
- }
3866
+ const policy = strategy.versioning;
3867
+ const diff = c.currentClean ? policy.diff(c.currentClean, chosenVersion) : "minor";
2876
3868
  const isPre = isPreReleaseVersion(chosenVersion);
2877
3869
  return {
2878
3870
  ...c,
2879
- newRange: c.coordinate.ecosystem === "npm" ? `${rangePrefix(c.currentRange)}${chosenVersion}` : chosenVersion,
3871
+ newRange: policy.rewrite(c.currentRange, chosenVersion),
2880
3872
  latest: chosenVersion,
2881
3873
  diff,
2882
3874
  selected: true,
@@ -2886,7 +3878,7 @@ function usePackages(strategy, module, modules, config, channel = "stable") {
2886
3878
  };
2887
3879
  })
2888
3880
  );
2889
- }, []);
3881
+ }, [strategy]);
2890
3882
  const switchTab = useCallback3((tab) => {
2891
3883
  setActiveTab(tab);
2892
3884
  setCursor(0);
@@ -2918,11 +3910,11 @@ function usePackages(strategy, module, modules, config, channel = "stable") {
2918
3910
  import { useState as useState4, useCallback as useCallback4 } from "react";
2919
3911
 
2920
3912
  // src/application/update-workflow.ts
2921
- import { existsSync as existsSync13 } from "fs";
2922
- import { relative as relative7 } from "path";
3913
+ import { existsSync as existsSync18 } from "fs";
3914
+ import { relative as relative10 } from "path";
2923
3915
 
2924
3916
  // src/adapters/shared/file-snapshot.ts
2925
- import { existsSync as existsSync12, readFileSync as readFileSync12, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
3917
+ import { existsSync as existsSync17, readFileSync as readFileSync17, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
2926
3918
  var FileSnapshot = class _FileSnapshot {
2927
3919
  constructor(entries2) {
2928
3920
  this.entries = entries2;
@@ -2933,7 +3925,7 @@ var FileSnapshot = class _FileSnapshot {
2933
3925
  const entries2 = /* @__PURE__ */ new Map();
2934
3926
  for (const file of files) {
2935
3927
  if (entries2.has(file)) continue;
2936
- entries2.set(file, existsSync12(file) ? readFileSync12(file) : null);
3928
+ entries2.set(file, existsSync17(file) ? readFileSync17(file) : null);
2937
3929
  }
2938
3930
  return new _FileSnapshot(entries2);
2939
3931
  }
@@ -2945,9 +3937,9 @@ var FileSnapshot = class _FileSnapshot {
2945
3937
  changedFiles() {
2946
3938
  return this.files.filter((file) => {
2947
3939
  const before = this.entries.get(file) ?? null;
2948
- const exists = existsSync12(file);
3940
+ const exists = existsSync17(file);
2949
3941
  if (before === null) return exists;
2950
- return !exists || !readFileSync12(file).equals(before);
3942
+ return !exists || !readFileSync17(file).equals(before);
2951
3943
  });
2952
3944
  }
2953
3945
  /** Writes every file back to its captured state; removes files that did not exist. */
@@ -2956,7 +3948,7 @@ var FileSnapshot = class _FileSnapshot {
2956
3948
  if (content === null) {
2957
3949
  rmSync2(file, { force: true });
2958
3950
  } else {
2959
- writeFileSync4(file, content);
3951
+ writeFileSync5(file, content);
2960
3952
  }
2961
3953
  }
2962
3954
  }
@@ -2995,7 +3987,7 @@ Restoring the installed state failed as well: ${message(err)}`;
2995
3987
  let baseline;
2996
3988
  if (!options.skipVerification) {
2997
3989
  progress("Running verification on the current state\u2026");
2998
- baseline = await verify(snapshot.files.filter((file) => existsSync13(file)));
3990
+ baseline = await verify(snapshot.files.filter((file) => existsSync18(file)));
2999
3991
  }
3000
3992
  let applied;
3001
3993
  try {
@@ -3041,7 +4033,7 @@ function message(err) {
3041
4033
  return err instanceof Error ? err.message : String(err);
3042
4034
  }
3043
4035
  function displayPaths(files, rootDir) {
3044
- return files.map((file) => relative7(rootDir, file).replace(/\\/g, "/"));
4036
+ return files.map((file) => relative10(rootDir, file).replace(/\\/g, "/"));
3045
4037
  }
3046
4038
 
3047
4039
  // src/hooks/use-updater.ts
@@ -3255,33 +4247,39 @@ var Header = ({
3255
4247
  // src/components/workspace-bar.tsx
3256
4248
  import { Box as Box3, Text as Text3 } from "ink";
3257
4249
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
3258
- var WorkspaceBar = ({ module, internalScopes, strategy, t }) => {
4250
+ var WorkspaceBar = ({ module, internalScopes, strategy, t, diagnostics = [] }) => {
3259
4251
  const scopeHint = internalScopes.length > 0 ? t.status.internalScopes(internalScopes) : t.status.noInternalScopes;
3260
- return /* @__PURE__ */ jsxs3(Box3, { marginY: 1, justifyContent: "space-between", alignItems: "center", children: [
3261
- /* @__PURE__ */ jsxs3(Box3, { alignItems: "center", flexShrink: 1, children: [
3262
- /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
3263
- "\u{1F4CD} ",
3264
- strategy.icon,
3265
- " ",
3266
- t.workspace.label,
3267
- ": "
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
+ ] })
3268
4268
  ] }),
3269
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: theme.colors.brandLight, children: module.name }),
3270
- /* @__PURE__ */ jsxs3(Text3, { color: theme.colors.muted, children: [
3271
- " (",
3272
- module.relPath,
3273
- ")"
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
+ ] })
3274
4277
  ] })
3275
4278
  ] }),
3276
- /* @__PURE__ */ jsxs3(Box3, { alignItems: "center", marginLeft: 2, children: [
3277
- /* @__PURE__ */ jsx3(Text3, { color: theme.colors.muted, children: scopeHint }),
3278
- /* @__PURE__ */ jsx3(Text3, { color: theme.colors.muted, children: " \u2502 " }),
3279
- /* @__PURE__ */ jsx3(Text3, { color: theme.colors.brandLight, bold: true, children: "[W]" }),
3280
- /* @__PURE__ */ jsxs3(Text3, { color: theme.colors.muted, children: [
3281
- " ",
3282
- t.workspace.switchHint.replace("[W]", "").trim()
3283
- ] })
3284
- ] })
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))
3285
4283
  ] });
3286
4284
  };
3287
4285
 
@@ -3331,7 +4329,9 @@ import { Box as Box5, Text as Text5 } from "ink";
3331
4329
  import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
3332
4330
  var PackageRow = ({ item, isFocused, t }) => {
3333
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 });
3334
- 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;
3335
4335
  const isDevOrTest = item.scope === "dev" || item.scope === "test";
3336
4336
  const property = item.declaration?.kind === "property" ? `\${${item.declaration.propertyName}}` : void 0;
3337
4337
  return /* @__PURE__ */ jsxs5(Box5, { justifyContent: "space-between", children: [
@@ -3510,7 +4510,7 @@ var ChickenMascot = ({ state = "idle", t, ecosystem = "npm" }) => {
3510
4510
  chickFace = cur.face;
3511
4511
  wingLeft = cur.l;
3512
4512
  wingRight = cur.r;
3513
- 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;
3514
4514
  currentChirp = chirpsPool[chirpIndex % chirpsPool.length];
3515
4515
  }
3516
4516
  return /* @__PURE__ */ jsxs7(Box7, { alignItems: "center", children: [
@@ -3788,6 +4788,7 @@ var VersionModal = ({
3788
4788
  ] }),
3789
4789
  /* @__PURE__ */ jsx11(Text11, { color: theme.colors.muted, children: t.versionModal.current(candidate.currentRange) })
3790
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) }) }),
3791
4792
  /* @__PURE__ */ jsxs11(
3792
4793
  Box11,
3793
4794
  {
@@ -3962,10 +4963,10 @@ var App = ({ config }) => {
3962
4963
  const [versionPickerCandidate, setVersionPickerCandidate] = useState8(null);
3963
4964
  const [ecosystem, setEcosystem] = useState8("npm");
3964
4965
  const strategy = useMemo4(() => EcosystemFactory.getStrategy(ecosystem), [ecosystem]);
3965
- const nextEcosystem = ecosystem === "npm" ? "maven" : "npm";
4966
+ const nextEcosystem = EcosystemFactory.next(ecosystem);
3966
4967
  const nextStrategy = useMemo4(() => EcosystemFactory.getStrategy(nextEcosystem), [nextEcosystem]);
3967
4968
  const toggleEcosystem = useCallback5(() => {
3968
- setEcosystem((prev) => prev === "npm" ? "maven" : "npm");
4969
+ setEcosystem((prev) => EcosystemFactory.next(prev));
3969
4970
  }, []);
3970
4971
  const { locale, toggleLocale, t } = useI18n(resolveInitialLocale(config.locale));
3971
4972
  const ws = useWorkspaces(strategy, config.rootDir);
@@ -4117,7 +5118,8 @@ var App = ({ config }) => {
4117
5118
  module: ws.active,
4118
5119
  internalScopes: config.internalScopes,
4119
5120
  strategy,
4120
- t
5121
+ t,
5122
+ diagnostics: ws.diagnostics
4121
5123
  }
4122
5124
  ),
4123
5125
  /* @__PURE__ */ jsx14(
@@ -4157,6 +5159,7 @@ var App = ({ config }) => {
4157
5159
  };
4158
5160
 
4159
5161
  // src/cli/parser.ts
5162
+ var isEcosystem = (v) => EcosystemFactory.getAvailableEcosystems().includes(v);
4160
5163
  function parseCliArgs(args) {
4161
5164
  const [first, ...rest] = args;
4162
5165
  if (!first || first === "--help" || first === "-h" || first === "help") {
@@ -4194,10 +5197,10 @@ function parseCliArgs(args) {
4194
5197
  result.help = true;
4195
5198
  } else if (arg.startsWith("--ecosystem=")) {
4196
5199
  const val = arg.split("=")[1];
4197
- if (val === "npm" || val === "maven") result.ecosystem = val;
5200
+ if (isEcosystem(val)) result.ecosystem = val;
4198
5201
  } else if (arg === "-e" || arg === "--ecosystem") {
4199
5202
  const val = remaining[++i];
4200
- if (val === "npm" || val === "maven") result.ecosystem = val;
5203
+ if (isEcosystem(val)) result.ecosystem = val;
4201
5204
  } else if (arg.startsWith("--module=")) {
4202
5205
  result.module = arg.split("=")[1];
4203
5206
  } else if (arg === "-m" || arg === "--module") {
@@ -4239,7 +5242,7 @@ SUBCOMMANDS:
4239
5242
  mcp Start stdio-based MCP server
4240
5243
 
4241
5244
  FLAGS:
4242
- -e, --ecosystem <npm|maven> Target ecosystem (default: npm)
5245
+ -e, --ecosystem <npm|maven|cargo> Target ecosystem (default: npm)
4243
5246
  -m, --module <name|path> Target module ID or relative path (default: root)
4244
5247
  -c, --channel <stable|all> Release channel (default: stable)
4245
5248
  -p, --packages <pkg1,pkg2@1.2.3> Comma-separated list of packages to update
@@ -4267,11 +5270,11 @@ EXAMPLES:
4267
5270
  async function handleModulesCommand(options, config) {
4268
5271
  const ecosystem = options.ecosystem ?? "npm";
4269
5272
  const strategy = EcosystemFactory.getStrategy(ecosystem);
4270
- const modules = await strategy.discovery.discover(config.rootDir);
5273
+ const { modules, diagnostics } = await discoverModules(strategy, config.rootDir);
4271
5274
  const displayName = strategy.displayNameFor?.(config.rootDir) ?? strategy.displayName;
4272
5275
  const packageManager = strategy.packageManagerFor?.(config.rootDir);
4273
5276
  if (options.json) {
4274
- console.log(JSON.stringify({ ecosystem, ...packageManager ? { packageManager } : {}, count: modules.length, modules }, null, 2));
5277
+ console.log(JSON.stringify({ ecosystem, ...packageManager ? { packageManager } : {}, count: modules.length, modules, diagnostics }, null, 2));
4275
5278
  return 0;
4276
5279
  }
4277
5280
  console.log(`
@@ -4282,6 +5285,7 @@ async function handleModulesCommand(options, config) {
4282
5285
  console.log(` \u2022 ${m.name.padEnd(45)} (${m.relPath})${isRootBadge}`);
4283
5286
  }
4284
5287
  console.log("");
5288
+ for (const problem of diagnostics) console.error(`\u26A0\uFE0F ${problem}`);
4285
5289
  return 0;
4286
5290
  }
4287
5291
 
@@ -4303,6 +5307,8 @@ function describeSkip(skipped) {
4303
5307
  return `workspace package that pnpm does not link unless it is declared with the workspace: protocol${detail}`;
4304
5308
  case "catalog-unresolved":
4305
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}`;
4306
5312
  }
4307
5313
  }
4308
5314
 
@@ -4311,7 +5317,8 @@ async function handleScanCommand(options, config) {
4311
5317
  const ecosystem = options.ecosystem ?? "npm";
4312
5318
  const channel = options.channel ?? config.channel ?? "stable";
4313
5319
  const strategy = EcosystemFactory.getStrategy(ecosystem);
4314
- 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}`);
4315
5322
  if (modules.length === 0) {
4316
5323
  if (options.json) {
4317
5324
  console.log(JSON.stringify({ error: `No ${strategy.displayName} modules found.`, ecosystem }, null, 2));
@@ -4461,7 +5468,7 @@ async function handleVersionsCommand(options, config) {
4461
5468
  }
4462
5469
 
4463
5470
  // src/application/update-selection.ts
4464
- import semver6 from "semver";
5471
+ import semver12 from "semver";
4465
5472
  var UpdateSelectionError = class extends Error {
4466
5473
  constructor(message2) {
4467
5474
  super(message2);
@@ -4486,9 +5493,9 @@ function selectRequested(scan, requests, options) {
4486
5493
  `'${request.identifier}' has no pending update. Specify a target version (e.g. ${request.identifier}@1.2.3).`
4487
5494
  );
4488
5495
  }
4489
- if (options.ecosystem === "maven") {
5496
+ if (!policyOf(options).allowsManualTargets) {
4490
5497
  throw new UpdateSelectionError(
4491
- `'${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.`
4492
5499
  );
4493
5500
  }
4494
5501
  if (options.originOf(request.identifier) !== "public") {
@@ -4513,12 +5520,12 @@ function retarget(found, target, options) {
4513
5520
  `'${found.coordinate.identifier}' is a workspace module; it can only be aligned to its local version ${found.latest}.`
4514
5521
  );
4515
5522
  }
4516
- if (options.ecosystem === "maven" && found.availableVersions && !found.availableVersions.includes(target)) {
5523
+ if (!policyOf(options).allowsManualTargets && found.availableVersions && !found.availableVersions.includes(target)) {
4517
5524
  throw new UpdateSelectionError(
4518
5525
  `'${found.coordinate.identifier}' has no version ${target} in the configured repositories. Available: ${found.availableVersions.slice(0, 5).join(", ")}${found.availableVersions.length > 5 ? ", \u2026" : ""}.`
4519
5526
  );
4520
5527
  }
4521
- const diff = computeDiff(found.currentClean, target, options.ecosystem);
5528
+ const diff = policyOf(options).diff(found.currentClean, target);
4522
5529
  if (diff === "major" && !options.allowMajor) {
4523
5530
  throw new UpdateSelectionError(
4524
5531
  `'${found.coordinate.identifier}' requires a major upgrade to ${target}. Pass --allow-major / allowMajor.`
@@ -4527,7 +5534,7 @@ function retarget(found, target, options) {
4527
5534
  const isPreRelease = isPreReleaseVersion(target);
4528
5535
  return {
4529
5536
  ...found,
4530
- newRange: options.ecosystem === "npm" ? `${rangePrefix(found.currentRange)}${target}` : target,
5537
+ newRange: policyOf(options).rewrite(found.currentRange, target),
4531
5538
  diff,
4532
5539
  selected: true,
4533
5540
  isCustomVersion: true,
@@ -4536,7 +5543,7 @@ function retarget(found, target, options) {
4536
5543
  };
4537
5544
  }
4538
5545
  function manualNpmCandidate(identifier, target, options) {
4539
- if (!semver6.valid(target)) {
5546
+ if (!semver12.valid(target)) {
4540
5547
  throw new UpdateSelectionError(`'${target}' is not a valid version for '${identifier}'.`);
4541
5548
  }
4542
5549
  return {
@@ -4553,10 +5560,8 @@ function manualNpmCandidate(identifier, target, options) {
4553
5560
  isCustomVersion: true
4554
5561
  };
4555
5562
  }
4556
- function computeDiff(current, target, ecosystem) {
4557
- const to = ecosystem === "maven" ? cleanJavaVersion(target) : semver6.valid(target);
4558
- if (!to || !semver6.valid(current)) return "minor";
4559
- return semver6.eq(current, to) ? "patch" : diffVersions(current, to);
5563
+ function policyOf(options) {
5564
+ return EcosystemFactory.getStrategy(options.ecosystem).versioning;
4560
5565
  }
4561
5566
 
4562
5567
  // src/cli/command-update.ts
@@ -4568,7 +5573,8 @@ async function handleUpdateCommand(options, config) {
4568
5573
  else console.error(`\u274C ${message2}`);
4569
5574
  return code;
4570
5575
  };
4571
- 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}`);
4572
5578
  if (modules.length === 0) return fail(`No ${strategy.displayName} modules found.`);
4573
5579
  const targetModule = options.module ? findModule(modules, options.module) : modules[0];
4574
5580
  if (!targetModule) return fail(`Module '${options.module}' not found.`);
@@ -4709,9 +5715,9 @@ function parsePackageArgument(item) {
4709
5715
  }
4710
5716
 
4711
5717
  // src/cli/command-init.ts
4712
- import { existsSync as existsSync14, readFileSync as readFileSync13, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2 } from "fs";
4713
- import { homedir, platform } from "os";
4714
- import { join as join19, 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";
4715
5721
  function mcpServerEntry() {
4716
5722
  return {
4717
5723
  command: "npx",
@@ -4754,10 +5760,10 @@ automatic rollback.
4754
5760
  requested). A failed install or verification is rolled back automatically.
4755
5761
  4. \`elevate_verify\` \u2014 Verify build integrity when needed
4756
5762
  `;
4757
- var home = homedir();
5763
+ var home = homedir2();
4758
5764
  var isWin = platform() === "win32";
4759
5765
  function appData() {
4760
- return process.env.APPDATA || join19(home, "AppData", "Roaming");
5766
+ return process.env.APPDATA || join24(home, "AppData", "Roaming");
4761
5767
  }
4762
5768
  function detectCursor(cwd) {
4763
5769
  const env = {
@@ -4769,25 +5775,25 @@ function detectCursor(cwd) {
4769
5775
  mcpConfigured: false,
4770
5776
  skillConfigured: false
4771
5777
  };
4772
- const workspaceCursorDir = join19(cwd, ".cursor");
4773
- const globalCursorDir = join19(home, ".cursor");
4774
- if (existsSync14(workspaceCursorDir) || existsSync14(globalCursorDir)) {
5778
+ const workspaceCursorDir = join24(cwd, ".cursor");
5779
+ const globalCursorDir = join24(home, ".cursor");
5780
+ if (existsSync19(workspaceCursorDir) || existsSync19(globalCursorDir)) {
4775
5781
  env.detected = true;
4776
- env.reason = existsSync14(workspaceCursorDir) ? `Found .cursor/ in workspace` : `Found ~/.cursor/ global config`;
5782
+ env.reason = existsSync19(workspaceCursorDir) ? `Found .cursor/ in workspace` : `Found ~/.cursor/ global config`;
4777
5783
  }
4778
- const mcpPath = join19(cwd, ".cursor", "mcp.json");
4779
- if (existsSync14(mcpPath)) {
5784
+ const mcpPath = join24(cwd, ".cursor", "mcp.json");
5785
+ if (existsSync19(mcpPath)) {
4780
5786
  try {
4781
- const content = JSON.parse(readFileSync13(mcpPath, "utf8"));
5787
+ const content = JSON.parse(readFileSync18(mcpPath, "utf8"));
4782
5788
  if (content?.mcpServers?.elevate) {
4783
5789
  env.mcpConfigured = true;
4784
5790
  }
4785
5791
  } catch {
4786
5792
  }
4787
5793
  }
4788
- const rulesPath = join19(cwd, ".cursorrules");
4789
- if (existsSync14(rulesPath)) {
4790
- const content = readFileSync13(rulesPath, "utf8");
5794
+ const rulesPath = join24(cwd, ".cursorrules");
5795
+ if (existsSync19(rulesPath)) {
5796
+ const content = readFileSync18(rulesPath, "utf8");
4791
5797
  if (content.includes("elevate") || content.includes("Elevate")) {
4792
5798
  env.skillConfigured = true;
4793
5799
  }
@@ -4804,12 +5810,12 @@ function detectClaudeDesktop() {
4804
5810
  mcpConfigured: false,
4805
5811
  skillConfigured: false
4806
5812
  };
4807
- const configPath = isWin ? join19(appData(), "Claude", "claude_desktop_config.json") : join19(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
4808
- if (existsSync14(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)) {
4809
5815
  env.detected = true;
4810
5816
  env.reason = `Found ${configPath}`;
4811
5817
  try {
4812
- const content = JSON.parse(readFileSync13(configPath, "utf8"));
5818
+ const content = JSON.parse(readFileSync18(configPath, "utf8"));
4813
5819
  if (content?.mcpServers?.elevate) {
4814
5820
  env.mcpConfigured = true;
4815
5821
  }
@@ -4828,23 +5834,23 @@ function detectClaudeCode(cwd) {
4828
5834
  mcpConfigured: false,
4829
5835
  skillConfigured: false
4830
5836
  };
4831
- const mcpJsonPath = join19(cwd, ".mcp.json");
4832
- const claudeMdPath = join19(cwd, "CLAUDE.md");
4833
- if (existsSync14(mcpJsonPath) || existsSync14(claudeMdPath)) {
5837
+ const mcpJsonPath = join24(cwd, ".mcp.json");
5838
+ const claudeMdPath = join24(cwd, "CLAUDE.md");
5839
+ if (existsSync19(mcpJsonPath) || existsSync19(claudeMdPath)) {
4834
5840
  env.detected = true;
4835
- env.reason = existsSync14(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`;
4836
5842
  }
4837
- if (existsSync14(mcpJsonPath)) {
5843
+ if (existsSync19(mcpJsonPath)) {
4838
5844
  try {
4839
- const content = JSON.parse(readFileSync13(mcpJsonPath, "utf8"));
5845
+ const content = JSON.parse(readFileSync18(mcpJsonPath, "utf8"));
4840
5846
  if (content?.mcpServers?.elevate) {
4841
5847
  env.mcpConfigured = true;
4842
5848
  }
4843
5849
  } catch {
4844
5850
  }
4845
5851
  }
4846
- if (existsSync14(claudeMdPath)) {
4847
- const content = readFileSync13(claudeMdPath, "utf8");
5852
+ if (existsSync19(claudeMdPath)) {
5853
+ const content = readFileSync18(claudeMdPath, "utf8");
4848
5854
  if (content.includes("elevate") || content.includes("Elevate")) {
4849
5855
  env.skillConfigured = true;
4850
5856
  }
@@ -4861,20 +5867,20 @@ function detectAntigravity(cwd) {
4861
5867
  mcpConfigured: false,
4862
5868
  skillConfigured: false
4863
5869
  };
4864
- const globalGeminiDir = join19(home, ".gemini");
4865
- const workspaceAgentsDir = join19(cwd, ".agents");
4866
- if (existsSync14(globalGeminiDir) || existsSync14(workspaceAgentsDir)) {
5870
+ const globalGeminiDir = join24(home, ".gemini");
5871
+ const workspaceAgentsDir = join24(cwd, ".agents");
5872
+ if (existsSync19(globalGeminiDir) || existsSync19(workspaceAgentsDir)) {
4867
5873
  env.detected = true;
4868
- env.reason = existsSync14(workspaceAgentsDir) ? `Found .agents/ in workspace` : `Found ~/.gemini/ global config`;
5874
+ env.reason = existsSync19(workspaceAgentsDir) ? `Found .agents/ in workspace` : `Found ~/.gemini/ global config`;
4869
5875
  }
4870
5876
  const mcpPaths = [
4871
- join19(cwd, ".agents", "mcp_config.json"),
4872
- join19(home, ".gemini", "config", "mcp_config.json")
5877
+ join24(cwd, ".agents", "mcp_config.json"),
5878
+ join24(home, ".gemini", "config", "mcp_config.json")
4873
5879
  ];
4874
5880
  for (const p of mcpPaths) {
4875
- if (existsSync14(p)) {
5881
+ if (existsSync19(p)) {
4876
5882
  try {
4877
- const content = JSON.parse(readFileSync13(p, "utf8"));
5883
+ const content = JSON.parse(readFileSync18(p, "utf8"));
4878
5884
  if (content?.mcpServers?.elevate) {
4879
5885
  env.mcpConfigured = true;
4880
5886
  break;
@@ -4895,24 +5901,24 @@ function detectVSCode(cwd) {
4895
5901
  mcpConfigured: false,
4896
5902
  skillConfigured: false
4897
5903
  };
4898
- const vscodeDir = join19(cwd, ".vscode");
4899
- if (existsSync14(vscodeDir)) {
5904
+ const vscodeDir = join24(cwd, ".vscode");
5905
+ if (existsSync19(vscodeDir)) {
4900
5906
  env.detected = true;
4901
5907
  env.reason = `Found .vscode/ in workspace`;
4902
5908
  }
4903
- const mcpPath = join19(cwd, ".vscode", "mcp.json");
4904
- if (existsSync14(mcpPath)) {
5909
+ const mcpPath = join24(cwd, ".vscode", "mcp.json");
5910
+ if (existsSync19(mcpPath)) {
4905
5911
  try {
4906
- const content = JSON.parse(readFileSync13(mcpPath, "utf8"));
5912
+ const content = JSON.parse(readFileSync18(mcpPath, "utf8"));
4907
5913
  if (content?.servers?.elevate) {
4908
5914
  env.mcpConfigured = true;
4909
5915
  }
4910
5916
  } catch {
4911
5917
  }
4912
5918
  }
4913
- const copilotInstructions = join19(cwd, ".github", "copilot-instructions.md");
4914
- if (existsSync14(copilotInstructions)) {
4915
- const content = readFileSync13(copilotInstructions, "utf8");
5919
+ const copilotInstructions = join24(cwd, ".github", "copilot-instructions.md");
5920
+ if (existsSync19(copilotInstructions)) {
5921
+ const content = readFileSync18(copilotInstructions, "utf8");
4916
5922
  if (content.includes("elevate") || content.includes("Elevate")) {
4917
5923
  env.skillConfigured = true;
4918
5924
  }
@@ -4929,14 +5935,14 @@ function detectWindsurf() {
4929
5935
  mcpConfigured: false,
4930
5936
  skillConfigured: false
4931
5937
  };
4932
- const configPath = isWin ? join19(appData(), "Codeium", "Windsurf", "mcp_config.json") : join19(home, ".codeium", "windsurf", "mcp_config.json");
4933
- const altPath = join19(home, ".codeium", "windsurf", "mcp_config.json");
4934
- if (existsSync14(configPath) || existsSync14(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)) {
4935
5941
  env.detected = true;
4936
5942
  env.reason = `Found Windsurf config`;
4937
- const path = existsSync14(configPath) ? configPath : altPath;
5943
+ const path = existsSync19(configPath) ? configPath : altPath;
4938
5944
  try {
4939
- const content = JSON.parse(readFileSync13(path, "utf8"));
5945
+ const content = JSON.parse(readFileSync18(path, "utf8"));
4940
5946
  if (content?.mcpServers?.elevate) {
4941
5947
  env.mcpConfigured = true;
4942
5948
  }
@@ -4947,9 +5953,9 @@ function detectWindsurf() {
4947
5953
  }
4948
5954
  function mergeJsonMcpServer(filePath, serverKey, topLevelKey) {
4949
5955
  let existing = {};
4950
- if (existsSync14(filePath)) {
5956
+ if (existsSync19(filePath)) {
4951
5957
  try {
4952
- existing = JSON.parse(readFileSync13(filePath, "utf8"));
5958
+ existing = JSON.parse(readFileSync18(filePath, "utf8"));
4953
5959
  } catch {
4954
5960
  existing = {};
4955
5961
  }
@@ -4961,69 +5967,69 @@ function mergeJsonMcpServer(filePath, serverKey, topLevelKey) {
4961
5967
  servers[serverKey] = mcpServerEntry();
4962
5968
  existing[topLevelKey] = servers;
4963
5969
  const dir = filePath.substring(0, filePath.lastIndexOf(isWin ? "\\" : "/"));
4964
- if (dir && !existsSync14(dir)) {
5970
+ if (dir && !existsSync19(dir)) {
4965
5971
  mkdirSync2(dir, { recursive: true });
4966
5972
  }
4967
- writeFileSync5(filePath, JSON.stringify(existing, null, 2) + "\n", "utf8");
5973
+ writeFileSync6(filePath, JSON.stringify(existing, null, 2) + "\n", "utf8");
4968
5974
  return { written: true, message: `${filePath}: elevate MCP server registered` };
4969
5975
  }
4970
5976
  function appendSkillToFile(filePath, content, marker) {
4971
- if (existsSync14(filePath)) {
4972
- const existing = readFileSync13(filePath, "utf8");
5977
+ if (existsSync19(filePath)) {
5978
+ const existing = readFileSync18(filePath, "utf8");
4973
5979
  if (existing.includes(marker)) {
4974
5980
  return { written: false, message: `${filePath}: skill already present` };
4975
5981
  }
4976
- writeFileSync5(filePath, existing + "\n\n" + content, "utf8");
5982
+ writeFileSync6(filePath, existing + "\n\n" + content, "utf8");
4977
5983
  return { written: true, message: `${filePath}: skill appended` };
4978
5984
  }
4979
5985
  const dir = filePath.substring(0, filePath.lastIndexOf(isWin ? "\\" : "/"));
4980
- if (dir && !existsSync14(dir)) {
5986
+ if (dir && !existsSync19(dir)) {
4981
5987
  mkdirSync2(dir, { recursive: true });
4982
5988
  }
4983
- writeFileSync5(filePath, content, "utf8");
5989
+ writeFileSync6(filePath, content, "utf8");
4984
5990
  return { written: true, message: `${filePath}: skill created` };
4985
5991
  }
4986
5992
  function configureCursor(cwd, result) {
4987
- const mcpPath = join19(cwd, ".cursor", "mcp.json");
5993
+ const mcpPath = join24(cwd, ".cursor", "mcp.json");
4988
5994
  const mcp = mergeJsonMcpServer(mcpPath, "elevate", "mcpServers");
4989
5995
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Cursor MCP: ${mcp.message}`);
4990
- const rulesPath = join19(cwd, ".cursorrules");
5996
+ const rulesPath = join24(cwd, ".cursorrules");
4991
5997
  const skill = appendSkillToFile(rulesPath, ELEVATE_SKILL_CONTENT, "elevate_scan");
4992
5998
  result.actions.push(`${skill.written ? "\u2705" : "\u23ED\uFE0F"} Cursor Rules: ${skill.message}`);
4993
5999
  }
4994
6000
  function configureClaudeDesktop(result) {
4995
- const configPath = isWin ? join19(appData(), "Claude", "claude_desktop_config.json") : join19(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");
4996
6002
  const mcp = mergeJsonMcpServer(configPath, "elevate", "mcpServers");
4997
6003
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Claude Desktop MCP: ${mcp.message}`);
4998
6004
  }
4999
6005
  function configureClaudeCode(cwd, result) {
5000
- const mcpPath = join19(cwd, ".mcp.json");
6006
+ const mcpPath = join24(cwd, ".mcp.json");
5001
6007
  const mcp = mergeJsonMcpServer(mcpPath, "elevate", "mcpServers");
5002
6008
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Claude Code MCP: ${mcp.message}`);
5003
- const claudeMdPath = join19(cwd, "CLAUDE.md");
6009
+ const claudeMdPath = join24(cwd, "CLAUDE.md");
5004
6010
  const skill = appendSkillToFile(claudeMdPath, ELEVATE_SKILL_CONTENT, "elevate_scan");
5005
6011
  result.actions.push(`${skill.written ? "\u2705" : "\u23ED\uFE0F"} Claude Code Skill: ${skill.message}`);
5006
6012
  }
5007
6013
  function configureAntigravity(cwd, result) {
5008
- const mcpPath = join19(cwd, ".agents", "mcp_config.json");
6014
+ const mcpPath = join24(cwd, ".agents", "mcp_config.json");
5009
6015
  const mcp = mergeJsonMcpServer(mcpPath, "elevate", "mcpServers");
5010
6016
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Antigravity MCP: ${mcp.message}`);
5011
6017
  }
5012
6018
  function configureVSCode(cwd, result) {
5013
- const mcpPath = join19(cwd, ".vscode", "mcp.json");
6019
+ const mcpPath = join24(cwd, ".vscode", "mcp.json");
5014
6020
  const mcp = mergeJsonMcpServer(mcpPath, "elevate", "servers");
5015
6021
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} VS Code MCP: ${mcp.message}`);
5016
- const instructionsPath = join19(cwd, ".github", "copilot-instructions.md");
6022
+ const instructionsPath = join24(cwd, ".github", "copilot-instructions.md");
5017
6023
  const skill = appendSkillToFile(instructionsPath, ELEVATE_SKILL_CONTENT, "elevate_scan");
5018
6024
  result.actions.push(`${skill.written ? "\u2705" : "\u23ED\uFE0F"} Copilot Instructions: ${skill.message}`);
5019
6025
  }
5020
6026
  function configureWindsurf(result) {
5021
- const configPath = isWin ? join19(appData(), "Codeium", "Windsurf", "mcp_config.json") : join19(home, ".codeium", "windsurf", "mcp_config.json");
6027
+ const configPath = isWin ? join24(appData(), "Codeium", "Windsurf", "mcp_config.json") : join24(home, ".codeium", "windsurf", "mcp_config.json");
5022
6028
  const mcp = mergeJsonMcpServer(configPath, "elevate", "mcpServers");
5023
6029
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Windsurf MCP: ${mcp.message}`);
5024
6030
  }
5025
6031
  async function handleInitCommand(options) {
5026
- const cwd = resolve11(process.cwd());
6032
+ const cwd = resolve15(process.cwd());
5027
6033
  const result = {
5028
6034
  cwd,
5029
6035
  environments: [],
@@ -5051,7 +6057,7 @@ async function handleInitCommand(options) {
5051
6057
  console.log(` Creating portable .mcp.json for broad compatibility.
5052
6058
  `);
5053
6059
  }
5054
- const mcpPath = join19(cwd, ".mcp.json");
6060
+ const mcpPath = join24(cwd, ".mcp.json");
5055
6061
  const mcp = mergeJsonMcpServer(mcpPath, "elevate", "mcpServers");
5056
6062
  result.actions.push(`${mcp.written ? "\u2705" : "\u23ED\uFE0F"} Generic MCP: ${mcp.message}`);
5057
6063
  } else {
@@ -5118,14 +6124,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5118
6124
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5119
6125
  var ECOSYSTEM_PROPERTY = {
5120
6126
  type: "string",
5121
- enum: ["npm", "maven"],
6127
+ enum: EcosystemFactory.getAvailableEcosystems(),
5122
6128
  description: "Target ecosystem (default: npm)"
5123
6129
  };
5124
6130
  async function startMcpServer(config) {
5125
6131
  const server = new Server(
5126
6132
  {
5127
6133
  name: "elevate-mcp",
5128
- version: "1.2.0"
6134
+ version: VERSION
5129
6135
  },
5130
6136
  {
5131
6137
  capabilities: {
@@ -5138,7 +6144,7 @@ async function startMcpServer(config) {
5138
6144
  tools: [
5139
6145
  {
5140
6146
  name: "elevate_discover_modules",
5141
- 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).",
5142
6148
  inputSchema: {
5143
6149
  type: "object",
5144
6150
  properties: {
@@ -5178,7 +6184,7 @@ async function startMcpServer(config) {
5178
6184
  ecosystem: ECOSYSTEM_PROPERTY,
5179
6185
  identifier: {
5180
6186
  type: "string",
5181
- description: "Package identifier (npm: chalk, maven: org.slf4j:slf4j-api)"
6187
+ description: "Package identifier (npm: chalk, maven: org.slf4j:slf4j-api, cargo: serde)"
5182
6188
  }
5183
6189
  },
5184
6190
  required: ["identifier"]
@@ -5259,12 +6265,13 @@ async function startMcpServer(config) {
5259
6265
  };
5260
6266
  try {
5261
6267
  if (name === "elevate_discover_modules") {
5262
- const modules = await strategy.discovery.discover(config.rootDir);
6268
+ const { modules, diagnostics } = await discoverModules(strategy, config.rootDir);
5263
6269
  const packageManager = strategy.packageManagerFor?.(config.rootDir);
5264
6270
  return reply({
5265
6271
  ecosystem,
5266
6272
  ...packageManager ? { packageManager } : {},
5267
6273
  count: modules.length,
6274
+ diagnostics,
5268
6275
  modules: modules.map((m) => ({
5269
6276
  id: m.id,
5270
6277
  name: m.name,