sdkvm 1.0.0 → 1.0.1

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/index.js CHANGED
@@ -16,7 +16,7 @@ var SdkvmError = class extends Error {
16
16
  };
17
17
  function toSdkvmError(err) {
18
18
  if (err instanceof SdkvmError) return err;
19
- if (err instanceof Error) return new SdkvmError(err.message, { hint: err.stack });
19
+ if (err instanceof Error) return new SdkvmError(err.message);
20
20
  return new SdkvmError(String(err));
21
21
  }
22
22
 
@@ -24,16 +24,14 @@ function toSdkvmError(err) {
24
24
  import pc from "picocolors";
25
25
 
26
26
  // src/core/env.ts
27
- function envOverride(newName, legacyName) {
28
- const fresh = process.env[newName];
29
- if (fresh !== void 0 && fresh !== "") return fresh;
30
- const legacy = process.env[legacyName];
31
- if (legacy !== void 0 && legacy !== "") return legacy;
27
+ function envGet(name) {
28
+ const value = process.env[name];
29
+ if (value !== void 0 && value !== "") return value;
32
30
  return void 0;
33
31
  }
34
32
 
35
33
  // src/ui/log.ts
36
- var isQuiet = () => Boolean(envOverride("SDKVM_QUIET", "JVM_QUIET"));
34
+ var isQuiet = () => Boolean(envGet("SDKVM_QUIET"));
37
35
  var label = {
38
36
  info: pc.cyan("sdkvm"),
39
37
  ok: pc.green("sdkvm"),
@@ -77,9 +75,9 @@ function detectPlatform(override) {
77
75
  aarch64: "aarch64",
78
76
  x64: "x64"
79
77
  };
80
- const os6 = osMap[rawPlatform];
78
+ const os5 = osMap[rawPlatform];
81
79
  const arch = archMap[rawArch];
82
- if (!os6) {
80
+ if (!os5) {
83
81
  throw new SdkvmError(`Unsupported operating system: ${rawPlatform}`, {
84
82
  hint: "sdkvm currently supports macOS, Linux and Windows."
85
83
  });
@@ -89,11 +87,11 @@ function detectPlatform(override) {
89
87
  hint: "sdkvm currently supports aarch64 (Apple Silicon / ARM) and x64."
90
88
  });
91
89
  }
92
- return { os: os6, arch, rawPlatform, rawArch };
90
+ return { os: os5, arch, rawPlatform, rawArch };
93
91
  }
94
92
 
95
93
  // src/core/config.ts
96
- import fs4 from "fs";
94
+ import fs5 from "fs";
97
95
  import path4 from "path";
98
96
 
99
97
  // src/core/version.ts
@@ -295,6 +293,62 @@ function parseFlutterUserSpec(input) {
295
293
  hint: "Expected: 3.47, 3.47.5, 3.49.0-0.1.pre, latest, or with vendor prefix like flutter-3.47"
296
294
  });
297
295
  }
296
+ var NODE_VENDOR_IDS = ["nodejs"];
297
+ function parseNodeVersion(vendor, input) {
298
+ const raw = input.trim();
299
+ const s = raw.replace(/^nodejs-/i, "").replace(/^node-/i, "").replace(/^v/i, "");
300
+ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(s);
301
+ if (!m || !m[1] || !m[2] || !m[3]) {
302
+ throw new SdkvmError(`Invalid Node.js version: "${input}"`, {
303
+ hint: "Expected forms: 22, 22.20.0, lts, latest"
304
+ });
305
+ }
306
+ return {
307
+ vendor,
308
+ major: Number(m[1]),
309
+ minor: Number(m[2]),
310
+ patch: Number(m[3]),
311
+ extra: null,
312
+ build: null,
313
+ raw
314
+ };
315
+ }
316
+ function formatNodeVersion(v) {
317
+ return `${v.major}.${v.minor}.${v.patch ?? 0}`;
318
+ }
319
+ function parseNodeDirName(dir) {
320
+ const m = new RegExp(`^(${NODE_VENDOR_IDS.join("|")})-(.+)$`).exec(dir);
321
+ if (!m || !m[1] || !m[2]) return null;
322
+ try {
323
+ return parseNodeVersion(m[1], m[2]);
324
+ } catch {
325
+ return null;
326
+ }
327
+ }
328
+ function parseNodeUserSpec(input) {
329
+ let s = input.trim().toLowerCase();
330
+ let vendor;
331
+ const m = /^(nodejs|node)-(.+)$/.exec(s);
332
+ if (m && m[2]) {
333
+ vendor = "nodejs";
334
+ s = m[2];
335
+ }
336
+ if (s === "latest") return { vendor, spec: { kind: "latest" } };
337
+ if (s === "lts" || s === "--lts") return { vendor, spec: { kind: "lts" } };
338
+ if (/^\d+\.\d+\.\d+$/.test(s)) return { vendor, spec: { kind: "full", version: s } };
339
+ if (/^\d+$/.test(s)) return { vendor, spec: { kind: "major", major: Number(s) } };
340
+ if (/^\d+\.\d+$/.test(s)) {
341
+ throw new SdkvmError(`Invalid Node.js version: "${input}"`, {
342
+ hint: 'Node lines are major-only \u2014 use "22" or a full version like "22.20.0"'
343
+ });
344
+ }
345
+ throw new SdkvmError(`Invalid Node.js version: "${input}"`, {
346
+ hint: "Expected: 22, 22.20.0, lts, latest, or with vendor prefix like nodejs-22.20.0"
347
+ });
348
+ }
349
+
350
+ // src/core/lock.ts
351
+ import fs4 from "fs";
298
352
 
299
353
  // src/core/paths.ts
300
354
  import fs3 from "fs";
@@ -384,8 +438,8 @@ async function httpText(url) {
384
438
 
385
439
  // src/vendor/temurin.ts
386
440
  var API = "https://api.adoptium.net";
387
- async function resolveLatestRedirect(major, os6, arch) {
388
- const url = `${API}/v3/binary/latest/${major}/ga/${os6}/${arch}/jdk/hotspot/normal/eclipse`;
441
+ async function resolveLatestRedirect(major, os5, arch) {
442
+ const url = `${API}/v3/binary/latest/${major}/ga/${os5}/${arch}/jdk/hotspot/normal/eclipse`;
389
443
  const res = await httpFetch(url, { redirect: "manual" });
390
444
  if (res.status !== 302 && res.status !== 307 && res.status !== 308) {
391
445
  throw new SdkvmError(`Adoptium API returned ${res.status} for JDK ${major}`);
@@ -399,12 +453,12 @@ function versionFromGithubUrl(url) {
399
453
  if (!m || !m[1]) throw new SdkvmError(`Cannot parse version from Adoptium URL: ${url}`);
400
454
  return decodeURIComponent(m[1]);
401
455
  }
402
- function githubAssetUrl(version, os6, arch) {
456
+ function githubAssetUrl(version, os5, arch) {
403
457
  const v = parseVersion("temurin", version);
404
458
  const major = v.major;
405
- const ext = os6 === "windows" ? "zip" : "tar.gz";
459
+ const ext = os5 === "windows" ? "zip" : "tar.gz";
406
460
  const underscored = formatVersion(v).replace("+", "_");
407
- const file = `OpenJDK${major}U-jdk_${arch}_${os6}_hotspot_${underscored}.${ext}`;
461
+ const file = `OpenJDK${major}U-jdk_${arch}_${os5}_hotspot_${underscored}.${ext}`;
408
462
  return `https://github.com/adoptium/temurin${major}-binaries/releases/download/jdk-${encodeURIComponent(version)}/${file}`;
409
463
  }
410
464
  async function fetchAvailable() {
@@ -457,6 +511,12 @@ var temurinVendor = {
457
511
  }
458
512
  };
459
513
 
514
+ // src/cli/cmdname.ts
515
+ var CLI_BIN = "sdkvm";
516
+ function cmdPath(type) {
517
+ return type === "java" ? CLI_BIN : `${CLI_BIN} ${type}`;
518
+ }
519
+
460
520
  // src/vendor/zulu.ts
461
521
  var API2 = "https://api.azul.com/metadata/v1";
462
522
  function azulParams(platform) {
@@ -465,13 +525,20 @@ function azulParams(platform) {
465
525
  arch: platform.arch === "aarch64" ? "arm" : "x64"
466
526
  };
467
527
  }
528
+ function zuluVersionMatches(javaVersion, wanted) {
529
+ if (!wanted) return true;
530
+ const actual = javaVersion.join(".");
531
+ if (actual === wanted) return true;
532
+ if (/^\d+$/.test(wanted)) return javaVersion[0] === Number(wanted);
533
+ return actual.startsWith(`${wanted}.`);
534
+ }
468
535
  function pickPlainJdk(packages, platform, versionPrefix) {
469
536
  const ext = platform.os === "windows" ? ".zip" : ".tar.gz";
470
537
  const wanted = versionPrefix ?? "";
471
538
  const candidates = packages.filter((p) => {
472
539
  if (!/^zulu[\d.]+-ca-jdk[\d.]*-/i.test(p.name)) return false;
473
540
  if (!p.name.endsWith(ext)) return false;
474
- if (wanted && !p.java_version.join(".").startsWith(wanted)) return false;
541
+ if (wanted && !zuluVersionMatches(p.java_version, wanted)) return false;
475
542
  return true;
476
543
  });
477
544
  candidates.sort((a, b) => {
@@ -483,8 +550,8 @@ function pickPlainJdk(packages, platform, versionPrefix) {
483
550
  return candidates[candidates.length - 1] ?? null;
484
551
  }
485
552
  async function queryPackages(javaVersion, platform) {
486
- const { os: os6, arch } = azulParams(platform);
487
- const url = `${API2}/zulu/packages/?java_version=${javaVersion}&os=${os6}&arch=${arch}&hw_bitness=64&release_status=ga&page_size=50`;
553
+ const { os: os5, arch } = azulParams(platform);
554
+ const url = `${API2}/zulu/packages/?java_version=${javaVersion}&os=${os5}&arch=${arch}&hw_bitness=64&release_status=ga&page_size=50`;
488
555
  const data = await httpJson(url);
489
556
  return Array.isArray(data) ? data : [];
490
557
  }
@@ -495,14 +562,12 @@ var zuluVendor = {
495
562
  supportsFullVersionList: true,
496
563
  async listMajors() {
497
564
  const universe = await temurinVendor.listMajors();
565
+ const platform = detectPlatform();
498
566
  const results = await Promise.all(
499
567
  universe.map(async ({ key }) => {
500
568
  try {
501
- const packages = await queryPackages(key, {
502
- os: "mac",
503
- arch: "aarch64"
504
- });
505
- const pick = pickPlainJdk(packages, { os: "mac", arch: "aarch64" });
569
+ const packages = await queryPackages(key, platform);
570
+ const pick = pickPlainJdk(packages, platform);
506
571
  if (!pick) return null;
507
572
  return { key, lts: LTS_MAJORS.has(Number(key)), latestFullVersion: pick.java_version.join(".") };
508
573
  } catch {
@@ -530,7 +595,7 @@ var zuluVendor = {
530
595
  const pick = pickPlainJdk(packages, platform, versionPrefix);
531
596
  if (!pick) {
532
597
  throw new SdkvmError(`No Zulu JDK build matches "${versionPrefix}"`, {
533
- hint: "Run `jvm ls -r` to see available versions."
598
+ hint: `Run \`${cmdPath("java")} ls -r\` to see available versions.`
534
599
  });
535
600
  }
536
601
  const versionStr = pick.java_version.join(".");
@@ -619,12 +684,6 @@ var correttoVendor = {
619
684
  }
620
685
  };
621
686
 
622
- // src/cli/cmdname.ts
623
- var CLI_BIN = "sdkvm";
624
- function cmdPath(type) {
625
- return type === "java" ? CLI_BIN : `${CLI_BIN} ${type}`;
626
- }
627
-
628
687
  // src/vendor/golang.ts
629
688
  var LIST_URL = "https://go.dev/dl/?mode=json&include=all";
630
689
  var GO_OS = { mac: "darwin", linux: "linux", windows: "windows" };
@@ -791,14 +850,102 @@ var flutterVendor = {
791
850
  }
792
851
  };
793
852
 
853
+ // src/vendor/nodejs.ts
854
+ var DIST = "https://nodejs.org/dist";
855
+ function tryParseEntry(e) {
856
+ try {
857
+ return parseNodeVersion("nodejs", e.version);
858
+ } catch {
859
+ return null;
860
+ }
861
+ }
862
+ async function fetchIndex() {
863
+ const entries = await httpJson(`${DIST}/index.json`);
864
+ return entries.map((e) => ({ e, v: tryParseEntry(e) })).filter((x) => x.v !== null).sort((a, b) => compareVersions(b.v, a.v)).map((x) => x.e);
865
+ }
866
+ async function fetchShasums(version) {
867
+ const text = await httpText(`${DIST}/${version}/SHASUMS256.txt`);
868
+ const sums = /* @__PURE__ */ new Map();
869
+ for (const line of text.split("\n")) {
870
+ const m = /^([0-9a-f]{64})\s+(\S+)$/.exec(line.trim());
871
+ if (m && m[1] && m[2]) sums.set(m[2], m[1]);
872
+ }
873
+ return sums;
874
+ }
875
+ var nodejsVendor = {
876
+ id: "nodejs",
877
+ label: "Node.js",
878
+ sdk: "node",
879
+ supportsFullVersionList: true,
880
+ async listMajors() {
881
+ const entries = await fetchIndex();
882
+ const lines = [];
883
+ const seen = /* @__PURE__ */ new Set();
884
+ for (const e of entries) {
885
+ const v = tryParseEntry(e);
886
+ if (!v || seen.has(v.major)) continue;
887
+ seen.add(v.major);
888
+ lines.push({ key: String(v.major), lts: e.lts !== false, latestFullVersion: formatNodeVersion(v) });
889
+ }
890
+ return lines;
891
+ },
892
+ async resolve(spec, platform) {
893
+ const entries = await fetchIndex();
894
+ let entry;
895
+ if (spec.kind === "latest") {
896
+ entry = entries[0];
897
+ } else if (spec.kind === "lts") {
898
+ entry = entries.find((e) => e.lts !== false);
899
+ } else if (spec.kind === "major") {
900
+ entry = entries.find((e) => tryParseEntry(e)?.major === spec.major);
901
+ } else if (spec.kind === "full") {
902
+ entry = entries.find((e) => {
903
+ const v = tryParseEntry(e);
904
+ return v !== null && formatNodeVersion(v) === spec.version.replace(/^v/, "");
905
+ });
906
+ }
907
+ if (!entry) {
908
+ const what = spec.kind === "full" ? spec.version : spec.kind === "major" ? String(spec.major) : spec.kind;
909
+ throw new SdkvmError(`No Node.js release matches "${what}"`, {
910
+ hint: "Run `sdkvm node ls -r` to list available major lines"
911
+ });
912
+ }
913
+ const version = parseNodeVersion("nodejs", entry.version);
914
+ const sums = await fetchShasums(entry.version);
915
+ const plat = platform.os === "mac" ? "darwin" : platform.os === "linux" ? "linux" : "win";
916
+ const arch = platform.arch === "aarch64" ? "arm64" : "x64";
917
+ const base = `node-${entry.version}-${plat}-${arch}`;
918
+ const candidates = platform.os === "windows" ? [`${base}.zip`] : [`${base}.tar.xz`, `${base}.tar.gz`];
919
+ const filename = candidates.find((f) => sums.has(f));
920
+ if (!filename) {
921
+ throw new SdkvmError(`No Node.js ${formatNodeVersion(version)} archive for ${platform.os}/${platform.arch}`, {
922
+ hint: "Run `sdkvm node ls -r` and pick a line that ships this platform (arm64 needs newer lines)"
923
+ });
924
+ }
925
+ const archive = filename.endsWith(".zip") ? "zip" : filename.endsWith(".tar.xz") ? "tar.xz" : "tar.gz";
926
+ const display = formatNodeVersion(version);
927
+ return {
928
+ vendorId: "nodejs",
929
+ version,
930
+ dirName: `nodejs-${display}`,
931
+ displayName: `Node.js ${display}`,
932
+ downloadUrl: `${DIST}/${entry.version}/${filename}`,
933
+ checksum: { kind: "sha256", expected: sums.get(filename) },
934
+ archive
935
+ };
936
+ }
937
+ };
938
+
794
939
  // src/vendor/index.ts
795
940
  var JAVA_VENDORS = [temurinVendor, zuluVendor, correttoVendor];
796
941
  var GO_VENDORS = [golangVendor];
797
942
  var FLUTTER_VENDORS = [flutterVendor];
943
+ var NODE_VENDORS = [nodejsVendor];
798
944
  function vendorsFor(type) {
799
945
  if (type === "java") return JAVA_VENDORS;
800
946
  if (type === "go") return GO_VENDORS;
801
947
  if (type === "flutter") return FLUTTER_VENDORS;
948
+ if (type === "node") return NODE_VENDORS;
802
949
  throw new SdkvmError(`Unknown SDK type: ${type}`);
803
950
  }
804
951
  function allVendorIds(type) {
@@ -833,6 +980,7 @@ var javaSdk = {
833
980
  currentLinkName: "current-java",
834
981
  envVar: "JAVA_HOME",
835
982
  supportsLts: true,
983
+ isLtsMajor: (major) => LTS_MAJORS.has(major),
836
984
  vendors: JAVA_VENDORS,
837
985
  parseUserSpec,
838
986
  parseDirName,
@@ -841,6 +989,9 @@ var javaSdk = {
841
989
  binRelPath(platform) {
842
990
  return `bin/${platform.os === "windows" ? "java.exe" : "java"}`;
843
991
  },
992
+ envBinSuffix(platform) {
993
+ return platform.os === "windows" ? "\\bin" : "/bin";
994
+ },
844
995
  locateHome(root) {
845
996
  const contentsHome = path2.join(root, "Contents", "Home");
846
997
  return fs2.existsSync(path2.join(contentsHome, "bin")) ? contentsHome : root;
@@ -854,7 +1005,7 @@ var goSdk = {
854
1005
  label: "Go",
855
1006
  installDirName: "gos",
856
1007
  currentLinkName: "current-go",
857
- envVar: "GOROOT",
1008
+ envVar: "GO_HOME",
858
1009
  supportsLts: false,
859
1010
  vendors: GO_VENDORS,
860
1011
  parseUserSpec: parseGoUserSpec,
@@ -864,6 +1015,9 @@ var goSdk = {
864
1015
  binRelPath(platform) {
865
1016
  return `bin/${platform.os === "windows" ? "go.exe" : "go"}`;
866
1017
  },
1018
+ envBinSuffix(platform) {
1019
+ return platform.os === "windows" ? "\\bin" : "/bin";
1020
+ },
867
1021
  // go 归档是单根 go/ 目录,无 macOS bundle 概念
868
1022
  locateHome: (root) => root,
869
1023
  versionCheck: { args: ["version"], stream: "stdout" }
@@ -875,7 +1029,7 @@ var flutterSdk = {
875
1029
  label: "Flutter",
876
1030
  installDirName: "flutters",
877
1031
  currentLinkName: "current-flutter",
878
- envVar: "FLUTTER_ROOT",
1032
+ envVar: "FLUTTER_HOME",
879
1033
  supportsLts: false,
880
1034
  vendors: FLUTTER_VENDORS,
881
1035
  parseUserSpec: parseFlutterUserSpec,
@@ -885,18 +1039,48 @@ var flutterSdk = {
885
1039
  binRelPath(platform) {
886
1040
  return `bin/${platform.os === "windows" ? "flutter.bat" : "flutter"}`;
887
1041
  },
1042
+ envBinSuffix(platform) {
1043
+ return platform.os === "windows" ? "\\bin" : "/bin";
1044
+ },
888
1045
  // flutter 归档是单根 flutter/ 目录,无 macOS bundle 概念
889
1046
  locateHome: (root) => root,
890
1047
  versionCheck: { args: ["--version"], stream: "stdout" }
891
1048
  };
892
1049
 
1050
+ // src/sdk/node.ts
1051
+ var nodeSdk = {
1052
+ id: "node",
1053
+ label: "Node.js",
1054
+ installDirName: "nodes",
1055
+ currentLinkName: "current-node",
1056
+ envVar: "NODE_HOME",
1057
+ supportsLts: true,
1058
+ // Node.js LTS 发布线为偶数年 major(18 / 20 / 22 / 24 …)
1059
+ isLtsMajor: (major) => major >= 4 && major % 2 === 0,
1060
+ vendors: NODE_VENDORS,
1061
+ parseUserSpec: parseNodeUserSpec,
1062
+ parseDirName: parseNodeDirName,
1063
+ formatVersion: formatNodeVersion,
1064
+ compareVersions,
1065
+ binRelPath(platform) {
1066
+ return platform.os === "windows" ? "node.exe" : "bin/node";
1067
+ },
1068
+ envBinSuffix(platform) {
1069
+ return platform.os === "windows" ? "" : "/bin";
1070
+ },
1071
+ // node 归档是单根 node-v{ver}-{plat}-{arch}/ 目录
1072
+ locateHome: (root) => root,
1073
+ versionCheck: { args: ["--version"], stream: "stdout" }
1074
+ };
1075
+
893
1076
  // src/sdk/index.ts
894
1077
  var SPECS = {
895
1078
  java: javaSdk,
896
1079
  go: goSdk,
897
- flutter: flutterSdk
1080
+ flutter: flutterSdk,
1081
+ node: nodeSdk
898
1082
  };
899
- var SDK_TYPES = ["java", "go", "flutter"];
1083
+ var SDK_TYPES = ["java", "go", "flutter", "node"];
900
1084
  function getSdkType(id) {
901
1085
  const spec = SPECS[id];
902
1086
  if (!spec) throw new SdkvmError(`Unknown SDK type: ${id}`);
@@ -905,7 +1089,7 @@ function getSdkType(id) {
905
1089
 
906
1090
  // src/core/paths.ts
907
1091
  function sdkvmHome() {
908
- return envOverride("SDKVM_HOME", "JVM_HOME") ?? path3.join(os.homedir(), ".sdkvm");
1092
+ return envGet("SDKVM_HOME") ?? path3.join(os.homedir(), ".sdkvm");
909
1093
  }
910
1094
  var paths = {
911
1095
  root: sdkvmHome,
@@ -925,116 +1109,192 @@ function ensureLayout() {
925
1109
  }
926
1110
  }
927
1111
 
1112
+ // src/core/lock.ts
1113
+ var STALE_MS = 5 * 60 * 1e3;
1114
+ var HEARTBEAT_MS = 60 * 1e3;
1115
+ function lockInfoPath() {
1116
+ return `${paths.lock()}/info.json`;
1117
+ }
1118
+ function readLockPid() {
1119
+ try {
1120
+ const raw = fs4.readFileSync(lockInfoPath(), "utf8");
1121
+ const parsed = JSON.parse(raw);
1122
+ return typeof parsed.pid === "number" ? parsed.pid : null;
1123
+ } catch {
1124
+ return null;
1125
+ }
1126
+ }
1127
+ function isProcessAlive(pid) {
1128
+ if (pid <= 0) return false;
1129
+ try {
1130
+ process.kill(pid, 0);
1131
+ return true;
1132
+ } catch {
1133
+ return false;
1134
+ }
1135
+ }
1136
+ function writeLockInfo(startedAt) {
1137
+ fs4.writeFileSync(
1138
+ lockInfoPath(),
1139
+ JSON.stringify({ pid: process.pid, startedAt, heartbeatAt: Date.now() })
1140
+ );
1141
+ }
1142
+ function touchLock() {
1143
+ const lockDir = paths.lock();
1144
+ if (!fs4.existsSync(lockDir)) return;
1145
+ const now = /* @__PURE__ */ new Date();
1146
+ try {
1147
+ fs4.utimesSync(lockDir, now, now);
1148
+ } catch {
1149
+ }
1150
+ try {
1151
+ const pid = readLockPid() ?? process.pid;
1152
+ let startedAt = Date.now();
1153
+ try {
1154
+ const raw = JSON.parse(fs4.readFileSync(lockInfoPath(), "utf8"));
1155
+ if (typeof raw.startedAt === "number") startedAt = raw.startedAt;
1156
+ } catch {
1157
+ }
1158
+ if (pid === process.pid) writeLockInfo(startedAt);
1159
+ } catch {
1160
+ }
1161
+ }
1162
+ function acquireLock() {
1163
+ const lockDir = paths.lock();
1164
+ fs4.mkdirSync(paths.root(), { recursive: true });
1165
+ try {
1166
+ fs4.mkdirSync(lockDir);
1167
+ writeLockInfo(Date.now());
1168
+ } catch (err) {
1169
+ const e = err;
1170
+ if (e.code === "EEXIST") {
1171
+ const holder = readLockPid();
1172
+ if (holder != null && !isProcessAlive(holder)) {
1173
+ fs4.rmSync(lockDir, { recursive: true, force: true });
1174
+ return acquireLock();
1175
+ }
1176
+ const stat = fs4.statSync(lockDir);
1177
+ if (Date.now() - stat.mtimeMs > STALE_MS) {
1178
+ fs4.rmSync(lockDir, { recursive: true, force: true });
1179
+ return acquireLock();
1180
+ }
1181
+ throw new SdkvmError("Another sdkvm operation is in progress", {
1182
+ hint: "If this is wrong, remove ~/.sdkvm/.lock manually."
1183
+ });
1184
+ }
1185
+ throw e;
1186
+ }
1187
+ }
1188
+ function releaseLock() {
1189
+ fs4.rmSync(paths.lock(), { recursive: true, force: true });
1190
+ }
1191
+ async function withLock(fn) {
1192
+ acquireLock();
1193
+ const timer = setInterval(() => touchLock(), HEARTBEAT_MS);
1194
+ timer.unref?.();
1195
+ try {
1196
+ return await fn();
1197
+ } finally {
1198
+ clearInterval(timer);
1199
+ releaseLock();
1200
+ }
1201
+ }
1202
+
928
1203
  // src/core/config.ts
929
1204
  var DEFAULT_CONFIG = {
930
1205
  version: 1,
931
1206
  defaultVendor: "temurin",
932
- mirror: {}
1207
+ mirror: {},
1208
+ npmRegistries: {}
933
1209
  };
934
1210
  function loadConfig() {
935
1211
  const file = paths.config();
936
- if (!fs4.existsSync(file)) return { ...DEFAULT_CONFIG, mirror: {} };
1212
+ if (!fs5.existsSync(file)) return { ...DEFAULT_CONFIG, mirror: {}, npmRegistries: {} };
937
1213
  try {
938
- const parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
1214
+ const parsed = JSON.parse(fs5.readFileSync(file, "utf8"));
939
1215
  const config = {
940
1216
  version: 1,
941
1217
  defaultVendor: parsed.defaultVendor && JAVA_VENDOR_IDS.includes(parsed.defaultVendor) ? parsed.defaultVendor : "temurin",
942
- mirror: {}
1218
+ mirror: {},
1219
+ npmRegistries: {}
943
1220
  };
944
1221
  if (parsed.mirror && typeof parsed.mirror === "object") {
945
1222
  for (const [id, v] of Object.entries(parsed.mirror)) {
946
1223
  if (typeof v === "string" && v.length > 0) config.mirror[id] = v;
947
1224
  }
948
1225
  }
1226
+ if (parsed.npmRegistries && typeof parsed.npmRegistries === "object") {
1227
+ for (const [id, v] of Object.entries(parsed.npmRegistries)) {
1228
+ if (typeof v === "string" && v.length > 0) config.npmRegistries[id] = v;
1229
+ }
1230
+ }
949
1231
  return config;
950
1232
  } catch (err) {
951
1233
  const bak = `${file}.bak`;
952
1234
  try {
953
- fs4.renameSync(file, bak);
1235
+ fs5.renameSync(file, bak);
954
1236
  log.warn(`config.json was corrupted; backed up to ${bak}, using defaults`);
955
1237
  } catch {
956
1238
  }
957
1239
  void err;
958
- return { ...DEFAULT_CONFIG, mirror: {} };
1240
+ return { ...DEFAULT_CONFIG, mirror: {}, npmRegistries: {} };
959
1241
  }
960
1242
  }
961
1243
  function saveConfig(config) {
962
1244
  ensureLayout();
963
1245
  const file = paths.config();
964
1246
  const tmp = path4.join(path4.dirname(file), `.config.json.tmp-${process.pid}`);
965
- fs4.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
1247
+ fs5.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
966
1248
  `);
967
- fs4.renameSync(tmp, file);
968
- }
969
-
970
- // src/core/lock.ts
971
- import fs5 from "fs";
972
- var STALE_MS = 5 * 60 * 1e3;
973
- function acquireLock() {
974
- const lockDir = paths.lock();
975
- fs5.mkdirSync(paths.root(), { recursive: true });
976
- try {
977
- fs5.mkdirSync(lockDir);
978
- fs5.writeFileSync(
979
- `${lockDir}/info.json`,
980
- JSON.stringify({ pid: process.pid, startedAt: Date.now() })
981
- );
982
- } catch (err) {
983
- const e = err;
984
- if (e.code === "EEXIST") {
985
- const stat = fs5.statSync(lockDir);
986
- if (Date.now() - stat.mtimeMs > STALE_MS) {
987
- fs5.rmSync(lockDir, { recursive: true, force: true });
988
- return acquireLock();
989
- }
990
- throw new SdkvmError("Another sdkvm operation is in progress", {
991
- hint: "If this is wrong, remove ~/.sdkvm/.lock manually."
992
- });
993
- }
994
- throw e;
995
- }
996
- }
997
- function releaseLock() {
998
- fs5.rmSync(paths.lock(), { recursive: true, force: true });
1249
+ fs5.renameSync(tmp, file);
999
1250
  }
1000
- async function withLock(fn) {
1251
+ function updateConfig(mutator) {
1001
1252
  acquireLock();
1002
1253
  try {
1003
- return await fn();
1254
+ const config = loadConfig();
1255
+ mutator(config);
1256
+ saveConfig(config);
1257
+ return config;
1004
1258
  } finally {
1005
1259
  releaseLock();
1006
1260
  }
1007
1261
  }
1008
1262
 
1009
1263
  // src/vendor/mirror.ts
1010
- function applyMirror(artifact, platform, mirrorRoot) {
1011
- if (!mirrorRoot) return artifact;
1264
+ var MIRROR_REWRITE_VENDORS = /* @__PURE__ */ new Set(["temurin", "golang", "flutter", "nodejs"]);
1265
+ function applyMirrorDetail(artifact, platform, mirrorRoot) {
1266
+ if (!mirrorRoot?.trim()) return { artifact, applied: false };
1267
+ const root = mirrorRoot.trim().replace(/\/+$/, "");
1012
1268
  if (artifact.vendorId === "golang") {
1013
- const root2 = mirrorRoot.replace(/\/+$/, "");
1014
1269
  const file = artifact.downloadUrl.split("/").pop();
1015
- if (!file) return artifact;
1016
- return { ...artifact, downloadUrl: `${root2}/${file}` };
1270
+ if (!file) return { artifact, applied: false };
1271
+ return { artifact: { ...artifact, downloadUrl: `${root}/${file}` }, applied: true };
1017
1272
  }
1018
1273
  if (artifact.vendorId === "flutter") {
1019
- const root2 = mirrorRoot.replace(/\/+$/, "");
1020
1274
  const url2 = artifact.downloadUrl.replace(
1021
1275
  /^https:\/\/storage\.googleapis\.com\/flutter_infra_release/,
1022
- root2
1276
+ root
1023
1277
  );
1024
- return { ...artifact, downloadUrl: url2 };
1278
+ if (url2 === artifact.downloadUrl) return { artifact, applied: false };
1279
+ return { artifact: { ...artifact, downloadUrl: url2 }, applied: true };
1280
+ }
1281
+ if (artifact.vendorId === "nodejs") {
1282
+ const url2 = artifact.downloadUrl.replace(/^https:\/\/nodejs\.org\/dist/, root);
1283
+ if (url2 === artifact.downloadUrl) return { artifact, applied: false };
1284
+ return { artifact: { ...artifact, downloadUrl: url2 }, applied: true };
1025
1285
  }
1026
- if (artifact.vendorId !== "temurin") return artifact;
1286
+ if (artifact.vendorId !== "temurin") return { artifact, applied: false };
1027
1287
  const m = /^https:\/\/github\.com\/adoptium\/temurin(\d+)-binaries\/releases\/download\/[^/]+\/(.+)$/.exec(
1028
1288
  artifact.downloadUrl
1029
1289
  );
1030
- if (!m || !m[1] || !m[2]) return artifact;
1031
- const root = mirrorRoot.replace(/\/+$/, "");
1290
+ if (!m || !m[1] || !m[2]) return { artifact, applied: false };
1032
1291
  const url = `${root}/${m[1]}/jdk/${platform.arch}/${platform.os}/${m[2]}`;
1033
- return { ...artifact, downloadUrl: url };
1292
+ return { artifact: { ...artifact, downloadUrl: url }, applied: true };
1034
1293
  }
1035
1294
 
1036
1295
  // src/net/download.ts
1037
1296
  import crypto from "crypto";
1297
+ import { once } from "events";
1038
1298
  import fs6 from "fs";
1039
1299
  import path5 from "path";
1040
1300
  var IDLE_TIMEOUT_MS = 6e4;
@@ -1044,6 +1304,7 @@ async function downloadFile(url, destFile, onProgress) {
1044
1304
  const out = fs6.createWriteStream(partFile);
1045
1305
  let bytes = 0;
1046
1306
  let total = null;
1307
+ let encoded2 = false;
1047
1308
  let stalled = false;
1048
1309
  const ac = new AbortController();
1049
1310
  const timer = setTimeout(() => {
@@ -1051,13 +1312,17 @@ async function downloadFile(url, destFile, onProgress) {
1051
1312
  ac.abort();
1052
1313
  }, IDLE_TIMEOUT_MS);
1053
1314
  try {
1054
- const res = await httpFetch(url, { signal: ac.signal });
1315
+ const res = await httpFetch(url, {
1316
+ signal: ac.signal,
1317
+ headers: { "accept-encoding": "identity" }
1318
+ });
1055
1319
  timer.refresh();
1056
1320
  total = Number(res.headers.get("content-length")) || null;
1321
+ encoded2 = Boolean(res.headers.get("content-encoding"));
1057
1322
  if (!res.body) throw new SdkvmError(`Empty response body: ${url}`);
1058
1323
  for await (const chunk of res.body) {
1059
1324
  timer.refresh();
1060
- out.write(chunk);
1325
+ if (!out.write(chunk)) await once(out, "drain");
1061
1326
  hash.update(chunk);
1062
1327
  bytes += chunk.byteLength;
1063
1328
  onProgress?.(bytes, total);
@@ -1084,7 +1349,7 @@ async function downloadFile(url, destFile, onProgress) {
1084
1349
  fs6.rmSync(partFile, { force: true });
1085
1350
  throw new SdkvmError(`Download incomplete: 0 bytes`, { hint: url });
1086
1351
  }
1087
- if (total !== null && bytes !== total) {
1352
+ if (total !== null && !encoded2 && bytes !== total) {
1088
1353
  fs6.rmSync(partFile, { force: true });
1089
1354
  throw new SdkvmError(`Download incomplete: ${bytes}/${total} bytes`, { hint: url });
1090
1355
  }
@@ -1111,9 +1376,15 @@ function extractExpectedChecksum(text) {
1111
1376
  const token = trimmed.split(/\s+/)[0] ?? "";
1112
1377
  return HEX64.test(token) ? token.toLowerCase() : null;
1113
1378
  }
1114
- async function verifyChecksum(artifact, actual) {
1379
+ async function verifyChecksum(artifact, actual, opts = {}) {
1380
+ const strict = opts.strict === true;
1115
1381
  const info = artifact.checksum;
1116
1382
  if (!info) {
1383
+ if (strict) {
1384
+ throw new SdkvmError(`No checksum source for ${artifact.displayName}`, {
1385
+ hint: "Mirrored downloads require a verifiable checksum; unset the mirror or use the official source."
1386
+ });
1387
+ }
1117
1388
  log.warn(`no checksum source for ${artifact.displayName}, skipping verification`);
1118
1389
  return;
1119
1390
  }
@@ -1121,12 +1392,23 @@ async function verifyChecksum(artifact, actual) {
1121
1392
  if (!expected && info.url) {
1122
1393
  try {
1123
1394
  expected = extractExpectedChecksum(await httpText(info.url));
1124
- } catch {
1395
+ } catch (err) {
1396
+ if (strict) {
1397
+ const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
1398
+ throw new SdkvmError(`Cannot fetch checksum for ${artifact.displayName}`, {
1399
+ hint: `${detail}. Mirrored installs need the official checksum URL to be reachable.`
1400
+ });
1401
+ }
1125
1402
  log.warn(`cannot fetch checksum for ${artifact.displayName}, skipping verification`);
1126
1403
  return;
1127
1404
  }
1128
1405
  }
1129
1406
  if (!expected) {
1407
+ if (strict) {
1408
+ throw new SdkvmError(`Checksum source has no valid hash for ${artifact.displayName}`, {
1409
+ hint: "Mirrored downloads require a verifiable checksum."
1410
+ });
1411
+ }
1130
1412
  log.warn(`checksum source has no valid hash for ${artifact.displayName}, skipping`);
1131
1413
  return;
1132
1414
  }
@@ -1252,6 +1534,18 @@ function createProgress(label2) {
1252
1534
  }
1253
1535
 
1254
1536
  // src/cli/install.ts
1537
+ async function renameWithRetry(from, to, attempts = 3) {
1538
+ for (let i = 1; ; i++) {
1539
+ try {
1540
+ fs9.renameSync(from, to);
1541
+ return;
1542
+ } catch (err) {
1543
+ if (i >= attempts) throw err;
1544
+ log.warn(`rename blocked (attempt ${i}/${attempts}), retrying in 1s ...`);
1545
+ await new Promise((r) => setTimeout(r, 1e3));
1546
+ }
1547
+ }
1548
+ }
1255
1549
  async function installCommand(type, specInput, opts) {
1256
1550
  const platform = detectPlatform();
1257
1551
  const config = loadConfig();
@@ -1260,20 +1554,31 @@ async function installCommand(type, specInput, opts) {
1260
1554
  const vendor = getVendor(type, vendorId);
1261
1555
  log.info(`resolving ${vendor.label} ${specInput} for ${platform.os}/${platform.arch} ...`);
1262
1556
  const resolved = await vendor.resolve(spec, platform);
1263
- const mirrorRoot = envOverride("SDKVM_MIRROR", "JVM_MIRROR") ?? config.mirror[vendorId] ?? null;
1264
- const artifact = applyMirror(resolved, platform, mirrorRoot);
1265
- const finalDir = path8.join(paths.sdks(type), artifact.dirName);
1266
- const hintVersion = type === "java" ? String(artifact.version.major) : `${artifact.version.major}.${artifact.version.minor}`;
1267
- if (fs9.existsSync(finalDir)) {
1268
- if (!opts.force) {
1269
- log.warn(`${artifact.displayName} is already installed`);
1270
- log.info(`run: ${cmdPath(type)} use ${hintVersion}`);
1271
- return;
1557
+ const mirrorRoot = envGet("SDKVM_MIRROR") ?? config.mirror[vendorId] ?? null;
1558
+ const { artifact, applied } = applyMirrorDetail(resolved, platform, mirrorRoot);
1559
+ if (mirrorRoot?.trim() && !applied) {
1560
+ if (!MIRROR_REWRITE_VENDORS.has(vendorId)) {
1561
+ log.warn(
1562
+ `mirror is set but ${vendorId} downloads are not mirrored; using the official URL`
1563
+ );
1564
+ } else {
1565
+ log.warn(
1566
+ `mirror root did not rewrite the download URL for ${vendorId}; using the official source`
1567
+ );
1272
1568
  }
1273
- log.warn(`--force: removing existing ${artifact.dirName}`);
1274
1569
  }
1570
+ const finalDir = path8.join(paths.sdks(type), artifact.dirName);
1571
+ const hintVersion = type === "java" || type === "node" ? String(artifact.version.major) : `${artifact.version.major}.${artifact.version.minor}`;
1275
1572
  await withLock(async () => {
1276
1573
  ensureLayout();
1574
+ if (fs9.existsSync(finalDir)) {
1575
+ if (!opts.force) {
1576
+ log.warn(`${artifact.displayName} is already installed`);
1577
+ log.info(`run: ${cmdPath(type)} use ${hintVersion}`);
1578
+ return;
1579
+ }
1580
+ log.warn(`--force: removing existing ${artifact.dirName}`);
1581
+ }
1277
1582
  for (const f of fs9.readdirSync(paths.cache())) {
1278
1583
  if (f.endsWith(".part")) fs9.rmSync(path8.join(paths.cache(), f), { force: true });
1279
1584
  }
@@ -1282,28 +1587,41 @@ async function installCommand(type, specInput, opts) {
1282
1587
  log.info(`downloading ${artifact.downloadUrl}`);
1283
1588
  const dl = await downloadFile(artifact.downloadUrl, dest, (b, t) => progress.update(b, t));
1284
1589
  progress.done(dl.bytes, null);
1285
- await verifyChecksum(artifact, dl.sha256);
1590
+ await verifyChecksum(artifact, dl.sha256, { strict: applied });
1286
1591
  const tmp = tmpExtractDir(paths.tmp());
1592
+ const bak = `${finalDir}.bak`;
1287
1593
  let finalTmp = tmp;
1288
1594
  try {
1289
1595
  log.info("extracting ...");
1290
1596
  await extractArchive(dest, artifact.archive, tmp, platform);
1291
1597
  const normalized = normalizeExtracted(tmp, platform, type);
1292
1598
  finalTmp = normalized.root;
1293
- if (fs9.existsSync(finalDir)) fs9.rmSync(finalDir, { recursive: true, force: true });
1294
- fs9.renameSync(normalized.root, finalDir);
1599
+ fs9.rmSync(bak, { recursive: true, force: true });
1600
+ if (fs9.existsSync(finalDir)) fs9.renameSync(finalDir, bak);
1601
+ try {
1602
+ await renameWithRetry(normalized.root, finalDir);
1603
+ } catch (err) {
1604
+ if (fs9.existsSync(bak) && !fs9.existsSync(finalDir)) {
1605
+ try {
1606
+ fs9.renameSync(bak, finalDir);
1607
+ } catch {
1608
+ }
1609
+ }
1610
+ throw err;
1611
+ }
1612
+ fs9.rmSync(bak, { recursive: true, force: true });
1295
1613
  } catch (err) {
1296
1614
  fs9.rmSync(finalTmp, { recursive: true, force: true });
1297
1615
  fs9.rmSync(tmp, { recursive: true, force: true });
1298
- throw err instanceof SdkvmError ? err : err;
1616
+ throw err;
1299
1617
  } finally {
1300
1618
  fs9.rmSync(dest, { force: true });
1301
1619
  fs9.rmSync(paths.tmp(), { recursive: true, force: true });
1302
1620
  fs9.mkdirSync(paths.tmp(), { recursive: true });
1303
1621
  }
1622
+ log.ok(`installed ${artifact.displayName} \u2192 ${finalDir}`);
1623
+ log.info(`switch to it: ${cmdPath(type)} use ${hintVersion}`);
1304
1624
  });
1305
- log.ok(`installed ${artifact.displayName} \u2192 ${finalDir}`);
1306
- log.info(`switch to it: ${cmdPath(type)} use ${hintVersion}`);
1307
1625
  }
1308
1626
 
1309
1627
  // src/cli/use.ts
@@ -1391,12 +1709,13 @@ function findInstalled(type, specInput, vendorArg) {
1391
1709
  (j) => j.version.major === parsed.major && j.version.minor === parsed.minor
1392
1710
  );
1393
1711
  } else if (parsed.kind === "lts") {
1394
- if (!spec.supportsLts) {
1395
- throw new SdkvmError("Go has no LTS releases", {
1712
+ const isLtsMajor = spec.isLtsMajor;
1713
+ if (!spec.supportsLts || !isLtsMajor) {
1714
+ throw new SdkvmError(`${spec.label} has no LTS releases`, {
1396
1715
  hint: `Try: ${cmdPath(type)} install latest`
1397
1716
  });
1398
1717
  }
1399
- matched = candidates.filter((j) => LTS_MAJORS.has(j.version.major));
1718
+ matched = candidates.filter((j) => isLtsMajor(j.version.major));
1400
1719
  } else if (parsed.kind === "latest") {
1401
1720
  matched = candidates;
1402
1721
  } else {
@@ -1438,8 +1757,6 @@ function detectRcFile(platform) {
1438
1757
  import fs12 from "fs";
1439
1758
  import os3 from "os";
1440
1759
  import path12 from "path";
1441
- var LEGACY_RC_BEGIN = "# >>> jvm init >>>";
1442
- var LEGACY_RC_END = "# <<< jvm init <<<";
1443
1760
  function rcBegin(type) {
1444
1761
  return `# >>> ${CLI_BIN} ${type} init >>>`;
1445
1762
  }
@@ -1448,28 +1765,27 @@ function rcEnd(type) {
1448
1765
  }
1449
1766
  function rcBlock(type) {
1450
1767
  const spec = getSdkType(type);
1768
+ const binSuffix = spec.envBinSuffix(detectPlatform()).replace(/\\/g, "/");
1451
1769
  const abs = paths.current(type);
1452
1770
  const rel = path12.relative(os3.homedir(), abs);
1453
- const link = rel.startsWith("..") ? abs : `$HOME/${rel}`;
1771
+ const toPosix = (p) => p.split(path12.sep).join("/");
1772
+ const link = rel.startsWith("..") || path12.isAbsolute(rel) ? toPosix(abs) : `$HOME/${toPosix(rel)}`;
1454
1773
  return [
1455
1774
  rcBegin(type),
1456
1775
  `export ${spec.envVar}="${link}"`,
1457
- `case ":$PATH:" in *":$${spec.envVar}/bin:"*) ;; *) export PATH="$${spec.envVar}/bin:$PATH";; esac`,
1776
+ `case ":$PATH:" in *":$${spec.envVar}${binSuffix}:"*) ;; *) export PATH="$${spec.envVar}${binSuffix}:$PATH";; esac`,
1458
1777
  rcEnd(type)
1459
1778
  ].join("\n");
1460
1779
  }
1461
1780
  function stripRcBlock(content, type) {
1462
1781
  return stripBlockBetween(content, rcBegin(type), rcEnd(type));
1463
1782
  }
1464
- function stripLegacyRcBlock(content) {
1465
- return stripBlockBetween(content, LEGACY_RC_BEGIN, LEGACY_RC_END);
1466
- }
1467
1783
  function stripBlockBetween(content, begin, end) {
1468
1784
  const re = new RegExp(`\\n*${escapeRegex(begin)}[\\s\\S]*?${escapeRegex(end)}\\n*`, "g");
1469
1785
  return content.replace(re, "\n");
1470
1786
  }
1471
1787
  function upsertRcContent(content, type) {
1472
- const stripped = stripLegacyRcBlock(stripRcBlock(content, type)).replace(/\s+$/, "");
1788
+ const stripped = stripRcBlock(content, type).replace(/\s+$/, "");
1473
1789
  return `${stripped}
1474
1790
 
1475
1791
  ${rcBlock(type)}
@@ -1490,11 +1806,30 @@ function encoded(ps) {
1490
1806
  return ["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(ps, "utf16le").toString("base64")];
1491
1807
  }
1492
1808
  function currentLinkWin(type) {
1493
- const rel = path13.relative(os4.homedir(), paths.current(type));
1809
+ const abs = paths.current(type);
1810
+ const rel = path13.relative(os4.homedir(), abs);
1811
+ if (rel.startsWith("..") || path13.isAbsolute(rel)) {
1812
+ return abs;
1813
+ }
1494
1814
  return `%USERPROFILE%\\${rel.split(path13.sep).join("\\")}`;
1495
1815
  }
1816
+ function broadcastPs() {
1817
+ return [
1818
+ `$sig='[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)] public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);'`,
1819
+ "Add-Type -MemberDefinition $sig -Name NativeMethods -Namespace Win32",
1820
+ "$r=[UIntPtr]::Zero",
1821
+ "[Win32.NativeMethods]::SendMessageTimeout([IntPtr]0xffff, 0x1A, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$r) | Out-Null"
1822
+ ];
1823
+ }
1496
1824
  async function setEnvWin(name, value) {
1497
- await run("setx", [name, value]);
1825
+ const escaped = value.replace(/'/g, "''");
1826
+ const ps = [
1827
+ "$k=[Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment',$true)",
1828
+ "if(-not $k){ throw 'no Environment key' }",
1829
+ `$k.SetValue('${name.replace(/'/g, "''")}', '${escaped}', [Microsoft.Win32.RegistryValueKind]::ExpandString)`,
1830
+ ...broadcastPs()
1831
+ ].join("\n");
1832
+ await run("powershell.exe", encoded(ps));
1498
1833
  }
1499
1834
  async function setSdkEnvWin(type) {
1500
1835
  const spec = getSdkType(type);
@@ -1513,15 +1848,13 @@ async function ensureUserPathWin(entry) {
1513
1848
  " if($k.GetValueKind('Path') -eq [Microsoft.Win32.RegistryValueKind]::String){ $kind=[Microsoft.Win32.RegistryValueKind]::String }",
1514
1849
  " $k.SetValue('Path', ($parts -join ';'), $kind)",
1515
1850
  "}",
1516
- `$sig='[DllImport("user32.dll", SetLastError=true, CharSet=CharSet.Auto)] public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);'`,
1517
- "Add-Type -MemberDefinition $sig -Name NativeMethods -Namespace Win32",
1518
- "$r=[UIntPtr]::Zero",
1519
- "[Win32.NativeMethods]::SendMessageTimeout([IntPtr]0xffff, 0x1A, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$r) | Out-Null"
1851
+ ...broadcastPs()
1520
1852
  ].join("\n");
1521
1853
  await run("powershell.exe", encoded(ps));
1522
1854
  }
1523
1855
  function sdkPathEntry(type) {
1524
- return `%${getSdkType(type).envVar}%\\bin`;
1856
+ const spec = getSdkType(type);
1857
+ return `%${spec.envVar}%${spec.envBinSuffix(detectPlatform())}`;
1525
1858
  }
1526
1859
 
1527
1860
  // src/cli/use.ts
@@ -1532,9 +1865,10 @@ async function showSdkVersion(binPath, type) {
1532
1865
  const { stdout, stderr } = await execFileAsync2(binPath, spec.versionCheck.args, {
1533
1866
  timeout: 3e4
1534
1867
  });
1535
- const text = spec.versionCheck.stream === "stdout" ? stdout : stderr;
1536
- const line = (text || "").split("\n").find((l) => l.includes("version"));
1537
- if (line) log.info(line.trim());
1868
+ const text = (spec.versionCheck.stream === "stdout" ? stdout : stderr) || "";
1869
+ const lines = text.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
1870
+ const line = lines.find((l) => /version/i.test(l)) ?? lines[0];
1871
+ if (line) log.info(line);
1538
1872
  } catch {
1539
1873
  }
1540
1874
  }
@@ -1543,7 +1877,9 @@ async function useCommand(type, specInput, opts) {
1543
1877
  const spec = getSdkType(type);
1544
1878
  const installed = findInstalled(type, specInput, opts.vendor);
1545
1879
  const label2 = `${installed.version.vendor}-${spec.formatVersion(installed.version)}`;
1546
- setCurrent(type, installed.home, platform);
1880
+ await withLock(async () => {
1881
+ setCurrent(type, installed.home, platform);
1882
+ });
1547
1883
  log.ok(`current \u2192 ${label2}`);
1548
1884
  if (platform.os === "windows") {
1549
1885
  await setSdkEnvWin(type);
@@ -1655,153 +1991,644 @@ async function uninstallCommand(type, specInput, opts) {
1655
1991
  log.ok(`removed ${installed.version.vendor}-${spec.formatVersion(installed.version)}`);
1656
1992
  }
1657
1993
 
1658
- // src/cli/mirror.ts
1659
- var RECOMMENDED = {
1660
- temurin: "https://mirrors.nju.edu.cn/adoptium",
1661
- golang: "https://golang.google.cn/dl",
1662
- flutter: "https://mirror.nju.edu.cn/flutter/flutter_infra_release"
1994
+ // src/cli/mirror-presets.ts
1995
+ var MIRRORABLE_BY_TYPE = {
1996
+ java: ["temurin"],
1997
+ go: ["golang"],
1998
+ flutter: ["flutter"],
1999
+ node: ["nodejs"]
1663
2000
  };
1664
- function mirrorCommand(type, action, urlOrVendor, maybeUrl) {
1665
- const config = loadConfig();
1666
- const mirrorable = allVendorIds(type).filter((id) => id in RECOMMENDED);
1667
- const first = mirrorable[0] ?? allVendorIds(type)[0] ?? "";
2001
+ var MIRROR_SITE_PRESETS = [
2002
+ {
2003
+ name: "nju",
2004
+ list: true,
2005
+ vendors: {
2006
+ temurin: "https://mirrors.nju.edu.cn/adoptium",
2007
+ golang: "https://mirror.nju.edu.cn/golang",
2008
+ flutter: "https://mirror.nju.edu.cn/flutter/flutter_infra_release",
2009
+ nodejs: "https://mirror.nju.edu.cn/nodejs-release"
2010
+ }
2011
+ },
2012
+ {
2013
+ name: "tuna",
2014
+ aliases: ["tsinghua"],
2015
+ list: true,
2016
+ vendors: {
2017
+ temurin: "https://mirrors.tuna.tsinghua.edu.cn/Adoptium",
2018
+ flutter: "https://mirrors.tuna.tsinghua.edu.cn/flutter/flutter_infra_release"
2019
+ }
2020
+ },
2021
+ {
2022
+ name: "aliyun",
2023
+ aliases: ["ali"],
2024
+ list: true,
2025
+ vendors: {
2026
+ golang: "https://mirrors.aliyun.com/golang",
2027
+ nodejs: "https://mirrors.aliyun.com/nodejs-release"
2028
+ }
2029
+ },
2030
+ {
2031
+ name: "huawei",
2032
+ list: true,
2033
+ vendors: {
2034
+ nodejs: "https://repo.huaweicloud.com/nodejs"
2035
+ }
2036
+ },
2037
+ {
2038
+ name: "official",
2039
+ list: true,
2040
+ vendors: {}
2041
+ }
2042
+ ];
2043
+ function normalizeMirrorUrl(url) {
2044
+ const trimmed = url.trim().replace(/\/+$/, "");
2045
+ try {
2046
+ const u = new URL(trimmed);
2047
+ const path16 = `${u.pathname}${u.search}${u.hash}`.replace(/\/+$/, "") || "";
2048
+ return `${u.protocol.toLowerCase()}//${u.host.toLowerCase()}${path16}`;
2049
+ } catch {
2050
+ return trimmed;
2051
+ }
2052
+ }
2053
+ function parseMirrorRootUrl(url) {
2054
+ const trimmed = url.trim();
2055
+ let parsed;
2056
+ try {
2057
+ parsed = new URL(trimmed);
2058
+ } catch {
2059
+ throw new Error(`invalid URL: ${url}`);
2060
+ }
2061
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2062
+ throw new Error(`invalid URL protocol: ${parsed.protocol}`);
2063
+ }
2064
+ return trimmed.replace(/\/+$/, "");
2065
+ }
2066
+ function mirrorableVendors(type) {
2067
+ return MIRRORABLE_BY_TYPE[type];
2068
+ }
2069
+ function siteVendorsForType(site, type) {
2070
+ const out = {};
2071
+ for (const id of mirrorableVendors(type)) {
2072
+ const url = site.vendors[id];
2073
+ if (url) out[id] = url.replace(/\/+$/, "");
2074
+ }
2075
+ return out;
2076
+ }
2077
+ function findMirrorSite(name) {
2078
+ const key = name.trim().toLowerCase();
2079
+ return MIRROR_SITE_PRESETS.find(
2080
+ (s) => s.name.toLowerCase() === key || s.aliases?.some((a) => a.toLowerCase() === key)
2081
+ );
2082
+ }
2083
+ function listMirrorSitesForType(type) {
2084
+ return MIRROR_SITE_PRESETS.filter((s) => {
2085
+ if (!s.list) return false;
2086
+ if (s.name === "official") return true;
2087
+ return Object.keys(siteVendorsForType(s, type)).length > 0;
2088
+ });
2089
+ }
2090
+ function matchMirrorSiteName(type, mirror) {
2091
+ const vendors = mirrorableVendors(type);
2092
+ const allOfficial = vendors.every((id) => {
2093
+ const v = mirror[id];
2094
+ return v == null || v === "";
2095
+ });
2096
+ if (allOfficial) return "official";
2097
+ for (const site of MIRROR_SITE_PRESETS) {
2098
+ if (site.name === "official" || !site.list) continue;
2099
+ const scoped = siteVendorsForType(site, type);
2100
+ const ids = Object.keys(scoped);
2101
+ if (ids.length === 0) continue;
2102
+ const hit = ids.every((id) => {
2103
+ const configured = mirror[id];
2104
+ if (configured == null || configured === "") return false;
2105
+ return normalizeMirrorUrl(configured) === normalizeMirrorUrl(scoped[id]);
2106
+ });
2107
+ if (hit) return site.name;
2108
+ }
2109
+ return null;
2110
+ }
2111
+ function formatMirrorListLine(name, detail, current) {
2112
+ const mark = current ? "*" : " ";
2113
+ const padded = `${name} `.padEnd(14, "-");
2114
+ return `${mark} ${padded} ${detail}`;
2115
+ }
2116
+ function availableSiteNamesForType(type) {
2117
+ return listMirrorSitesForType(type).map((s) => s.name);
2118
+ }
2119
+
2120
+ // src/cli/mirror.ts
2121
+ function mirrorableIds(type) {
2122
+ return [...mirrorableVendors(type)];
2123
+ }
2124
+ function firstMirrorable(type) {
2125
+ return mirrorableVendors(type)[0] ?? "";
2126
+ }
2127
+ function mirrorCommand(type, action, arg1, arg2) {
2128
+ const mirrorable = mirrorableIds(type);
2129
+ const first = firstMirrorable(type);
2130
+ const prefix = cmdPath(type);
2131
+ if (action === "ls" || action === "list") {
2132
+ const config2 = loadConfig();
2133
+ const matched2 = matchMirrorSiteName(type, config2.mirror);
2134
+ for (const site of listMirrorSitesForType(type)) {
2135
+ let detail;
2136
+ if (site.name === "official") {
2137
+ detail = "(official source)";
2138
+ } else {
2139
+ const scoped = siteVendorsForType(site, type);
2140
+ detail = Object.entries(scoped).map(([id, url]) => mirrorable.length === 1 ? url : `${id}=${url}`).join(" ");
2141
+ }
2142
+ log.raw(formatMirrorListLine(site.name, detail, matched2 === site.name));
2143
+ }
2144
+ if (matched2 == null) {
2145
+ const parts = mirrorable.map((id) => {
2146
+ const url = config2.mirror[id];
2147
+ return url ? `${id}=${url}` : `${id}=(official)`;
2148
+ });
2149
+ log.raw(formatMirrorListLine("custom", parts.join(" "), true));
2150
+ }
2151
+ return;
2152
+ }
2153
+ if (action === "use") {
2154
+ const name = arg1;
2155
+ if (!name) {
2156
+ throw new SdkvmError(`usage: ${prefix} mirror use <site>`, {
2157
+ hint: `Available: ${availableSiteNamesForType(type).join(", ")}`
2158
+ });
2159
+ }
2160
+ const site = findMirrorSite(name);
2161
+ if (!site || site.name !== "official" && Object.keys(siteVendorsForType(site, type)).length === 0) {
2162
+ throw new SdkvmError(`Unknown or unsupported mirror site "${name}" for ${type}`, {
2163
+ hint: `Available: ${availableSiteNamesForType(type).join(", ")}`
2164
+ });
2165
+ }
2166
+ if (site.name === "official") {
2167
+ updateConfig((config2) => {
2168
+ for (const id of mirrorable) {
2169
+ delete config2.mirror[id];
2170
+ }
2171
+ });
2172
+ log.ok(`mirror for ${type} \u2192 official`);
2173
+ return;
2174
+ }
2175
+ const scoped = siteVendorsForType(site, type);
2176
+ const changed = [];
2177
+ updateConfig((config2) => {
2178
+ for (const [id, url] of Object.entries(scoped)) {
2179
+ config2.mirror[id] = url;
2180
+ changed.push(`${id} \u2192 ${url}`);
2181
+ }
2182
+ });
2183
+ log.ok(`mirror site ${site.name}: ${changed.join("; ")}`);
2184
+ return;
2185
+ }
2186
+ if (action === "current") {
2187
+ const config2 = loadConfig();
2188
+ const matched2 = matchMirrorSiteName(type, config2.mirror);
2189
+ if (matched2 === "official") {
2190
+ log.raw(`official \u2192 (no mirror)`);
2191
+ for (const id of allVendorIds(type)) {
2192
+ log.raw(` ${id.padEnd(8)} (official)`);
2193
+ }
2194
+ return;
2195
+ }
2196
+ if (matched2) {
2197
+ const site = findMirrorSite(matched2);
2198
+ const scoped = siteVendorsForType(site, type);
2199
+ const firstUrl = Object.values(scoped)[0] ?? "";
2200
+ log.raw(`${matched2} \u2192 ${firstUrl}`);
2201
+ } else {
2202
+ log.raw("custom \u2192");
2203
+ }
2204
+ for (const id of allVendorIds(type)) {
2205
+ const url = config2.mirror[id];
2206
+ log.raw(` ${id.padEnd(8)} ${url ?? "(official)"}`);
2207
+ }
2208
+ return;
2209
+ }
1668
2210
  if (action === "set") {
1669
2211
  let vendor;
1670
2212
  let url;
1671
- if (maybeUrl !== void 0) {
1672
- vendor = urlOrVendor ?? "";
1673
- url = maybeUrl;
2213
+ if (arg2 !== void 0) {
2214
+ vendor = arg1 ?? "";
2215
+ url = arg2;
1674
2216
  } else {
1675
2217
  vendor = first;
1676
- url = urlOrVendor;
2218
+ url = arg1;
2219
+ }
2220
+ if (!url) throw new SdkvmError(`usage: ${prefix} mirror set [vendor] <url>`);
2221
+ const siteHit = findMirrorSite(url);
2222
+ if (!url.includes("://") && siteHit) {
2223
+ throw new SdkvmError(`"${url}" is a mirror site name, not a URL`, {
2224
+ hint: `Use: ${prefix} mirror use ${siteHit.name}`
2225
+ });
1677
2226
  }
1678
- if (!url) throw new SdkvmError(`usage: ${cmdPath(type)} mirror set [vendor] <url>`);
1679
2227
  if (!mirrorable.includes(vendor)) {
1680
2228
  throw new SdkvmError(`mirroring is only supported for ${mirrorable.join(", ") || "none"} (got "${vendor}")`, {
1681
- hint: `recommended: ${cmdPath(type)} mirror set ${first} ${RECOMMENDED[first] ?? ""}`.trim()
2229
+ hint: first ? `recommended: ${prefix} mirror use nju (or set URL: ${prefix} mirror set ${first} <url>)` : void 0
1682
2230
  });
1683
2231
  }
2232
+ let normalized;
1684
2233
  try {
1685
- new URL(url);
1686
- } catch {
1687
- throw new SdkvmError(`invalid URL: ${url}`);
2234
+ normalized = parseMirrorRootUrl(url);
2235
+ } catch (err) {
2236
+ const msg = err instanceof Error ? err.message : String(err);
2237
+ throw new SdkvmError(msg, {
2238
+ hint: msg.includes("protocol") ? "Expected http:// or https://" : void 0
2239
+ });
1688
2240
  }
1689
- config.mirror[vendor] = url.replace(/\/+$/, "");
1690
- saveConfig(config);
1691
- log.ok(`mirror for ${vendor} \u2192 ${config.mirror[vendor]}`);
2241
+ updateConfig((config2) => {
2242
+ config2.mirror[vendor] = normalized;
2243
+ });
2244
+ log.ok(`mirror for ${vendor} \u2192 ${normalized}`);
1692
2245
  return;
1693
2246
  }
1694
2247
  if (action === "unset") {
1695
- const vendor = urlOrVendor ?? first;
2248
+ const vendor = arg1 ?? first;
2249
+ if (vendor && findMirrorSite(vendor)?.name === "official") {
2250
+ throw new SdkvmError(`To clear mirrors, use: ${prefix} mirror use official`, {
2251
+ hint: `Or: ${prefix} mirror unset ${first || "<vendor>"}`
2252
+ });
2253
+ }
1696
2254
  if (!mirrorable.includes(vendor)) {
1697
2255
  throw new SdkvmError(`mirroring is only supported for ${mirrorable.join(", ") || "none"}`);
1698
2256
  }
1699
- config.mirror[vendor] = null;
1700
- saveConfig(config);
2257
+ updateConfig((config2) => {
2258
+ delete config2.mirror[vendor];
2259
+ });
1701
2260
  log.ok(`mirror for ${vendor} cleared (official source)`);
1702
2261
  return;
1703
2262
  }
1704
- log.raw("mirrors:");
2263
+ if (action !== void 0 && action !== "show") {
2264
+ throw new SdkvmError(`unknown mirror action "${action}"`, {
2265
+ hint: `usage: ${prefix} mirror ls|use|current|show|set|unset`
2266
+ });
2267
+ }
2268
+ const config = loadConfig();
2269
+ const matched = matchMirrorSiteName(type, config.mirror);
2270
+ log.raw(`mirrors${matched ? ` (${matched})` : ""}:`);
1705
2271
  for (const id of allVendorIds(type)) {
1706
2272
  const url = config.mirror[id];
1707
2273
  log.raw(` ${id.padEnd(8)} ${url ?? "(official)"}`);
1708
2274
  }
1709
- if (first in RECOMMENDED) {
1710
- log.raw(`recommended for CN users: ${cmdPath(type)} mirror set ${first} ${RECOMMENDED[first]}`);
2275
+ if (first) {
2276
+ log.raw(`list sites: ${prefix} mirror ls`);
2277
+ log.raw(`use a site: ${prefix} mirror use nju`);
1711
2278
  }
1712
2279
  }
1713
2280
 
1714
- // src/core/migrate.ts
2281
+ // src/cli/upgrade.ts
2282
+ import { spawn } from "child_process";
1715
2283
  import fs14 from "fs";
1716
- import os5 from "os";
1717
2284
  import path15 from "path";
1718
- async function migrateLegacyHomeIfNeeded(opts = {}) {
1719
- const oldHome = opts.oldHome ?? path15.join(os5.homedir(), ".jvm");
1720
- const newHome = opts.newHome ?? sdkvmHome();
1721
- if (opts.newHome === void 0 && envOverride("SDKVM_HOME", "JVM_HOME")) return "not-needed";
1722
- if (!fs14.existsSync(oldHome)) return "not-needed";
1723
- if (fs14.existsSync(newHome)) {
1724
- log.warn(`found both ${oldHome} and ${newHome}; keeping ${newHome}, not merging`);
1725
- log.warn(`move SDKs into ${newHome} manually if needed, then remove ${oldHome}`);
1726
- return "conflict";
1727
- }
1728
- const platform = opts.platform ?? detectPlatform();
1729
- const rc = opts.rcFile !== void 0 ? opts.rcFile : detectRcFile(platform.os);
1730
- const oldLock = path15.join(oldHome, ".lock");
1731
- if (fs14.existsSync(oldLock)) {
1732
- const stat = fs14.statSync(oldLock);
1733
- if (Date.now() - stat.mtimeMs <= 5 * 60 * 1e3) {
1734
- log.warn("another sdkvm operation seems in progress; skipping migration this run");
1735
- return "not-needed";
1736
- }
1737
- }
1738
- fs14.renameSync(oldHome, newHome);
1739
- const prevHome = process.env.SDKVM_HOME;
1740
- process.env.SDKVM_HOME = newHome;
1741
- try {
1742
- await fixupAfterRename(oldHome, newHome, platform, rc);
1743
- } finally {
1744
- if (prevHome === void 0) delete process.env.SDKVM_HOME;
1745
- else process.env.SDKVM_HOME = prevHome;
2285
+ import { fileURLToPath as fileURLToPath2 } from "url";
2286
+ var RELEASE_REPO = "QInJ1995/sdkvm";
2287
+ var RELEASE_ASSET = "sdkvm.tgz";
2288
+ var RELEASE_SUMS = "SHA256SUMS";
2289
+ function packageRoot(metaUrl = import.meta.url) {
2290
+ return path15.resolve(path15.dirname(fileURLToPath2(metaUrl)), "..");
2291
+ }
2292
+ function isScriptInstall(probe = {}) {
2293
+ const home = path15.resolve(probe.home ?? sdkvmHome());
2294
+ const cliRoot = path15.resolve(path15.join(home, "cli"));
2295
+ const runtimeRoot = path15.resolve(path15.join(home, "runtime"));
2296
+ const pkg = path15.resolve(probe.packageRoot ?? packageRoot());
2297
+ if (pkg === cliRoot) return true;
2298
+ const exec = path15.resolve(probe.execPath ?? process.execPath);
2299
+ return exec === runtimeRoot || exec.startsWith(runtimeRoot + path15.sep);
2300
+ }
2301
+ function releaseBase() {
2302
+ const fromEnv = process.env.SDKVM_RELEASE_BASE?.replace(/\/+$/, "");
2303
+ return fromEnv || `https://github.com/${RELEASE_REPO}/releases`;
2304
+ }
2305
+ function releaseAssetUrl(name, base = releaseBase()) {
2306
+ return `${base}/latest/download/${name}`;
2307
+ }
2308
+ function checksumFor(sumsText, fileName) {
2309
+ for (const line of sumsText.split("\n")) {
2310
+ const match = /^([0-9a-f]{64})\s+\*?(\S+)\s*$/i.exec(line.trim());
2311
+ const hash = match?.[1];
2312
+ const name = match?.[2];
2313
+ if (hash && name === fileName) return hash.toLowerCase();
1746
2314
  }
1747
- log.ok(`migrated ${oldHome} \u2192 ${newHome} (your installed JDKs are untouched)`);
1748
- return "migrated";
2315
+ return null;
1749
2316
  }
1750
- async function fixupAfterRename(oldHome, newHome, platform, rc) {
2317
+ async function prepareCliPackage(archiveFile, home = sdkvmHome()) {
2318
+ const staging = path15.join(home, "cli.next");
2319
+ fs14.rmSync(staging, { recursive: true, force: true });
2320
+ fs14.mkdirSync(home, { recursive: true });
2321
+ await extractArchive(archiveFile, "tar.gz", staging, detectPlatform());
2322
+ const unpacked = path15.join(staging, "package");
2323
+ if (!fs14.existsSync(path15.join(unpacked, "package.json"))) {
2324
+ fs14.rmSync(staging, { recursive: true, force: true });
2325
+ throw new SdkvmError("Release archive is missing package/package.json", { hint: archiveFile });
2326
+ }
2327
+ return unpacked;
2328
+ }
2329
+ async function replaceCliPackage(archiveFile, home = sdkvmHome()) {
2330
+ const staging = path15.join(home, "cli.next");
2331
+ const bak = path15.join(home, "cli.bak");
2332
+ const cli = path15.join(home, "cli");
2333
+ const unpacked = await prepareCliPackage(archiveFile, home);
2334
+ fs14.rmSync(bak, { recursive: true, force: true });
2335
+ if (fs14.existsSync(cli)) fs14.renameSync(cli, bak);
1751
2336
  try {
1752
- const oldLink = path15.join(newHome, "current");
1753
- let target = null;
1754
- try {
1755
- const st = fs14.lstatSync(oldLink);
1756
- if (st.isSymbolicLink()) {
1757
- target = path15.resolve(path15.dirname(oldLink), fs14.readlinkSync(oldLink));
2337
+ fs14.renameSync(unpacked, cli);
2338
+ } catch (err) {
2339
+ if (fs14.existsSync(bak) && !fs14.existsSync(cli)) {
2340
+ try {
2341
+ fs14.renameSync(bak, cli);
2342
+ } catch {
1758
2343
  }
1759
- } catch {
1760
2344
  }
1761
- if (target) {
1762
- const newTarget = target.startsWith(oldHome) ? newHome + target.slice(oldHome.length) : target;
1763
- if (fs14.existsSync(newTarget)) setCurrent("java", newTarget, platform);
2345
+ fs14.rmSync(staging, { recursive: true, force: true });
2346
+ throw err;
2347
+ }
2348
+ fs14.rmSync(bak, { recursive: true, force: true });
2349
+ fs14.rmSync(staging, { recursive: true, force: true });
2350
+ }
2351
+ function windowsUpgradeScript(home) {
2352
+ return [
2353
+ "@echo off",
2354
+ "setlocal",
2355
+ `set "HOME=${home}"`,
2356
+ "timeout /t 2 /nobreak >nul",
2357
+ 'if exist "%HOME%\\cli.bak" rmdir /s /q "%HOME%\\cli.bak"',
2358
+ 'if exist "%HOME%\\cli" move /y "%HOME%\\cli" "%HOME%\\cli.bak" >nul',
2359
+ 'move /y "%HOME%\\cli.next\\package" "%HOME%\\cli" >nul',
2360
+ 'if exist "%HOME%\\cli\\package.json" (',
2361
+ ' if exist "%HOME%\\cli.bak" rmdir /s /q "%HOME%\\cli.bak"',
2362
+ ' if exist "%HOME%\\cli.next" rmdir /s /q "%HOME%\\cli.next"',
2363
+ ") else (",
2364
+ ' if exist "%HOME%\\cli.bak" if not exist "%HOME%\\cli" move /y "%HOME%\\cli.bak" "%HOME%\\cli" >nul',
2365
+ ")",
2366
+ 'del "%~f0"',
2367
+ ""
2368
+ ].join("\r\n");
2369
+ }
2370
+ function scheduleWindowsCliReplace(home) {
2371
+ const script = path15.join(home, "upgrade-apply.cmd");
2372
+ fs14.writeFileSync(script, windowsUpgradeScript(home), "utf8");
2373
+ const child = spawn("cmd.exe", ["/c", script], {
2374
+ detached: true,
2375
+ stdio: "ignore",
2376
+ windowsHide: true
2377
+ });
2378
+ child.unref();
2379
+ }
2380
+ async function upgradeCommand() {
2381
+ if (!isScriptInstall()) {
2382
+ log.info("this install came from npm; upgrade with: npm update -g sdkvm");
2383
+ log.info("or: pnpm update -g sdkvm / yarn global upgrade sdkvm / bun update -g sdkvm");
2384
+ return;
2385
+ }
2386
+ await withLock(async () => {
2387
+ const sumsUrl = releaseAssetUrl(RELEASE_SUMS);
2388
+ const assetUrl = releaseAssetUrl(RELEASE_ASSET);
2389
+ log.info(`checking ${assetUrl}`);
2390
+ const expected = checksumFor(await httpText(sumsUrl), RELEASE_ASSET);
2391
+ if (!expected) {
2392
+ throw new SdkvmError(`No checksum for ${RELEASE_ASSET}`, { hint: sumsUrl });
1764
2393
  }
2394
+ fs14.mkdirSync(paths.cache(), { recursive: true });
2395
+ const dest = path15.join(paths.cache(), RELEASE_ASSET);
1765
2396
  try {
1766
- fs14.rmSync(oldLink, { force: true });
1767
- } catch {
1768
- fs14.rmSync(oldLink, { recursive: true, force: true });
2397
+ const downloaded = await downloadFile(assetUrl, dest);
2398
+ if (downloaded.sha256 !== expected) {
2399
+ throw new SdkvmError(`Checksum mismatch for ${RELEASE_ASSET}`, {
2400
+ hint: `expected ${expected}, got ${downloaded.sha256}`
2401
+ });
2402
+ }
2403
+ const before = getVersion();
2404
+ const home = sdkvmHome();
2405
+ if (process.platform === "win32") {
2406
+ await prepareCliPackage(dest, home);
2407
+ scheduleWindowsCliReplace(home);
2408
+ log.ok(
2409
+ `upgrade ${before} scheduled; exit this process and wait a moment for ${path15.join(home, "cli")} to refresh`
2410
+ );
2411
+ return;
2412
+ }
2413
+ await replaceCliPackage(dest, home);
2414
+ const after = getVersion();
2415
+ log.ok(
2416
+ `upgraded CLI ${before} \u2192 ${after} in ${path15.join(home, "cli")}; runtime and installed SDKs were left in place`
2417
+ );
2418
+ } finally {
2419
+ fs14.rmSync(dest, { force: true });
2420
+ fs14.rmSync(`${dest}.part`, { force: true });
1769
2421
  }
2422
+ });
2423
+ }
2424
+
2425
+ // src/cli/nrm.ts
2426
+ var NPM_REGISTRY_PRESETS = [
2427
+ { name: "npm", url: "https://registry.npmjs.org/", list: true },
2428
+ { name: "yarn", url: "https://registry.yarnpkg.com/", list: true },
2429
+ { name: "tencent", url: "https://mirrors.tencent.com/npm/", list: true },
2430
+ { name: "cnpm", url: "https://r.cnpmjs.org/", list: true },
2431
+ { name: "taobao", url: "https://registry.npmmirror.com/", list: true },
2432
+ { name: "npmmirror", url: "https://registry.npmmirror.com/", list: false },
2433
+ { name: "npmMirror", url: "https://skimdb.npmjs.com/registry/", list: true },
2434
+ { name: "huawei", url: "https://repo.huaweicloud.com/repository/npm/", list: true }
2435
+ ];
2436
+ var NAME_RE = /^[A-Za-z][A-Za-z0-9_-]*$/;
2437
+ var TEST_TIMEOUT_MS = 5e3;
2438
+ function normalizeRegistryUrl(url) {
2439
+ return url.trim().replace(/\/+$/, "").toLowerCase();
2440
+ }
2441
+ function isBuiltinRegistryName(name) {
2442
+ const key = name.trim().toLowerCase();
2443
+ return NPM_REGISTRY_PRESETS.some((p) => p.name.toLowerCase() === key);
2444
+ }
2445
+ function listRegistryEntries() {
2446
+ const custom = loadConfig().npmRegistries;
2447
+ const out = [...NPM_REGISTRY_PRESETS];
2448
+ for (const [name, url] of Object.entries(custom)) {
2449
+ out.push({ name, url, list: true, custom: true });
2450
+ }
2451
+ return out;
2452
+ }
2453
+ function findRegistryByName(name) {
2454
+ const key = name.trim();
2455
+ const all = listRegistryEntries();
2456
+ return all.find((p) => p.name === key || p.name.toLowerCase() === key.toLowerCase());
2457
+ }
2458
+ function matchListedRegistryName(registryUrl) {
2459
+ const norm = normalizeRegistryUrl(registryUrl);
2460
+ const hit = listRegistryEntries().find((p) => p.list && normalizeRegistryUrl(p.url) === norm);
2461
+ return hit?.name ?? null;
2462
+ }
2463
+ function formatNrmListLine(name, url, current) {
2464
+ const mark = current ? "*" : " ";
2465
+ const padded = `${name} `.padEnd(14, "-");
2466
+ return `${mark} ${padded} ${url}`;
2467
+ }
2468
+ async function defaultNpmRunner(args) {
2469
+ try {
2470
+ return await run("npm", args);
1770
2471
  } catch (err) {
1771
- log.warn(
1772
- `current link not migrated (${err instanceof Error ? err.message : String(err)}); run: sdkvm java use <version>`
1773
- );
2472
+ if (err instanceof SdkvmError && /Failed to run npm/.test(err.message)) {
2473
+ throw new SdkvmError("npm is not available", {
2474
+ hint: "Install Node.js first, or run: sdkvm node use lts"
2475
+ });
2476
+ }
2477
+ throw err;
1774
2478
  }
1775
- if (platform.os === "windows") {
1776
- try {
1777
- await setSdkEnvWin("java");
1778
- } catch (err) {
1779
- log.warn(`registry JAVA_HOME not updated: ${err instanceof Error ? err.message : String(err)}`);
2479
+ }
2480
+ async function defaultRegistryProbe(url) {
2481
+ const target = url.replace(/\/+$/, "") + "/";
2482
+ const started = Date.now();
2483
+ const res = await fetch(target, {
2484
+ method: "HEAD",
2485
+ redirect: "follow",
2486
+ signal: AbortSignal.timeout(TEST_TIMEOUT_MS)
2487
+ });
2488
+ if (res.status >= 500) {
2489
+ throw new Error(`HTTP ${res.status}`);
2490
+ }
2491
+ return Date.now() - started;
2492
+ }
2493
+ async function getNpmRegistry(npmRun = defaultNpmRunner) {
2494
+ const { stdout } = await npmRun(["config", "get", "registry"]);
2495
+ const url = stdout.trim();
2496
+ if (!url || url === "undefined") {
2497
+ throw new SdkvmError("npm config get registry returned empty", {
2498
+ hint: "Check your npm installation: npm config get registry"
2499
+ });
2500
+ }
2501
+ return url;
2502
+ }
2503
+ async function setNpmRegistry(url, npmRun = defaultNpmRunner) {
2504
+ await npmRun(["config", "set", "registry", url, "--location=user"]);
2505
+ }
2506
+ async function nrmLs(npmRun = defaultNpmRunner) {
2507
+ const current = await getNpmRegistry(npmRun);
2508
+ const matched = matchListedRegistryName(current);
2509
+ for (const p of listRegistryEntries()) {
2510
+ if (!p.list) continue;
2511
+ log.raw(formatNrmListLine(p.name, p.url, matched === p.name));
2512
+ }
2513
+ if (!matched) {
2514
+ log.raw(formatNrmListLine("custom", current, true));
2515
+ }
2516
+ }
2517
+ async function nrmCurrent(npmRun = defaultNpmRunner) {
2518
+ const current = await getNpmRegistry(npmRun);
2519
+ const matched = matchListedRegistryName(current);
2520
+ if (matched) {
2521
+ log.raw(`${matched} \u2192 ${current}`);
2522
+ } else {
2523
+ log.raw(`custom \u2192 ${current}`);
2524
+ }
2525
+ }
2526
+ async function nrmUse(name, npmRun = defaultNpmRunner) {
2527
+ const entry = findRegistryByName(name);
2528
+ if (!entry) {
2529
+ const names = listRegistryEntries().map((p) => p.name).join(", ");
2530
+ throw new SdkvmError(`Unknown registry "${name}"`, {
2531
+ hint: `Available: ${names}`
2532
+ });
2533
+ }
2534
+ await setNpmRegistry(entry.url, npmRun);
2535
+ log.ok(`npm registry \u2192 ${entry.name} (${entry.url})`);
2536
+ }
2537
+ function nrmAdd(name, url) {
2538
+ const key = name.trim();
2539
+ if (!NAME_RE.test(key)) {
2540
+ throw new SdkvmError(`Invalid registry name "${name}"`, {
2541
+ hint: "Use letters, digits, _ or -; must start with a letter"
2542
+ });
2543
+ }
2544
+ if (isBuiltinRegistryName(key)) {
2545
+ throw new SdkvmError(`Cannot overwrite built-in registry "${key}"`, {
2546
+ hint: "Pick another name, or use: sdkvm nrm use " + key
2547
+ });
2548
+ }
2549
+ let parsed;
2550
+ try {
2551
+ parsed = new URL(url.trim());
2552
+ } catch {
2553
+ throw new SdkvmError(`Invalid URL: ${url}`);
2554
+ }
2555
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2556
+ throw new SdkvmError(`Invalid URL protocol: ${parsed.protocol}`, {
2557
+ hint: "Expected http:// or https://"
2558
+ });
2559
+ }
2560
+ parsed.pathname = parsed.pathname.replace(/\/+$/, "") + "/";
2561
+ const normalized = parsed.href;
2562
+ updateConfig((config) => {
2563
+ const existingKey = Object.keys(config.npmRegistries).find(
2564
+ (k) => k.toLowerCase() === key.toLowerCase()
2565
+ );
2566
+ if (existingKey && existingKey !== key) {
2567
+ delete config.npmRegistries[existingKey];
2568
+ }
2569
+ config.npmRegistries[key] = normalized;
2570
+ });
2571
+ log.ok(`added registry ${key} \u2192 ${normalized}`);
2572
+ }
2573
+ function nrmDel(name) {
2574
+ const key = name.trim();
2575
+ if (isBuiltinRegistryName(key)) {
2576
+ throw new SdkvmError(`Cannot delete built-in registry "${key}"`);
2577
+ }
2578
+ let deleted;
2579
+ updateConfig((config) => {
2580
+ const existing = Object.keys(config.npmRegistries).find((k) => k.toLowerCase() === key.toLowerCase());
2581
+ if (!existing) {
2582
+ throw new SdkvmError(`Unknown custom registry "${name}"`, {
2583
+ hint: "Only registries added with sdkvm nrm add can be deleted"
2584
+ });
1780
2585
  }
2586
+ delete config.npmRegistries[existing];
2587
+ deleted = existing;
2588
+ });
2589
+ log.ok(`deleted registry ${deleted}`);
2590
+ }
2591
+ async function nrmTest(name, opts = {}) {
2592
+ const npmRun = opts.npmRun ?? defaultNpmRunner;
2593
+ const probe = opts.probe ?? defaultRegistryProbe;
2594
+ const current = await getNpmRegistry(npmRun);
2595
+ const matched = matchListedRegistryName(current);
2596
+ let targets = listRegistryEntries().filter((p) => p.list);
2597
+ if (name) {
2598
+ const entry = findRegistryByName(name);
2599
+ if (!entry) {
2600
+ throw new SdkvmError(`Unknown registry "${name}"`, {
2601
+ hint: "Run: sdkvm nrm ls"
2602
+ });
2603
+ }
2604
+ targets = [{ ...entry, list: true }];
1781
2605
  }
1782
- if (rc && fs14.existsSync(rc)) {
2606
+ for (const p of targets) {
2607
+ const isCurrent = matched === p.name || normalizeRegistryUrl(p.url) === normalizeRegistryUrl(current);
1783
2608
  try {
1784
- if (fs14.readFileSync(rc, "utf8").includes(LEGACY_RC_BEGIN)) upsertRcFile(rc, "java");
1785
- } catch {
1786
- log.warn(`rc file not updated: ${rc}`);
2609
+ const ms = await probe(p.url);
2610
+ log.raw(formatNrmListLine(p.name, `${ms} ms`, isCurrent));
2611
+ } catch (err) {
2612
+ const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
2613
+ log.raw(formatNrmListLine(p.name, `Fetch Error (${detail})`, isCurrent));
1787
2614
  }
1788
2615
  }
1789
2616
  }
1790
2617
 
1791
2618
  // src/index.ts
1792
2619
  var program = new Command();
1793
- program.name("sdkvm").description("SDK version manager \u2014 install & switch JDKs (Temurin / Zulu / Corretto), Go toolchains, and Flutter SDKs").version(getVersion());
2620
+ program.name("sdkvm").description("SDK version manager \u2014 install & switch JDKs (Temurin / Zulu / Corretto), Go toolchains, Flutter SDKs, and Node.js runtimes").version(getVersion());
1794
2621
  function registerSdkCommands(cmd, type) {
1795
2622
  const s = getSdkType(type);
1796
2623
  const isJava = type === "java";
1797
- const installHelp = isJava ? "21 | lts | 21.0.5 | 21.0.5+11 | temurin-21" : type === "go" ? "1.24 | 1.24.5 | latest | golang-1.24" : "3.47 | 3.47.5 | 3.49.0-0.1.pre | latest | flutter-3.47";
2624
+ const installHelp = isJava ? "21 | lts | 21.0.5 | 21.0.5+11 | temurin-21" : type === "go" ? "1.24 | 1.24.5 | latest | golang-1.24" : type === "flutter" ? "3.47 | 3.47.5 | 3.49.0-0.1.pre | latest | flutter-3.47" : "22 | 22.20.0 | lts | latest | nodejs-22.20.0";
1798
2625
  const vendorIds = s.vendors.map((v) => v.id).join(" | ");
1799
2626
  cmd.command("install").description(`install a ${s.label}: ${installHelp}`).argument("<version>", "version spec (optional vendor- prefix)").option("--vendor <id>", `vendor: ${vendorIds}`).option("--force", "reinstall even if already installed").action((v, o) => installCommand(type, v, o));
1800
- cmd.command("use").description(`switch the current ${s.label} (updates ${s.envVar} / PATH)`).argument("<version>", `installed version, e.g. ${isJava ? "21 / temurin-21.0.5+11" : type === "go" ? "1.24 / golang-1.24.5" : "3.47 / flutter-3.47.5"}`).option("--vendor <id>", "restrict matching to a vendor").action((v, o) => useCommand(type, v, o));
2627
+ cmd.command("use").description(`switch the current ${s.label} (updates ${s.envVar} / PATH)`).argument("<version>", `installed version, e.g. ${isJava ? "21 / temurin-21.0.5+11" : type === "go" ? "1.24 / golang-1.24.5" : type === "flutter" ? "3.47 / flutter-3.47.5" : "22 / nodejs-22.20.0"}`).option("--vendor <id>", "restrict matching to a vendor").action((v, o) => useCommand(type, v, o));
1801
2628
  cmd.command("ls").alias("list").description(`list installed ${s.label}s (\u2192 marks current); -r lists installable versions`).option("-r, --remote", "list installable versions from all vendors").option("--vendor <id>", "filter --remote output to one vendor").action((o) => lsCommand(type, o));
1802
2629
  cmd.command("current").description(`show the current ${s.label}`).action(() => currentCommand([type]));
1803
- cmd.command("uninstall").description(`remove an installed ${s.label}`).argument("<version>", `installed version, e.g. ${isJava ? "21 / temurin-21.0.5+11" : type === "go" ? "1.24 / golang-1.24.5" : "3.47 / flutter-3.47.5"}`).option("--vendor <id>", "restrict matching to a vendor").action((v, o) => uninstallCommand(type, v, o));
1804
- cmd.command("mirror").description("show or set the download mirror").argument("[action]", "show | set | unset").argument("[vendor]", "vendor id").argument("[url]", "mirror root URL").action(
2630
+ cmd.command("uninstall").description(`remove an installed ${s.label}`).argument("<version>", `installed version, e.g. ${isJava ? "21 / temurin-21.0.5+11" : type === "go" ? "1.24 / golang-1.24.5" : type === "flutter" ? "3.47 / flutter-3.47.5" : "22 / nodejs-22.20.0"}`).option("--vendor <id>", "restrict matching to a vendor").action((v, o) => uninstallCommand(type, v, o));
2631
+ cmd.command("mirror").description("show, set, or switch download mirror sites for this SDK").argument("[action]", "ls | use | current | show | set | unset").argument("[nameOrVendor]", "site name, vendor id, or URL").argument("[url]", "mirror root URL (for set)").action(
1805
2632
  (a, v, u) => mirrorCommand(type, a, v, u)
1806
2633
  );
1807
2634
  }
@@ -1815,11 +2642,20 @@ goCmd.action(() => goCmd.help());
1815
2642
  var flutterCmd = program.command("flutter").description("Flutter SDK subcommands");
1816
2643
  registerSdkCommands(flutterCmd, "flutter");
1817
2644
  flutterCmd.action(() => flutterCmd.help());
2645
+ var nodeCmd = program.command("node").description("Node.js subcommands");
2646
+ registerSdkCommands(nodeCmd, "node");
2647
+ nodeCmd.action(() => nodeCmd.help());
1818
2648
  program.commands.find((c) => c.name() === "current")?.action(() => currentCommand());
1819
2649
  program.command("version").description("print sdkvm CLI version").action(versionCommand);
1820
- program.hook("preAction", async () => {
1821
- await migrateLegacyHomeIfNeeded();
1822
- });
2650
+ program.command("upgrade").description("upgrade the sdkvm CLI (script install replaces ~/.sdkvm/cli; npm install prints npm update -g)").action(upgradeCommand);
2651
+ var nrmCmd = program.command("nrm").description("npm registry manager (like nrm)");
2652
+ nrmCmd.command("ls").alias("list").description("list npm registries (* marks current)").action(() => nrmLs());
2653
+ nrmCmd.command("current").description("print the current npm registry").action(() => nrmCurrent());
2654
+ nrmCmd.command("use").description("switch the user-level npm registry").argument("<name>", "registry name, e.g. npm / taobao / myprivate").action((name) => nrmUse(name));
2655
+ nrmCmd.command("add").description("add a custom npm registry").argument("<name>", "registry name").argument("<url>", "registry URL").action((name, url) => nrmAdd(name, url));
2656
+ nrmCmd.command("del").alias("delete").alias("rm").description("delete a custom npm registry").argument("<name>", "custom registry name").action((name) => nrmDel(name));
2657
+ nrmCmd.command("test").description("ping registries and print latency").argument("[name]", "optional registry name; omit to test all").action((name) => nrmTest(name));
2658
+ nrmCmd.action(() => nrmCmd.help());
1823
2659
  program.parseAsync(process.argv).catch((err) => {
1824
2660
  const e = toSdkvmError(err);
1825
2661
  log.error(e.message);