@hexclave/cli 1.0.68 → 1.0.70
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 +101 -41
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/lib/dashboard-release.test.ts +105 -1
- package/src/lib/dashboard-release.ts +157 -41
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import fs from "node:fs";
|
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { createUrlIfValid } from "@hexclave/shared/dist/utils/urls";
|
|
20
20
|
import { createTar } from "@hexclave/shared/dist/utils/tar";
|
|
21
|
+
import * as zlib from "node:zlib";
|
|
21
22
|
import { gzipSync } from "node:zlib";
|
|
22
23
|
import { hexclaveDevEnvStatePath } from "@hexclave/shared/dist/utils/dev-env-state-path";
|
|
23
24
|
import { createHash, randomBytes } from "crypto";
|
|
@@ -32,6 +33,7 @@ import { createInitPrompt } from "@hexclave/shared/dist/helpers/init-prompt";
|
|
|
32
33
|
import { execFileSync, spawn } from "child_process";
|
|
33
34
|
import { Readable } from "stream";
|
|
34
35
|
import { pipeline } from "stream/promises";
|
|
36
|
+
import * as tar from "tar";
|
|
35
37
|
import extractZip from "extract-zip";
|
|
36
38
|
import { randomBytes as randomBytes$1 } from "node:crypto";
|
|
37
39
|
import { KnownErrors } from "@hexclave/shared";
|
|
@@ -884,7 +886,7 @@ async function dashboardRequest(path, options, secret, port) {
|
|
|
884
886
|
throw new CliError(`Failed to reach local Hexclave dashboard at ${url}: ${errorMessage(error)}`);
|
|
885
887
|
}
|
|
886
888
|
}
|
|
887
|
-
function isRecord$
|
|
889
|
+
function isRecord$2(value) {
|
|
888
890
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
889
891
|
}
|
|
890
892
|
async function responseErrorMessage(response) {
|
|
@@ -892,10 +894,10 @@ async function responseErrorMessage(response) {
|
|
|
892
894
|
if (text.length === 0) return "empty response body";
|
|
893
895
|
try {
|
|
894
896
|
const parsed = JSON.parse(text);
|
|
895
|
-
if (isRecord$
|
|
897
|
+
if (isRecord$2(parsed)) {
|
|
896
898
|
const error = parsed.error;
|
|
897
899
|
if (typeof error === "string") return error;
|
|
898
|
-
if (isRecord$
|
|
900
|
+
if (isRecord$2(error) && typeof error.message === "string") return error.message;
|
|
899
901
|
}
|
|
900
902
|
} catch {}
|
|
901
903
|
return text;
|
|
@@ -1842,6 +1844,9 @@ const DASHBOARD_CACHE_DIR_NAME = "dashboards";
|
|
|
1842
1844
|
const DASHBOARD_COMPLETE_MARKER = ".hexclave-complete";
|
|
1843
1845
|
const LOG_PREFIX$1 = "[Hexclave] ";
|
|
1844
1846
|
const SAFE_VERSION_REGEX = /^v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
|
|
1847
|
+
function isRecord$1(value) {
|
|
1848
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
1849
|
+
}
|
|
1845
1850
|
const MANIFEST_FETCH_TIMEOUT_MS = 1e4;
|
|
1846
1851
|
const DASHBOARD_DOWNLOAD_TIMEOUT_MS = 5 * 6e4;
|
|
1847
1852
|
function isAllowedDownloadUrl(url) {
|
|
@@ -1855,19 +1860,65 @@ function isAllowedDownloadUrl(url) {
|
|
|
1855
1860
|
if (parsed.protocol === "http:") return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1";
|
|
1856
1861
|
return false;
|
|
1857
1862
|
}
|
|
1863
|
+
const DASHBOARD_PLATFORM_KEYS = [
|
|
1864
|
+
"darwin-arm64",
|
|
1865
|
+
"darwin-x64",
|
|
1866
|
+
"linux-arm64",
|
|
1867
|
+
"linux-x64",
|
|
1868
|
+
"win32-arm64",
|
|
1869
|
+
"win32-x64"
|
|
1870
|
+
];
|
|
1858
1871
|
function logDashboard(message) {
|
|
1859
1872
|
console.warn(`${LOG_PREFIX$1}${message}`);
|
|
1860
1873
|
}
|
|
1861
1874
|
function parseDashboardManifest(raw) {
|
|
1862
|
-
if (raw
|
|
1875
|
+
if (!isRecord$1(raw)) return null;
|
|
1863
1876
|
const manifest = raw;
|
|
1864
1877
|
if (typeof manifest.version !== "string" || !SAFE_VERSION_REGEX.test(manifest.version)) return null;
|
|
1865
1878
|
if (typeof manifest.sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(manifest.sha256)) return null;
|
|
1866
1879
|
if (typeof manifest.url !== "string" || !isAllowedDownloadUrl(manifest.url)) return null;
|
|
1880
|
+
let platformArchives;
|
|
1881
|
+
if (manifest.platformArchives != null) {
|
|
1882
|
+
if (!isRecord$1(manifest.platformArchives)) return {
|
|
1883
|
+
version: manifest.version,
|
|
1884
|
+
sha256: manifest.sha256.toLowerCase(),
|
|
1885
|
+
url: manifest.url
|
|
1886
|
+
};
|
|
1887
|
+
const validPlatformArchives = {};
|
|
1888
|
+
for (const [platform, value] of Object.entries(manifest.platformArchives)) {
|
|
1889
|
+
if (!DASHBOARD_PLATFORM_KEYS.some((supportedKey) => supportedKey === platform)) continue;
|
|
1890
|
+
if (!isRecord$1(value)) continue;
|
|
1891
|
+
const archive = value;
|
|
1892
|
+
if (typeof archive.sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(archive.sha256) || typeof archive.url !== "string" || !isAllowedDownloadUrl(archive.url)) continue;
|
|
1893
|
+
validPlatformArchives[platform] = {
|
|
1894
|
+
sha256: archive.sha256.toLowerCase(),
|
|
1895
|
+
url: archive.url
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
if (Object.keys(validPlatformArchives).length > 0) platformArchives = validPlatformArchives;
|
|
1899
|
+
}
|
|
1867
1900
|
return {
|
|
1868
1901
|
version: manifest.version,
|
|
1869
1902
|
sha256: manifest.sha256.toLowerCase(),
|
|
1903
|
+
url: manifest.url,
|
|
1904
|
+
...platformArchives == null ? {} : { platformArchives }
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
function dashboardPlatformKey(platform = process.platform, arch = process.arch) {
|
|
1908
|
+
const key = `${platform}-${arch}`;
|
|
1909
|
+
return DASHBOARD_PLATFORM_KEYS.find((supportedKey) => supportedKey === key);
|
|
1910
|
+
}
|
|
1911
|
+
function hasZstdSupport(zlibModule = zlib) {
|
|
1912
|
+
return typeof zlibModule.createZstdCompress === "function" && typeof zlibModule.createZstdDecompress === "function";
|
|
1913
|
+
}
|
|
1914
|
+
function selectDashboardArchive(manifest, platformKey = dashboardPlatformKey(), zstdAvailable = hasZstdSupport()) {
|
|
1915
|
+
const platformArchive = !zstdAvailable || platformKey == null ? void 0 : manifest.platformArchives?.[platformKey];
|
|
1916
|
+
return platformArchive == null ? {
|
|
1917
|
+
sha256: manifest.sha256,
|
|
1870
1918
|
url: manifest.url
|
|
1919
|
+
} : {
|
|
1920
|
+
...platformArchive,
|
|
1921
|
+
cacheSuffix: platformKey
|
|
1871
1922
|
};
|
|
1872
1923
|
}
|
|
1873
1924
|
function dashboardDirOverride(env = process.env) {
|
|
@@ -1881,11 +1932,11 @@ function dashboardManifestUrl(env = process.env) {
|
|
|
1881
1932
|
function dashboardCacheRoot() {
|
|
1882
1933
|
return join(dirname(devEnvStatePath()), DASHBOARD_CACHE_DIR_NAME);
|
|
1883
1934
|
}
|
|
1884
|
-
function dashboardVersionDir(version) {
|
|
1885
|
-
return join(dashboardCacheRoot(), version);
|
|
1935
|
+
function dashboardVersionDir(version, cacheSuffix) {
|
|
1936
|
+
return join(dashboardCacheRoot(), cacheSuffix == null ? version : `${version}--${cacheSuffix}`);
|
|
1886
1937
|
}
|
|
1887
|
-
function isDashboardCached(version) {
|
|
1888
|
-
const dir =
|
|
1938
|
+
function isDashboardCached(version, cacheSuffix, cacheRoot = dashboardCacheRoot()) {
|
|
1939
|
+
const dir = join(cacheRoot, cacheSuffix == null ? version : `${version}--${cacheSuffix}`);
|
|
1889
1940
|
return existsSync(join(dir, DASHBOARD_COMPLETE_MARKER)) && existsSync(join(dir, DASHBOARD_SERVER_RELATIVE_PATH));
|
|
1890
1941
|
}
|
|
1891
1942
|
function parseVersion(version) {
|
|
@@ -1917,10 +1968,19 @@ function isVersionNewer$1(candidate, current) {
|
|
|
1917
1968
|
if (candidate.hasPrerelease !== current.hasPrerelease) return !candidate.hasPrerelease;
|
|
1918
1969
|
return false;
|
|
1919
1970
|
}
|
|
1920
|
-
function
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1971
|
+
function latestCachedDashboard(platformKey, cacheRoot = dashboardCacheRoot()) {
|
|
1972
|
+
if (!existsSync(cacheRoot)) return void 0;
|
|
1973
|
+
const suffix = platformKey == null ? "" : `--${platformKey}`;
|
|
1974
|
+
const cached = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && (platformKey == null ? !DASHBOARD_PLATFORM_KEYS.some((supportedKey) => entry.name.endsWith(`--${supportedKey}`)) : entry.name.endsWith(suffix))).map((entry) => {
|
|
1975
|
+
const version = suffix.length === 0 ? entry.name : entry.name.slice(0, -suffix.length);
|
|
1976
|
+
const root = join(cacheRoot, platformKey == null ? version : `${version}--${platformKey}`);
|
|
1977
|
+
return isDashboardCached(version, platformKey, cacheRoot) ? {
|
|
1978
|
+
version,
|
|
1979
|
+
root
|
|
1980
|
+
} : void 0;
|
|
1981
|
+
}).filter((entry) => entry != null);
|
|
1982
|
+
const version = pickLatestVersion(cached.map((entry) => entry.version));
|
|
1983
|
+
return version == null ? void 0 : cached.find((entry) => entry.version === version);
|
|
1924
1984
|
}
|
|
1925
1985
|
async function fetchDashboardManifest(env = process.env) {
|
|
1926
1986
|
const url = dashboardManifestUrl(env);
|
|
@@ -1945,39 +2005,44 @@ async function sha256File(path) {
|
|
|
1945
2005
|
await pipeline(createReadStream(path), hash);
|
|
1946
2006
|
return hash.digest("hex");
|
|
1947
2007
|
}
|
|
1948
|
-
async function downloadDashboardRelease(manifest, onProgress) {
|
|
2008
|
+
async function downloadDashboardRelease(manifest, archive, onProgress) {
|
|
1949
2009
|
const cacheRoot = dashboardCacheRoot();
|
|
1950
2010
|
mkdirSync(cacheRoot, { recursive: true });
|
|
1951
2011
|
const suffix = `${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
1952
|
-
const
|
|
2012
|
+
const tmpArchive = join(cacheRoot, `.download-${manifest.version}-${suffix}${archive.cacheSuffix == null ? ".zip" : ".tar.zst"}`);
|
|
1953
2013
|
const tmpDir = join(cacheRoot, `.extract-${manifest.version}-${suffix}`);
|
|
1954
|
-
const targetDir = dashboardVersionDir(manifest.version);
|
|
2014
|
+
const targetDir = dashboardVersionDir(manifest.version, archive.cacheSuffix);
|
|
1955
2015
|
try {
|
|
1956
2016
|
onProgress?.(`Downloading Hexclave dashboard ${manifest.version}`);
|
|
1957
|
-
const response = await fetch(
|
|
2017
|
+
const response = await fetch(archive.url, {
|
|
1958
2018
|
redirect: "follow",
|
|
1959
2019
|
signal: AbortSignal.timeout(DASHBOARD_DOWNLOAD_TIMEOUT_MS)
|
|
1960
2020
|
});
|
|
1961
2021
|
if (!isAllowedDownloadUrl(response.url)) throw new CliError(`Dashboard ${manifest.version} download was redirected to a disallowed URL (${response.url}).`);
|
|
1962
|
-
if (!response.ok || response.body == null) throw new CliError(`Failed to download dashboard ${manifest.version} (HTTP ${response.status}) from ${
|
|
1963
|
-
await pipeline(Readable.fromWeb(response.body), createWriteStream(
|
|
2022
|
+
if (!response.ok || response.body == null) throw new CliError(`Failed to download dashboard ${manifest.version} (HTTP ${response.status}) from ${archive.url}.`);
|
|
2023
|
+
await pipeline(Readable.fromWeb(response.body), createWriteStream(tmpArchive));
|
|
1964
2024
|
onProgress?.(`Verifying Hexclave dashboard ${manifest.version}`);
|
|
1965
|
-
const digest = await sha256File(
|
|
1966
|
-
if (digest !==
|
|
2025
|
+
const digest = await sha256File(tmpArchive);
|
|
2026
|
+
if (digest !== archive.sha256) throw new CliError(`Dashboard ${manifest.version} failed its integrity check (expected ${archive.sha256}, got ${digest}).`);
|
|
1967
2027
|
rmSync(tmpDir, {
|
|
1968
2028
|
recursive: true,
|
|
1969
2029
|
force: true
|
|
1970
2030
|
});
|
|
1971
2031
|
mkdirSync(tmpDir, { recursive: true });
|
|
1972
2032
|
onProgress?.(`Extracting Hexclave dashboard ${manifest.version}`);
|
|
1973
|
-
await extractZip(
|
|
2033
|
+
if (archive.cacheSuffix == null) await extractZip(tmpArchive, { dir: tmpDir });
|
|
2034
|
+
else await pipeline(createReadStream(tmpArchive), zlib.createZstdDecompress(), tar.x({
|
|
2035
|
+
cwd: tmpDir,
|
|
2036
|
+
strict: true,
|
|
2037
|
+
preservePaths: false
|
|
2038
|
+
}));
|
|
1974
2039
|
if (!existsSync(join(tmpDir, DASHBOARD_SERVER_RELATIVE_PATH))) throw new CliError(`Dashboard ${manifest.version} archive is missing its server entrypoint.`);
|
|
1975
|
-
writeFileSync(join(tmpDir, DASHBOARD_COMPLETE_MARKER), `${
|
|
1976
|
-
if (isDashboardCached(manifest.version)) return;
|
|
2040
|
+
writeFileSync(join(tmpDir, DASHBOARD_COMPLETE_MARKER), `${archive.sha256}\n`);
|
|
2041
|
+
if (isDashboardCached(manifest.version, archive.cacheSuffix)) return;
|
|
1977
2042
|
try {
|
|
1978
2043
|
renameSync(tmpDir, targetDir);
|
|
1979
2044
|
} catch {
|
|
1980
|
-
if (isDashboardCached(manifest.version)) return;
|
|
2045
|
+
if (isDashboardCached(manifest.version, archive.cacheSuffix)) return;
|
|
1981
2046
|
rmSync(targetDir, {
|
|
1982
2047
|
recursive: true,
|
|
1983
2048
|
force: true
|
|
@@ -1985,7 +2050,7 @@ async function downloadDashboardRelease(manifest, onProgress) {
|
|
|
1985
2050
|
renameSync(tmpDir, targetDir);
|
|
1986
2051
|
}
|
|
1987
2052
|
} finally {
|
|
1988
|
-
rmSync(
|
|
2053
|
+
rmSync(tmpArchive, { force: true });
|
|
1989
2054
|
rmSync(tmpDir, {
|
|
1990
2055
|
recursive: true,
|
|
1991
2056
|
force: true
|
|
@@ -2003,35 +2068,30 @@ async function resolveDashboardRuntime(opts = {}) {
|
|
|
2003
2068
|
}
|
|
2004
2069
|
const manifest = opts.manifest !== void 0 ? opts.manifest : await fetchDashboardManifest();
|
|
2005
2070
|
if (manifest != null) {
|
|
2006
|
-
|
|
2007
|
-
|
|
2071
|
+
const archive = selectDashboardArchive(manifest);
|
|
2072
|
+
if (isDashboardCached(manifest.version, archive.cacheSuffix)) return {
|
|
2073
|
+
root: dashboardVersionDir(manifest.version, archive.cacheSuffix),
|
|
2008
2074
|
version: manifest.version
|
|
2009
2075
|
};
|
|
2010
2076
|
try {
|
|
2011
|
-
await downloadDashboardRelease(manifest, opts.onProgress);
|
|
2077
|
+
await downloadDashboardRelease(manifest, archive, opts.onProgress);
|
|
2012
2078
|
return {
|
|
2013
|
-
root: dashboardVersionDir(manifest.version),
|
|
2079
|
+
root: dashboardVersionDir(manifest.version, archive.cacheSuffix),
|
|
2014
2080
|
version: manifest.version
|
|
2015
2081
|
};
|
|
2016
2082
|
} catch (error) {
|
|
2017
|
-
const cached =
|
|
2083
|
+
const cached = latestCachedDashboard(dashboardPlatformKey()) ?? latestCachedDashboard();
|
|
2018
2084
|
if (cached != null) {
|
|
2019
|
-
logDashboard(`Failed to download dashboard ${manifest.version} (${errorMessage$1(error)}); using cached ${cached}.`);
|
|
2020
|
-
return
|
|
2021
|
-
root: dashboardVersionDir(cached),
|
|
2022
|
-
version: cached
|
|
2023
|
-
};
|
|
2085
|
+
logDashboard(`Failed to download dashboard ${manifest.version} (${errorMessage$1(error)}); using cached ${cached.version}.`);
|
|
2086
|
+
return cached;
|
|
2024
2087
|
}
|
|
2025
2088
|
throw error;
|
|
2026
2089
|
}
|
|
2027
2090
|
}
|
|
2028
|
-
const cached =
|
|
2091
|
+
const cached = latestCachedDashboard(dashboardPlatformKey()) ?? latestCachedDashboard();
|
|
2029
2092
|
if (cached != null) {
|
|
2030
|
-
logDashboard(`Offline: using cached Hexclave dashboard ${cached}.`);
|
|
2031
|
-
return
|
|
2032
|
-
root: dashboardVersionDir(cached),
|
|
2033
|
-
version: cached
|
|
2034
|
-
};
|
|
2093
|
+
logDashboard(`Offline: using cached Hexclave dashboard ${cached.version}.`);
|
|
2094
|
+
return cached;
|
|
2035
2095
|
}
|
|
2036
2096
|
throw new CliError(["Could not download the Hexclave development-environment dashboard and no cached copy is available.", `Check your network connection, or set ${DASHBOARD_DIR_OVERRIDE_ENV_VAR} to a local dashboard build.`].join(" "));
|
|
2037
2097
|
}
|