@dennisrepa/elevate 1.5.0 → 1.7.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/README.md CHANGED
@@ -215,7 +215,7 @@ Detailed parameter references are available in [`docs/en.md`](./docs/en.md) and
215
215
  * **pnpm specifics.** `catalog:` entries are updated where they are declared, in `pnpm-workspace.yaml`. A plain range on a workspace module (`^1.0.0`) is listed as *not linked* and never rewritten to `workspace:`, because pnpm resolves it from the registry. Elevate does not read pnpm settings such as `minimumReleaseAge` or the approval of build scripts, so pnpm may refuse an offered version; the update is then rolled back with pnpm's message. pnpm 8 and older, Windows and `shared-workspace-lockfile=false` are not covered.
216
216
  * **Broken configuration stops Elevate.** An `elevate.config.json` that is not valid JSON, not an object, or whose `internalScopes` is not a list of strings is reported and Elevate exits with code `1`, because ignoring it would switch off the protection of internal packages. Without any configuration file Elevate works with defaults.
217
217
  * **Dependency sections.** `dependencies`, `devDependencies` and `optionalDependencies` are covered. `peerDependencies` and `overrides` are never changed.
218
- * **Cargo.** Requirements are raised keeping their precision, so a loose `"1"` only shows breaking updates; wildcard and compound requirements (`*`, `>=1.2, <2`), git and external path dependencies are left alone. Lock-file-only updates, registries that need authentication and `[patch]` sections are not covered; a vendored or local-registry source replacement is refused rather than bypassed.
218
+ * **Cargo.** Requirements are raised keeping their precision, so a loose `"1"` only shows breaking updates; wildcard and compound requirements (`*`, `>=1.2, <2`), git and external path dependencies are left alone. Lock-file-only updates (`[Lock]`) are offered for direct dependencies when `Cargo.lock` is behind a requirement that already allows a newer release; transitive crates are not, and a crate with a pending manifest update shows only that candidate. Registries that need authentication and `[patch]` sections are not covered; a vendored or local-registry source replacement is refused rather than bypassed.
219
219
  * **Maven.** Maven plugin versions and declarations inside `<profiles>` are not offered. Dependencies whose version is declared outside the repository (an external parent or BOM) are listed as *not offered* with the reason.
220
220
 
221
221
  ---
package/dist/cli.js CHANGED
@@ -1351,7 +1351,7 @@ var CargoConfigReader = class {
1351
1351
 
1352
1352
  // src/version.ts
1353
1353
  import { createRequire } from "module";
1354
- var VERSION = true ? "1.5.0" : fromPackage();
1354
+ var VERSION = true ? "1.7.0" : fromPackage();
1355
1355
 
1356
1356
  // src/adapters/cargo/cargo-index.ts
1357
1357
  var RegistryAuthRequiredError = class extends Error {
@@ -1463,6 +1463,13 @@ import semver6 from "semver";
1463
1463
  // src/adapters/cargo/cargo-versions.ts
1464
1464
  import semver5 from "semver";
1465
1465
  var rust = (v) => semver5.coerce(v)?.version;
1466
+ function eligibleVersions(versions, options) {
1467
+ const project = options.rustVersion ? rust(options.rustVersion) : void 0;
1468
+ return versions.filter((v) => !v.yanked && semver5.valid(v.version)).filter((v) => options.channel === "all" || semver5.prerelease(v.version) === null).filter((v) => {
1469
+ const needs = v.rustVersion ? rust(v.rustVersion) : void 0;
1470
+ return !(project && needs && semver5.gt(needs, project));
1471
+ }).map((v) => v.version).sort(semver5.rcompare);
1472
+ }
1466
1473
  function selectVersion(versions, options) {
1467
1474
  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
1475
  const project = options.rustVersion ? rust(options.rustVersion) : void 0;
@@ -1549,7 +1556,7 @@ function cargoDiff(from, to) {
1549
1556
  }
1550
1557
 
1551
1558
  // src/adapters/cargo/cargo-scanner.ts
1552
- import { readFileSync as readFileSync10 } from "fs";
1559
+ import { readFileSync as readFileSync11 } from "fs";
1553
1560
  import { join as join11, relative as relative4 } from "path";
1554
1561
  import semver8 from "semver";
1555
1562
 
@@ -1621,6 +1628,20 @@ function locateSection(path) {
1621
1628
  return { section, inWorkspace, target, rest: path.slice(i + 1) };
1622
1629
  }
1623
1630
 
1631
+ // src/adapters/cargo/cargo-lockfile.ts
1632
+ import { existsSync as existsSync7, readFileSync as readFileSync10 } from "fs";
1633
+ import { getStaticTOMLValue as getStaticTOMLValue2, parseTOML as parseTOML3 } from "toml-eslint-parser";
1634
+ function readLockedVersions(lockFile) {
1635
+ const locked = /* @__PURE__ */ new Map();
1636
+ if (!existsSync7(lockFile)) return locked;
1637
+ try {
1638
+ const data = getStaticTOMLValue2(parseTOML3(readFileSync10(lockFile, "utf8")));
1639
+ for (const pkg of data.package ?? []) locked.set(pkg.name, [...locked.get(pkg.name) ?? [], pkg.version]);
1640
+ } catch {
1641
+ }
1642
+ return locked;
1643
+ }
1644
+
1624
1645
  // src/adapters/cargo/cargo-scanner.ts
1625
1646
  var CONCURRENCY2 = 6;
1626
1647
  var DIFF_ORDER2 = { major: 0, minor: 1, patch: 2 };
@@ -1634,6 +1655,7 @@ var CargoDependencyAdapter = class {
1634
1655
  const pkg = metadata.packages.find((p) => samePath(join11(p.manifest_path, ".."), module.path));
1635
1656
  if (!pkg) return { candidates: [], skipped: [] };
1636
1657
  const own = readDeclarations(pkg.manifest_path);
1658
+ const locks = readLockedVersions(join11(metadata.workspace_root, "Cargo.lock"));
1637
1659
  const rootManifest = join11(metadata.workspace_root, "Cargo.toml");
1638
1660
  const root = readDeclarations(rootManifest);
1639
1661
  const groups = groupDependencies(pkg.dependencies);
@@ -1650,7 +1672,7 @@ var CargoDependencyAdapter = class {
1650
1672
  return target ? alignment(primary, target, located, common) : null;
1651
1673
  }
1652
1674
  if (!primary.source || primary.source.startsWith("git+")) return null;
1653
- const outcome = await this.registryOutcome(primary, located, pkg, metadata, common, context);
1675
+ const outcome = await this.registryOutcome(primary, located, pkg, metadata, locks, common, context);
1654
1676
  const other = group.find((d) => d !== primary && !sameRequirement(d.req, primary.req));
1655
1677
  if (outcome && "candidate" in outcome && other) {
1656
1678
  outcome.note = {
@@ -1674,7 +1696,7 @@ var CargoDependencyAdapter = class {
1674
1696
  candidates.sort((a, b) => (DIFF_ORDER2[a.diff] ?? 3) - (DIFF_ORDER2[b.diff] ?? 3));
1675
1697
  return { candidates, skipped };
1676
1698
  }
1677
- async registryOutcome(dep, located, pkg, metadata, common, context) {
1699
+ async registryOutcome(dep, located, pkg, metadata, locks, common, context) {
1678
1700
  const literal = located?.version.value ?? dep.req;
1679
1701
  if (!isUpgradableRequirement(literal)) return null;
1680
1702
  if (!located || !sameRequirement(located.version.value, dep.req)) {
@@ -1708,10 +1730,10 @@ var CargoDependencyAdapter = class {
1708
1730
  if (floor && held && semver8.gt(held.version, floor)) {
1709
1731
  return { skipped: { identifier: dep.name, origin: origin.kind, reason: "incompatible-rust-version", detail: ` (${held.version} needs Rust ${held.requires})` } };
1710
1732
  }
1711
- return null;
1733
+ return yankedLock(dep, literal, origin, versions, locks);
1712
1734
  }
1713
1735
  const newRange = rewriteRequirement(literal, latest);
1714
- if (newRange === literal) return null;
1736
+ if (newRange === literal) return lockOutcome(dep, literal, origin, versions, pkg, metadata, locks, common, context);
1715
1737
  const tag = semver8.prerelease(latest) ? extractPreReleaseTag(latest) ?? "PRE" : void 0;
1716
1738
  const diff = cargoDiff(floor, latest);
1717
1739
  const candidate = {
@@ -1734,6 +1756,39 @@ var CargoDependencyAdapter = class {
1734
1756
  return { candidate };
1735
1757
  }
1736
1758
  };
1759
+ function lockOutcome(dep, requirement, origin, versions, pkg, metadata, locks, common, context) {
1760
+ const locked = (locks.get(dep.name) ?? []).filter((v) => satisfiesRequirement(requirement, v)).sort(semver8.rcompare)[0];
1761
+ if (!locked) return null;
1762
+ const options = { channel: context.channel, rustVersion: pkg.rust_version ?? void 0 };
1763
+ const allowed = versions.filter((v) => satisfiesRequirement(requirement, v.version));
1764
+ const newer = eligibleVersions(allowed, options).filter((v) => semver8.gt(v, locked));
1765
+ const latest = newer[0];
1766
+ if (!latest) return yankedLock(dep, requirement, origin, versions, locks);
1767
+ const lockFile = join11(metadata.workspace_root, "Cargo.lock");
1768
+ return {
1769
+ candidate: {
1770
+ coordinate: { identifier: dep.name, artifact: dep.name, ecosystem: "cargo", ...dep.registry ? { registry: dep.registry } : {} },
1771
+ currentRange: locked,
1772
+ currentClean: locked,
1773
+ latest,
1774
+ newRange: latest,
1775
+ diff: cargoDiff(locked, latest),
1776
+ scope: common.scope,
1777
+ // A release the requirement allows is compatible by definition, so it is safe to preselect.
1778
+ selected: true,
1779
+ origin,
1780
+ action: "lock",
1781
+ declaration: { file: lockFile, kind: "lockfile", displayPath: display(context.rootDir, lockFile) },
1782
+ availableVersions: newer,
1783
+ ...common.qualifier ? { qualifier: common.qualifier } : {}
1784
+ }
1785
+ };
1786
+ }
1787
+ function yankedLock(dep, requirement, origin, versions, locks) {
1788
+ const locked = (locks.get(dep.name) ?? []).filter((v) => satisfiesRequirement(requirement, v)).sort(semver8.rcompare)[0];
1789
+ if (!locked || !versions.some((v) => v.yanked && v.version === locked)) return null;
1790
+ return { skipped: { identifier: dep.name, origin: origin.kind, reason: "locked-version-yanked", detail: locked } };
1791
+ }
1737
1792
  function alignment(dep, target, located, common) {
1738
1793
  const local = target.version;
1739
1794
  if (!located || !local || !semver8.valid(local)) return null;
@@ -1772,7 +1827,7 @@ function groupDependencies(deps) {
1772
1827
  }
1773
1828
  function readDeclarations(manifestPath) {
1774
1829
  try {
1775
- return parseCargoManifest(readFileSync10(manifestPath, "utf8"));
1830
+ return parseCargoManifest(readFileSync11(manifestPath, "utf8"));
1776
1831
  } catch {
1777
1832
  return [];
1778
1833
  }
@@ -1964,20 +2019,6 @@ function applyRangeEdits(source, edits) {
1964
2019
  return result;
1965
2020
  }
1966
2021
 
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
2022
  // src/adapters/cargo/cargo-updater.ts
1982
2023
  var UPDATE_TIMEOUT_MS = 10 * 6e4;
1983
2024
  var workspaceDir = (module, rootDir) => module.aggregatorDir ?? module.path ?? rootDir;
@@ -1991,6 +2032,9 @@ var CargoUpdaterAdapter = class {
1991
2032
  const dir = workspaceDir(module, rootDir);
1992
2033
  const lockFile = join12(dir, "Cargo.lock");
1993
2034
  const lockedBefore = readLockedVersions(lockFile);
2035
+ const lockUpdates = updates.filter((u) => u.action === "lock");
2036
+ updates = updates.filter((u) => u.action !== "lock");
2037
+ this.checkLockUpdates(lockUpdates, lockFile, lockedBefore);
1994
2038
  const byFile = /* @__PURE__ */ new Map();
1995
2039
  for (const update of updates) {
1996
2040
  const file = resolve6(update.declaration?.file ?? join12(module.path, "Cargo.toml"));
@@ -2028,13 +2072,37 @@ var CargoUpdaterAdapter = class {
2028
2072
  if (existsSync8(lockFile) && registryUpdates.length > 0) {
2029
2073
  await this.refreshLock(dir, lockFile, lockedBefore, registryUpdates, onProgress);
2030
2074
  }
2075
+ for (const update of lockUpdates) {
2076
+ const name = update.coordinate.identifier;
2077
+ onProgress(`Locking ${name} at ${update.latest}\u2026`);
2078
+ await this.cargoUpdate(dir, ["-p", `${name}@${update.currentClean}`, "--precise", update.latest]);
2079
+ }
2080
+ const parts = [];
2081
+ if (updates.length > 0) {
2082
+ parts.push(`Updated ${updates.length} requirement(s) in ${writes.length} Cargo.toml file(s)${existsSync8(lockFile) ? "; Cargo.lock refreshed" : ""}`);
2083
+ }
2084
+ if (lockUpdates.length > 0) parts.push(`locked ${lockUpdates.length} crate(s) at a newer version in Cargo.lock`);
2085
+ const message2 = parts.join("; ");
2031
2086
  return {
2032
- updatedCount: updates.length,
2033
- auditMessage: `Updated ${updates.length} requirement(s) in ${writes.length} Cargo.toml file(s)${existsSync8(lockFile) ? "; Cargo.lock refreshed" : ""}.`,
2087
+ updatedCount: updates.length + lockUpdates.length,
2088
+ auditMessage: `${message2.charAt(0).toUpperCase()}${message2.slice(1)}.`,
2034
2089
  auditSeverity: "clean",
2035
2090
  integrityViolations: []
2036
2091
  };
2037
2092
  }
2093
+ /** A lock-only update needs a lock file that still holds the version the scan saw. */
2094
+ checkLockUpdates(lockUpdates, lockFile, locked) {
2095
+ if (lockUpdates.length === 0) return;
2096
+ if (!existsSync8(lockFile)) {
2097
+ throw new InstallError(`${lockFile} does not exist any more; rescan the module.`);
2098
+ }
2099
+ for (const update of lockUpdates) {
2100
+ const name = update.coordinate.identifier;
2101
+ if (!(locked.get(name) ?? []).includes(update.currentClean)) {
2102
+ throw new InstallError(`Cargo.lock no longer locks ${name} at ${update.currentClean}; rescan the module.`);
2103
+ }
2104
+ }
2105
+ }
2038
2106
  async resync() {
2039
2107
  }
2040
2108
  async refreshLock(dir, lockFile, before, updates, onProgress) {
@@ -2075,11 +2143,13 @@ var same = (a, b) => relative5(resolve7(a), resolve7(b)) === "";
2075
2143
  function planCheck(metadata, module, changedFiles, locked) {
2076
2144
  const args = ["check", "--all-targets", ...locked ? ["--locked"] : []];
2077
2145
  const rootManifest = join13(metadata.workspace_root, "Cargo.toml");
2146
+ const lockFile = join13(metadata.workspace_root, "Cargo.lock");
2078
2147
  const memberManifests = new Set(metadata.packages.map((p) => resolve7(p.manifest_path)));
2079
2148
  if (changedFiles.some((f) => same(f, rootManifest))) {
2080
2149
  const isVirtualRoot = !memberManifests.has(resolve7(rootManifest));
2081
2150
  if (isVirtualRoot || metadata.packages.length > 1) return [...args, "--workspace"];
2082
2151
  }
2152
+ if (changedFiles.length > 0 && changedFiles.every((f) => same(f, lockFile))) return [...args, "--workspace"];
2083
2153
  const changed = new Set(
2084
2154
  metadata.packages.filter((p) => changedFiles.some((f) => same(f, p.manifest_path))).map((p) => p.name)
2085
2155
  );
@@ -3480,6 +3550,7 @@ var de = {
3480
3550
  prod: "(prod/compile)",
3481
3551
  align: "[Angleichen]",
3482
3552
  internal: "[Intern]",
3553
+ lock: "[Lock]",
3483
3554
  shared: (count) => `[+${count} Crates]`
3484
3555
  },
3485
3556
  status: {
@@ -3591,6 +3662,7 @@ var en = {
3591
3662
  prod: "(prod/compile)",
3592
3663
  align: "[Align]",
3593
3664
  internal: "[Internal]",
3665
+ lock: "[Lock]",
3594
3666
  shared: (count) => `[+${count} crates]`
3595
3667
  },
3596
3668
  status: {
@@ -4361,6 +4433,10 @@ var PackageRow = ({ item, isFocused, t }) => {
4361
4433
  t.badges.align,
4362
4434
  " "
4363
4435
  ] }),
4436
+ item.action === "lock" && /* @__PURE__ */ jsxs5(Text5, { bold: true, color: theme.colors.brandLight, children: [
4437
+ t.badges.lock,
4438
+ " "
4439
+ ] }),
4364
4440
  item.origin.kind === "private" && /* @__PURE__ */ jsxs5(Text5, { color: theme.colors.brandLight, children: [
4365
4441
  t.badges.internal,
4366
4442
  " "
@@ -5309,6 +5385,8 @@ function describeSkip(skipped) {
5309
5385
  return `catalog entry could not be resolved${detail}`;
5310
5386
  case "incompatible-rust-version":
5311
5387
  return `newer versions need a newer Rust than the project's rust-version${detail}`;
5388
+ case "locked-version-yanked":
5389
+ return `Cargo.lock pins a yanked release and no allowed release can replace it${detail}`;
5312
5390
  }
5313
5391
  }
5314
5392
 
@@ -5397,7 +5475,7 @@ async function handleScanCommand(options, config) {
5397
5475
  if (result.candidates.length === 0) console.log(" \u2728 All dependencies are up to date.");
5398
5476
  for (const u of result.candidates) {
5399
5477
  const diffTag = `[${u.diff.toUpperCase()}]`.padEnd(9);
5400
- const actionTag = u.action === "align" ? "[ALIGN] " : u.origin.kind === "private" ? "[INTERNAL] " : "";
5478
+ const actionTag = u.action === "align" ? "[ALIGN] " : u.action === "lock" ? "[LOCK] " : u.origin.kind === "private" ? "[INTERNAL] " : "";
5401
5479
  const preBadge = u.isPreRelease ? `[${u.preReleaseTag || "PRE"}] ` : "";
5402
5480
  const where = u.declaration && u.declaration.displayPath !== modulePom ? ` (in ${u.declaration.displayPath}${u.declaration.propertyName ? ` \${${u.declaration.propertyName}}` : ""})` : "";
5403
5481
  console.log(
@@ -5534,6 +5612,8 @@ function retarget(found, target, options) {
5534
5612
  const isPreRelease = isPreReleaseVersion(target);
5535
5613
  return {
5536
5614
  ...found,
5615
+ // A lock update pins exactly the version that was picked.
5616
+ ...found.action === "lock" ? { latest: target } : {},
5537
5617
  newRange: policyOf(options).rewrite(found.currentRange, target),
5538
5618
  diff,
5539
5619
  selected: true,