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/LICENSE +1 -1
- package/README.en.md +599 -0
- package/README.md +347 -192
- package/dist/index.js +1061 -225
- package/package.json +6 -4
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
|
|
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
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
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(
|
|
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
|
|
78
|
+
const os5 = osMap[rawPlatform];
|
|
81
79
|
const arch = archMap[rawArch];
|
|
82
|
-
if (!
|
|
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:
|
|
90
|
+
return { os: os5, arch, rawPlatform, rawArch };
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
// src/core/config.ts
|
|
96
|
-
import
|
|
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,
|
|
388
|
-
const url = `${API}/v3/binary/latest/${major}/ga/${
|
|
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,
|
|
456
|
+
function githubAssetUrl(version, os5, arch) {
|
|
403
457
|
const v = parseVersion("temurin", version);
|
|
404
458
|
const major = v.major;
|
|
405
|
-
const ext =
|
|
459
|
+
const ext = os5 === "windows" ? "zip" : "tar.gz";
|
|
406
460
|
const underscored = formatVersion(v).replace("+", "_");
|
|
407
|
-
const file = `OpenJDK${major}U-jdk_${arch}_${
|
|
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
|
|
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:
|
|
487
|
-
const url = `${API2}/zulu/packages/?java_version=${javaVersion}&os=${
|
|
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
|
-
|
|
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:
|
|
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: "
|
|
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: "
|
|
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
|
|
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 (!
|
|
1212
|
+
if (!fs5.existsSync(file)) return { ...DEFAULT_CONFIG, mirror: {}, npmRegistries: {} };
|
|
937
1213
|
try {
|
|
938
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
1247
|
+
fs5.writeFileSync(tmp, `${JSON.stringify(config, null, 2)}
|
|
966
1248
|
`);
|
|
967
|
-
|
|
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
|
-
|
|
1251
|
+
function updateConfig(mutator) {
|
|
1001
1252
|
acquireLock();
|
|
1002
1253
|
try {
|
|
1003
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
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: `${
|
|
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
|
-
|
|
1276
|
+
root
|
|
1023
1277
|
);
|
|
1024
|
-
return {
|
|
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, {
|
|
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 =
|
|
1264
|
-
const artifact =
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
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
|
-
|
|
1294
|
-
fs9.renameSync(
|
|
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
|
|
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
|
-
|
|
1395
|
-
|
|
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) =>
|
|
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
|
|
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}
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1537
|
-
|
|
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
|
-
|
|
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
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
flutter: "
|
|
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
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
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 (
|
|
1672
|
-
vendor =
|
|
1673
|
-
url =
|
|
2213
|
+
if (arg2 !== void 0) {
|
|
2214
|
+
vendor = arg1 ?? "";
|
|
2215
|
+
url = arg2;
|
|
1674
2216
|
} else {
|
|
1675
2217
|
vendor = first;
|
|
1676
|
-
url =
|
|
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: ${
|
|
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
|
-
|
|
1686
|
-
} catch {
|
|
1687
|
-
|
|
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
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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 =
|
|
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
|
-
|
|
1700
|
-
|
|
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
|
-
|
|
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
|
|
1710
|
-
log.raw(`
|
|
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/
|
|
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
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
const
|
|
1729
|
-
const
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
|
|
1748
|
-
return "migrated";
|
|
2315
|
+
return null;
|
|
1749
2316
|
}
|
|
1750
|
-
async function
|
|
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
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
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
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
-
|
|
1772
|
-
|
|
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
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
-
|
|
2606
|
+
for (const p of targets) {
|
|
2607
|
+
const isCurrent = matched === p.name || normalizeRegistryUrl(p.url) === normalizeRegistryUrl(current);
|
|
1783
2608
|
try {
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
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
|
|
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
|
|
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.
|
|
1821
|
-
|
|
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);
|