@hexclave/cli 1.0.67 → 1.0.69
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 +346 -65
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/commands/init.ts +0 -4
- package/src/commands/project.ts +4 -5
- package/src/commands/team.command.test.ts +124 -0
- package/src/commands/team.test.ts +25 -0
- package/src/commands/team.ts +301 -0
- package/src/index.ts +2 -0
- package/src/lib/create-project.ts +11 -13
- package/src/lib/dashboard-release.test.ts +105 -1
- package/src/lib/dashboard-release.ts +157 -41
- package/src/lib/resolve-team.test.ts +58 -0
- package/src/lib/resolve-team.ts +62 -0
- package/dist/errors-CyPbl0LV.js +0 -21
- package/dist/errors-CyPbl0LV.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as CliError, r as errorMessage$1, t as AuthError } from "./errors-CyPbl0LV.js";
|
|
3
2
|
import * as Sentry from "@sentry/node";
|
|
4
3
|
import "@hexclave/shared/dist/utils/env";
|
|
5
4
|
import { StatusError, captureError, registerErrorSink, throwErr } from "@hexclave/shared/dist/utils/errors";
|
|
@@ -19,6 +18,7 @@ import fs from "node:fs";
|
|
|
19
18
|
import path from "node:path";
|
|
20
19
|
import { createUrlIfValid } from "@hexclave/shared/dist/utils/urls";
|
|
21
20
|
import { createTar } from "@hexclave/shared/dist/utils/tar";
|
|
21
|
+
import * as zlib from "node:zlib";
|
|
22
22
|
import { gzipSync } from "node:zlib";
|
|
23
23
|
import { hexclaveDevEnvStatePath } from "@hexclave/shared/dist/utils/dev-env-state-path";
|
|
24
24
|
import { createHash, randomBytes } from "crypto";
|
|
@@ -33,8 +33,10 @@ import { createInitPrompt } from "@hexclave/shared/dist/helpers/init-prompt";
|
|
|
33
33
|
import { execFileSync, spawn } from "child_process";
|
|
34
34
|
import { Readable } from "stream";
|
|
35
35
|
import { pipeline } from "stream/promises";
|
|
36
|
+
import * as tar from "tar";
|
|
36
37
|
import extractZip from "extract-zip";
|
|
37
38
|
import { randomBytes as randomBytes$1 } from "node:crypto";
|
|
39
|
+
import { KnownErrors } from "@hexclave/shared";
|
|
38
40
|
//#region \0rolldown/runtime.js
|
|
39
41
|
var __defProp = Object.defineProperty;
|
|
40
42
|
var __exportAll = (all, no_symbols) => {
|
|
@@ -129,6 +131,23 @@ function initSentry() {
|
|
|
129
131
|
});
|
|
130
132
|
}
|
|
131
133
|
//#endregion
|
|
134
|
+
//#region src/lib/errors.ts
|
|
135
|
+
var CliError = class extends Error {
|
|
136
|
+
constructor(message) {
|
|
137
|
+
super(message);
|
|
138
|
+
this.name = "CliError";
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
var AuthError = class extends CliError {
|
|
142
|
+
constructor(message) {
|
|
143
|
+
super(message);
|
|
144
|
+
this.name = "AuthError";
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
function errorMessage$1(error) {
|
|
148
|
+
return error instanceof Error ? error.message : String(error);
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
132
151
|
//#region src/lib/config.ts
|
|
133
152
|
const ENV_CONFIG_PATH = process.env.STACK_CLI_CONFIG_PATH;
|
|
134
153
|
const HEXCLAVE_CONFIG_PATH = path$1.join(os.homedir(), ".config", "hexclave", "credentials.json");
|
|
@@ -867,7 +886,7 @@ async function dashboardRequest(path, options, secret, port) {
|
|
|
867
886
|
throw new CliError(`Failed to reach local Hexclave dashboard at ${url}: ${errorMessage(error)}`);
|
|
868
887
|
}
|
|
869
888
|
}
|
|
870
|
-
function isRecord$
|
|
889
|
+
function isRecord$2(value) {
|
|
871
890
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
872
891
|
}
|
|
873
892
|
async function responseErrorMessage(response) {
|
|
@@ -875,10 +894,10 @@ async function responseErrorMessage(response) {
|
|
|
875
894
|
if (text.length === 0) return "empty response body";
|
|
876
895
|
try {
|
|
877
896
|
const parsed = JSON.parse(text);
|
|
878
|
-
if (isRecord$
|
|
897
|
+
if (isRecord$2(parsed)) {
|
|
879
898
|
const error = parsed.error;
|
|
880
899
|
if (typeof error === "string") return error;
|
|
881
|
-
if (isRecord$
|
|
900
|
+
if (isRecord$2(error) && typeof error.message === "string") return error.message;
|
|
882
901
|
}
|
|
883
902
|
} catch {}
|
|
884
903
|
return text;
|
|
@@ -1388,6 +1407,39 @@ function isNonInteractiveEnv() {
|
|
|
1388
1407
|
return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.NONINTERACTIVE || !process.stdin.isTTY);
|
|
1389
1408
|
}
|
|
1390
1409
|
//#endregion
|
|
1410
|
+
//#region src/lib/resolve-team.ts
|
|
1411
|
+
async function resolveTeam(teams, options = {}) {
|
|
1412
|
+
const teamId = options.teamId?.trim();
|
|
1413
|
+
if (teamId) {
|
|
1414
|
+
const team = teams.find((candidate) => candidate.id === teamId);
|
|
1415
|
+
if (team == null) throw new CliError(`Team '${teamId}' was not found on your account.`);
|
|
1416
|
+
return team;
|
|
1417
|
+
}
|
|
1418
|
+
if (teams.length === 0) {
|
|
1419
|
+
if (options.createTeam != null) {
|
|
1420
|
+
if (isNonInteractiveEnv()) throw new CliError("No teams found. Run `hexclave team create` first, or pass --team-id.");
|
|
1421
|
+
const displayName = (await input({
|
|
1422
|
+
message: "Team display name:",
|
|
1423
|
+
validate: (value) => value.trim().length > 0 || "Display name cannot be empty."
|
|
1424
|
+
})).trim();
|
|
1425
|
+
return await options.createTeam(displayName);
|
|
1426
|
+
}
|
|
1427
|
+
throw new CliError("No teams found. Run `hexclave team create` first.");
|
|
1428
|
+
}
|
|
1429
|
+
if (teams.length === 1) return teams[0];
|
|
1430
|
+
if (isNonInteractiveEnv()) throw new CliError("--team-id is required in non-interactive environments (CI).");
|
|
1431
|
+
const selectedTeamId = await select({
|
|
1432
|
+
message: "Choose a team:",
|
|
1433
|
+
choices: teams.map((team) => ({
|
|
1434
|
+
name: `${team.displayName || "(unnamed)"} (${team.id})`,
|
|
1435
|
+
value: team.id
|
|
1436
|
+
}))
|
|
1437
|
+
});
|
|
1438
|
+
const selectedTeam = teams.find((team) => team.id === selectedTeamId);
|
|
1439
|
+
if (selectedTeam == null) throw new CliError("The selected team was not found.");
|
|
1440
|
+
return selectedTeam;
|
|
1441
|
+
}
|
|
1442
|
+
//#endregion
|
|
1391
1443
|
//#region src/lib/create-project.ts
|
|
1392
1444
|
var create_project_exports = /* @__PURE__ */ __exportAll({ createProjectInteractively: () => createProjectInteractively });
|
|
1393
1445
|
async function createProjectInteractively(user, opts = {}) {
|
|
@@ -1400,14 +1452,14 @@ async function createProjectInteractively(user, opts = {}) {
|
|
|
1400
1452
|
validate: (v) => v.trim().length > 0 || "Display name cannot be empty."
|
|
1401
1453
|
})).trim();
|
|
1402
1454
|
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
return await user.createProject({
|
|
1407
|
-
displayName,
|
|
1408
|
-
teamId: teams[0].id
|
|
1409
|
-
});
|
|
1455
|
+
const team = await resolveTeam(await user.listTeams(), {
|
|
1456
|
+
teamId: opts.teamId,
|
|
1457
|
+
createTeam: async (teamDisplayName) => await user.createTeam({ displayName: teamDisplayName })
|
|
1410
1458
|
});
|
|
1459
|
+
return await withProgress("Creating project", async () => await user.createProject({
|
|
1460
|
+
displayName,
|
|
1461
|
+
teamId: team.id
|
|
1462
|
+
}));
|
|
1411
1463
|
}
|
|
1412
1464
|
//#endregion
|
|
1413
1465
|
//#region src/commands/init.ts
|
|
@@ -1590,12 +1642,9 @@ async function writeProjectKeysToEnv(project, outputDir) {
|
|
|
1590
1642
|
}
|
|
1591
1643
|
async function handleCreateCloud(_flags, opts, outputDir) {
|
|
1592
1644
|
const sessionAuth = await ensureLoggedInSession();
|
|
1593
|
-
const
|
|
1594
|
-
const { dashboardUrl } = resolveLoginConfig();
|
|
1595
|
-
const newProject = await createProjectInteractively(user, {
|
|
1645
|
+
const newProject = await createProjectInteractively(await withProgress("Loading account", async () => await getInternalUser(sessionAuth)), {
|
|
1596
1646
|
displayName: opts.displayName,
|
|
1597
|
-
defaultDisplayName: path$1.basename(outputDir)
|
|
1598
|
-
dashboardUrl
|
|
1647
|
+
defaultDisplayName: path$1.basename(outputDir)
|
|
1599
1648
|
});
|
|
1600
1649
|
console.log(`\nCreated project: ${newProject.displayName} (${newProject.id})\n`);
|
|
1601
1650
|
await writeProjectKeysToEnv(newProject, outputDir);
|
|
@@ -1622,11 +1671,7 @@ async function handleLinkFromCloud(_flags, opts, outputDir) {
|
|
|
1622
1671
|
const { dashboardUrl } = resolveLoginConfig();
|
|
1623
1672
|
throw new CliError(`You don't own any projects. Create one at ${dashboardUrl} or re-run and choose to create one.`);
|
|
1624
1673
|
}
|
|
1625
|
-
const
|
|
1626
|
-
const newProject = await createProjectInteractively(user, {
|
|
1627
|
-
defaultDisplayName: path$1.basename(outputDir),
|
|
1628
|
-
dashboardUrl
|
|
1629
|
-
});
|
|
1674
|
+
const newProject = await createProjectInteractively(user, { defaultDisplayName: path$1.basename(outputDir) });
|
|
1630
1675
|
console.log(`\nCreated project: ${newProject.displayName} (${newProject.id})\n`);
|
|
1631
1676
|
projects = [newProject];
|
|
1632
1677
|
autoCreatedProjectId = newProject.id;
|
|
@@ -1741,19 +1786,17 @@ function registerProjectCommand(program) {
|
|
|
1741
1786
|
if (program.opts().json) console.log(JSON.stringify(results, null, 2));
|
|
1742
1787
|
else console.log(formatProjectList(results));
|
|
1743
1788
|
});
|
|
1744
|
-
project.command("create").description("Create a new cloud project").option("--cloud", "Confirm that this creates a cloud project").option("--display-name <name>", "Project display name").action(async (opts) => {
|
|
1789
|
+
project.command("create").description("Create a new cloud project").option("--cloud", "Confirm that this creates a cloud project").option("--display-name <name>", "Project display name").option("--team-id <id>", "Team that owns the project").action(async (opts) => {
|
|
1745
1790
|
if (!opts.cloud) throw new CliError("hexclave project create currently only creates cloud projects. Pass --cloud to confirm.");
|
|
1746
|
-
const [{ getInternalUser }, {
|
|
1791
|
+
const [{ getInternalUser }, { resolveSessionAuth }, { createProjectInteractively }] = await Promise.all([
|
|
1747
1792
|
Promise.resolve().then(() => app_exports),
|
|
1748
1793
|
Promise.resolve().then(() => auth_exports),
|
|
1749
1794
|
Promise.resolve().then(() => create_project_exports)
|
|
1750
1795
|
]);
|
|
1751
1796
|
const auth = resolveSessionAuth();
|
|
1752
|
-
const
|
|
1753
|
-
const { dashboardUrl } = resolveLoginConfig();
|
|
1754
|
-
const newProject = await createProjectInteractively(user, {
|
|
1797
|
+
const newProject = await createProjectInteractively(await withProgress("Loading account", async () => await getInternalUser(auth)), {
|
|
1755
1798
|
displayName: opts.displayName,
|
|
1756
|
-
|
|
1799
|
+
teamId: opts.teamId
|
|
1757
1800
|
});
|
|
1758
1801
|
if (program.opts().json) console.log(JSON.stringify({
|
|
1759
1802
|
id: newProject.id,
|
|
@@ -1801,6 +1844,9 @@ const DASHBOARD_CACHE_DIR_NAME = "dashboards";
|
|
|
1801
1844
|
const DASHBOARD_COMPLETE_MARKER = ".hexclave-complete";
|
|
1802
1845
|
const LOG_PREFIX$1 = "[Hexclave] ";
|
|
1803
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
|
+
}
|
|
1804
1850
|
const MANIFEST_FETCH_TIMEOUT_MS = 1e4;
|
|
1805
1851
|
const DASHBOARD_DOWNLOAD_TIMEOUT_MS = 5 * 6e4;
|
|
1806
1852
|
function isAllowedDownloadUrl(url) {
|
|
@@ -1814,19 +1860,65 @@ function isAllowedDownloadUrl(url) {
|
|
|
1814
1860
|
if (parsed.protocol === "http:") return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1";
|
|
1815
1861
|
return false;
|
|
1816
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
|
+
];
|
|
1817
1871
|
function logDashboard(message) {
|
|
1818
1872
|
console.warn(`${LOG_PREFIX$1}${message}`);
|
|
1819
1873
|
}
|
|
1820
1874
|
function parseDashboardManifest(raw) {
|
|
1821
|
-
if (raw
|
|
1875
|
+
if (!isRecord$1(raw)) return null;
|
|
1822
1876
|
const manifest = raw;
|
|
1823
1877
|
if (typeof manifest.version !== "string" || !SAFE_VERSION_REGEX.test(manifest.version)) return null;
|
|
1824
1878
|
if (typeof manifest.sha256 !== "string" || !/^[0-9a-f]{64}$/i.test(manifest.sha256)) return null;
|
|
1825
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
|
+
}
|
|
1826
1900
|
return {
|
|
1827
1901
|
version: manifest.version,
|
|
1828
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,
|
|
1829
1918
|
url: manifest.url
|
|
1919
|
+
} : {
|
|
1920
|
+
...platformArchive,
|
|
1921
|
+
cacheSuffix: platformKey
|
|
1830
1922
|
};
|
|
1831
1923
|
}
|
|
1832
1924
|
function dashboardDirOverride(env = process.env) {
|
|
@@ -1840,11 +1932,11 @@ function dashboardManifestUrl(env = process.env) {
|
|
|
1840
1932
|
function dashboardCacheRoot() {
|
|
1841
1933
|
return join(dirname(devEnvStatePath()), DASHBOARD_CACHE_DIR_NAME);
|
|
1842
1934
|
}
|
|
1843
|
-
function dashboardVersionDir(version) {
|
|
1844
|
-
return join(dashboardCacheRoot(), version);
|
|
1935
|
+
function dashboardVersionDir(version, cacheSuffix) {
|
|
1936
|
+
return join(dashboardCacheRoot(), cacheSuffix == null ? version : `${version}--${cacheSuffix}`);
|
|
1845
1937
|
}
|
|
1846
|
-
function isDashboardCached(version) {
|
|
1847
|
-
const dir =
|
|
1938
|
+
function isDashboardCached(version, cacheSuffix, cacheRoot = dashboardCacheRoot()) {
|
|
1939
|
+
const dir = join(cacheRoot, cacheSuffix == null ? version : `${version}--${cacheSuffix}`);
|
|
1848
1940
|
return existsSync(join(dir, DASHBOARD_COMPLETE_MARKER)) && existsSync(join(dir, DASHBOARD_SERVER_RELATIVE_PATH));
|
|
1849
1941
|
}
|
|
1850
1942
|
function parseVersion(version) {
|
|
@@ -1876,10 +1968,19 @@ function isVersionNewer$1(candidate, current) {
|
|
|
1876
1968
|
if (candidate.hasPrerelease !== current.hasPrerelease) return !candidate.hasPrerelease;
|
|
1877
1969
|
return false;
|
|
1878
1970
|
}
|
|
1879
|
-
function
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
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);
|
|
1883
1984
|
}
|
|
1884
1985
|
async function fetchDashboardManifest(env = process.env) {
|
|
1885
1986
|
const url = dashboardManifestUrl(env);
|
|
@@ -1904,39 +2005,44 @@ async function sha256File(path) {
|
|
|
1904
2005
|
await pipeline(createReadStream(path), hash);
|
|
1905
2006
|
return hash.digest("hex");
|
|
1906
2007
|
}
|
|
1907
|
-
async function downloadDashboardRelease(manifest, onProgress) {
|
|
2008
|
+
async function downloadDashboardRelease(manifest, archive, onProgress) {
|
|
1908
2009
|
const cacheRoot = dashboardCacheRoot();
|
|
1909
2010
|
mkdirSync(cacheRoot, { recursive: true });
|
|
1910
2011
|
const suffix = `${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
1911
|
-
const
|
|
2012
|
+
const tmpArchive = join(cacheRoot, `.download-${manifest.version}-${suffix}${archive.cacheSuffix == null ? ".zip" : ".tar.zst"}`);
|
|
1912
2013
|
const tmpDir = join(cacheRoot, `.extract-${manifest.version}-${suffix}`);
|
|
1913
|
-
const targetDir = dashboardVersionDir(manifest.version);
|
|
2014
|
+
const targetDir = dashboardVersionDir(manifest.version, archive.cacheSuffix);
|
|
1914
2015
|
try {
|
|
1915
2016
|
onProgress?.(`Downloading Hexclave dashboard ${manifest.version}`);
|
|
1916
|
-
const response = await fetch(
|
|
2017
|
+
const response = await fetch(archive.url, {
|
|
1917
2018
|
redirect: "follow",
|
|
1918
2019
|
signal: AbortSignal.timeout(DASHBOARD_DOWNLOAD_TIMEOUT_MS)
|
|
1919
2020
|
});
|
|
1920
2021
|
if (!isAllowedDownloadUrl(response.url)) throw new CliError(`Dashboard ${manifest.version} download was redirected to a disallowed URL (${response.url}).`);
|
|
1921
|
-
if (!response.ok || response.body == null) throw new CliError(`Failed to download dashboard ${manifest.version} (HTTP ${response.status}) from ${
|
|
1922
|
-
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));
|
|
1923
2024
|
onProgress?.(`Verifying Hexclave dashboard ${manifest.version}`);
|
|
1924
|
-
const digest = await sha256File(
|
|
1925
|
-
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}).`);
|
|
1926
2027
|
rmSync(tmpDir, {
|
|
1927
2028
|
recursive: true,
|
|
1928
2029
|
force: true
|
|
1929
2030
|
});
|
|
1930
2031
|
mkdirSync(tmpDir, { recursive: true });
|
|
1931
2032
|
onProgress?.(`Extracting Hexclave dashboard ${manifest.version}`);
|
|
1932
|
-
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
|
+
}));
|
|
1933
2039
|
if (!existsSync(join(tmpDir, DASHBOARD_SERVER_RELATIVE_PATH))) throw new CliError(`Dashboard ${manifest.version} archive is missing its server entrypoint.`);
|
|
1934
|
-
writeFileSync(join(tmpDir, DASHBOARD_COMPLETE_MARKER), `${
|
|
1935
|
-
if (isDashboardCached(manifest.version)) return;
|
|
2040
|
+
writeFileSync(join(tmpDir, DASHBOARD_COMPLETE_MARKER), `${archive.sha256}\n`);
|
|
2041
|
+
if (isDashboardCached(manifest.version, archive.cacheSuffix)) return;
|
|
1936
2042
|
try {
|
|
1937
2043
|
renameSync(tmpDir, targetDir);
|
|
1938
2044
|
} catch {
|
|
1939
|
-
if (isDashboardCached(manifest.version)) return;
|
|
2045
|
+
if (isDashboardCached(manifest.version, archive.cacheSuffix)) return;
|
|
1940
2046
|
rmSync(targetDir, {
|
|
1941
2047
|
recursive: true,
|
|
1942
2048
|
force: true
|
|
@@ -1944,7 +2050,7 @@ async function downloadDashboardRelease(manifest, onProgress) {
|
|
|
1944
2050
|
renameSync(tmpDir, targetDir);
|
|
1945
2051
|
}
|
|
1946
2052
|
} finally {
|
|
1947
|
-
rmSync(
|
|
2053
|
+
rmSync(tmpArchive, { force: true });
|
|
1948
2054
|
rmSync(tmpDir, {
|
|
1949
2055
|
recursive: true,
|
|
1950
2056
|
force: true
|
|
@@ -1962,35 +2068,30 @@ async function resolveDashboardRuntime(opts = {}) {
|
|
|
1962
2068
|
}
|
|
1963
2069
|
const manifest = opts.manifest !== void 0 ? opts.manifest : await fetchDashboardManifest();
|
|
1964
2070
|
if (manifest != null) {
|
|
1965
|
-
|
|
1966
|
-
|
|
2071
|
+
const archive = selectDashboardArchive(manifest);
|
|
2072
|
+
if (isDashboardCached(manifest.version, archive.cacheSuffix)) return {
|
|
2073
|
+
root: dashboardVersionDir(manifest.version, archive.cacheSuffix),
|
|
1967
2074
|
version: manifest.version
|
|
1968
2075
|
};
|
|
1969
2076
|
try {
|
|
1970
|
-
await downloadDashboardRelease(manifest, opts.onProgress);
|
|
2077
|
+
await downloadDashboardRelease(manifest, archive, opts.onProgress);
|
|
1971
2078
|
return {
|
|
1972
|
-
root: dashboardVersionDir(manifest.version),
|
|
2079
|
+
root: dashboardVersionDir(manifest.version, archive.cacheSuffix),
|
|
1973
2080
|
version: manifest.version
|
|
1974
2081
|
};
|
|
1975
2082
|
} catch (error) {
|
|
1976
|
-
const cached =
|
|
2083
|
+
const cached = latestCachedDashboard(dashboardPlatformKey()) ?? latestCachedDashboard();
|
|
1977
2084
|
if (cached != null) {
|
|
1978
|
-
logDashboard(`Failed to download dashboard ${manifest.version} (${errorMessage$1(error)}); using cached ${cached}.`);
|
|
1979
|
-
return
|
|
1980
|
-
root: dashboardVersionDir(cached),
|
|
1981
|
-
version: cached
|
|
1982
|
-
};
|
|
2085
|
+
logDashboard(`Failed to download dashboard ${manifest.version} (${errorMessage$1(error)}); using cached ${cached.version}.`);
|
|
2086
|
+
return cached;
|
|
1983
2087
|
}
|
|
1984
2088
|
throw error;
|
|
1985
2089
|
}
|
|
1986
2090
|
}
|
|
1987
|
-
const cached =
|
|
2091
|
+
const cached = latestCachedDashboard(dashboardPlatformKey()) ?? latestCachedDashboard();
|
|
1988
2092
|
if (cached != null) {
|
|
1989
|
-
logDashboard(`Offline: using cached Hexclave dashboard ${cached}.`);
|
|
1990
|
-
return
|
|
1991
|
-
root: dashboardVersionDir(cached),
|
|
1992
|
-
version: cached
|
|
1993
|
-
};
|
|
2093
|
+
logDashboard(`Offline: using cached Hexclave dashboard ${cached.version}.`);
|
|
2094
|
+
return cached;
|
|
1994
2095
|
}
|
|
1995
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(" "));
|
|
1996
2097
|
}
|
|
@@ -3323,6 +3424,185 @@ function registerWhoamiCommand(program) {
|
|
|
3323
3424
|
});
|
|
3324
3425
|
}
|
|
3325
3426
|
//#endregion
|
|
3427
|
+
//#region src/commands/team.ts
|
|
3428
|
+
function isJson(program) {
|
|
3429
|
+
return Boolean(program.opts().json);
|
|
3430
|
+
}
|
|
3431
|
+
function printResult(program, result, humanOutput) {
|
|
3432
|
+
console.log(isJson(program) ? JSON.stringify(result, null, 2) : humanOutput);
|
|
3433
|
+
}
|
|
3434
|
+
async function withTeamPermissionError(operation, callback) {
|
|
3435
|
+
try {
|
|
3436
|
+
return await callback();
|
|
3437
|
+
} catch (error) {
|
|
3438
|
+
if (KnownErrors.TeamPermissionRequired.isInstance(error)) throw new CliError(`Cannot ${operation}. You do not have the required permission for this team.`);
|
|
3439
|
+
throw error;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
async function getTeam(user, options) {
|
|
3443
|
+
return await resolveTeam(await user.listTeams(), options);
|
|
3444
|
+
}
|
|
3445
|
+
async function getUser() {
|
|
3446
|
+
return await getInternalUser(resolveSessionAuth());
|
|
3447
|
+
}
|
|
3448
|
+
async function confirmDestructive(action, options) {
|
|
3449
|
+
if (options.yes) return;
|
|
3450
|
+
if (isNonInteractiveEnv()) throw new CliError(`--yes is required to ${action} in non-interactive environments (CI).`);
|
|
3451
|
+
if (!await confirm({
|
|
3452
|
+
message: `Are you sure you want to ${action}?`,
|
|
3453
|
+
default: false
|
|
3454
|
+
})) throw new CliError("Aborted.");
|
|
3455
|
+
}
|
|
3456
|
+
function printTeams(program, teams) {
|
|
3457
|
+
const result = teams.map((team) => ({
|
|
3458
|
+
id: team.id,
|
|
3459
|
+
displayName: team.displayName
|
|
3460
|
+
}));
|
|
3461
|
+
printResult(program, result, formatTeamList(result));
|
|
3462
|
+
}
|
|
3463
|
+
function formatTeamList(teams) {
|
|
3464
|
+
if (teams.length === 0) return "No teams found.";
|
|
3465
|
+
return teams.map((team) => `${team.id}\t${team.displayName}`).join("\n");
|
|
3466
|
+
}
|
|
3467
|
+
function formatTeamMembers(members) {
|
|
3468
|
+
if (members.length === 0) return "No team members found.";
|
|
3469
|
+
return members.map((member) => `${member.id}\t${member.displayName ?? "(none)"}`).join("\n");
|
|
3470
|
+
}
|
|
3471
|
+
function printTeam(program, team, prefix) {
|
|
3472
|
+
const result = {
|
|
3473
|
+
id: team.id,
|
|
3474
|
+
displayName: team.displayName
|
|
3475
|
+
};
|
|
3476
|
+
printResult(program, result, `${prefix}: ${result.id} (${result.displayName})`);
|
|
3477
|
+
}
|
|
3478
|
+
function registerTeamMembersCommand(team, program) {
|
|
3479
|
+
const members = team.command("members").description("Manage team members");
|
|
3480
|
+
members.command("list").description("List members of a team").option("--team-id <id>", "Team ID").action(async (options) => {
|
|
3481
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3482
|
+
const result = (await withTeamPermissionError("list team members", () => selectedTeam.listUsers())).map((member) => ({
|
|
3483
|
+
id: member.id,
|
|
3484
|
+
displayName: member.teamProfile.displayName
|
|
3485
|
+
}));
|
|
3486
|
+
printResult(program, result, formatTeamMembers(result));
|
|
3487
|
+
});
|
|
3488
|
+
members.command("remove").description("Remove a member from a team").option("--team-id <id>", "Team ID").requiredOption("--user-id <id>", "User ID to remove").option("-y, --yes", "Skip the confirmation prompt").action(async (options) => {
|
|
3489
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3490
|
+
await confirmDestructive(`remove user ${options.userId} from team ${selectedTeam.id}`, options);
|
|
3491
|
+
await withTeamPermissionError("remove this team member", () => selectedTeam.removeUser(options.userId));
|
|
3492
|
+
printResult(program, {
|
|
3493
|
+
teamId: selectedTeam.id,
|
|
3494
|
+
userId: options.userId
|
|
3495
|
+
}, `Removed team member: ${options.userId}`);
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
function registerTeamInvitationsCommand(team, program) {
|
|
3499
|
+
const invitations = team.command("invitations").description("Manage team invitations");
|
|
3500
|
+
invitations.command("list").description("List invitations sent by a team").option("--team-id <id>", "Team ID").action(async (options) => {
|
|
3501
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3502
|
+
const result = (await withTeamPermissionError("list team invitations", () => selectedTeam.listInvitations())).map((invitation) => ({
|
|
3503
|
+
id: invitation.id,
|
|
3504
|
+
email: invitation.recipientEmail,
|
|
3505
|
+
expiresAt: invitation.expiresAt.toISOString()
|
|
3506
|
+
}));
|
|
3507
|
+
printResult(program, result, result.length === 0 ? "No invitations found." : result.map((invitation) => `${invitation.id}\t${invitation.email ?? "(unknown)"}\t${invitation.expiresAt}`).join("\n"));
|
|
3508
|
+
});
|
|
3509
|
+
invitations.command("revoke").description("Revoke a team invitation").option("--team-id <id>", "Team ID").requiredOption("--invitation-id <id>", "Invitation ID").option("-y, --yes", "Skip the confirmation prompt").action(async (options) => {
|
|
3510
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3511
|
+
await confirmDestructive(`revoke invitation ${options.invitationId}`, options);
|
|
3512
|
+
const invitation = (await withTeamPermissionError("list team invitations", () => selectedTeam.listInvitations())).find((candidate) => candidate.id === options.invitationId);
|
|
3513
|
+
if (invitation == null) throw new CliError(`Invitation '${options.invitationId}' was not found for team '${selectedTeam.id}'.`);
|
|
3514
|
+
await withTeamPermissionError("revoke this invitation", () => invitation.revoke());
|
|
3515
|
+
printResult(program, {
|
|
3516
|
+
teamId: selectedTeam.id,
|
|
3517
|
+
invitationId: options.invitationId
|
|
3518
|
+
}, `Invitation revoked: ${options.invitationId}`);
|
|
3519
|
+
});
|
|
3520
|
+
invitations.command("received").description("List invitations received by the current user").action(async () => {
|
|
3521
|
+
const result = (await (await getUser()).listTeamInvitations()).map((invitation) => ({
|
|
3522
|
+
id: invitation.id,
|
|
3523
|
+
teamId: invitation.teamId,
|
|
3524
|
+
teamDisplayName: invitation.teamDisplayName,
|
|
3525
|
+
email: invitation.recipientEmail,
|
|
3526
|
+
expiresAt: invitation.expiresAt.toISOString()
|
|
3527
|
+
}));
|
|
3528
|
+
printResult(program, result, result.length === 0 ? "No received invitations found." : result.map((invitation) => `${invitation.id}\t${invitation.teamId}\t${invitation.teamDisplayName}\t${invitation.expiresAt}`).join("\n"));
|
|
3529
|
+
});
|
|
3530
|
+
invitations.command("accept").description("Accept an invitation received by the current user").requiredOption("--invitation-id <id>", "Invitation ID").action(async (options) => {
|
|
3531
|
+
const invitation = (await (await getUser()).listTeamInvitations()).find((candidate) => candidate.id === options.invitationId);
|
|
3532
|
+
if (invitation == null) throw new CliError(`Received invitation '${options.invitationId}' was not found.`);
|
|
3533
|
+
await invitation.accept();
|
|
3534
|
+
printResult(program, {
|
|
3535
|
+
invitationId: options.invitationId,
|
|
3536
|
+
teamId: invitation.teamId
|
|
3537
|
+
}, `Invitation accepted for team: ${invitation.teamId}`);
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
function registerTeamCommand(program) {
|
|
3541
|
+
const team = program.command("team").description("Manage teams");
|
|
3542
|
+
team.command("list").description("List your teams").action(async () => {
|
|
3543
|
+
printTeams(program, await (await getUser()).listTeams());
|
|
3544
|
+
});
|
|
3545
|
+
team.command("create").description("Create a team").option("--display-name <name>", "Team display name").action(async (options) => {
|
|
3546
|
+
let displayName = options.displayName?.trim();
|
|
3547
|
+
if (!displayName) {
|
|
3548
|
+
if (isNonInteractiveEnv()) throw new CliError("--display-name is required in non-interactive environments (CI).");
|
|
3549
|
+
displayName = (await input({
|
|
3550
|
+
message: "Team display name:",
|
|
3551
|
+
validate: (value) => value.trim().length > 0 || "Display name cannot be empty."
|
|
3552
|
+
})).trim();
|
|
3553
|
+
}
|
|
3554
|
+
printTeam(program, await (await getUser()).createTeam({ displayName }), "Team created");
|
|
3555
|
+
});
|
|
3556
|
+
registerTeamMembersCommand(team, program);
|
|
3557
|
+
team.command("invite").description("Invite a user to a team").option("--team-id <id>", "Team ID").option("--email <email>", "Email address to invite").action(async (options) => {
|
|
3558
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3559
|
+
let email = options.email?.trim();
|
|
3560
|
+
if (!email) {
|
|
3561
|
+
if (isNonInteractiveEnv()) throw new CliError("--email is required in non-interactive environments (CI).");
|
|
3562
|
+
email = (await input({
|
|
3563
|
+
message: "Email address:",
|
|
3564
|
+
validate: (value) => value.trim().length > 0 || "Email address cannot be empty."
|
|
3565
|
+
})).trim();
|
|
3566
|
+
}
|
|
3567
|
+
await withTeamPermissionError("invite this user", () => selectedTeam.inviteUser({ email }));
|
|
3568
|
+
printResult(program, {
|
|
3569
|
+
teamId: selectedTeam.id,
|
|
3570
|
+
email
|
|
3571
|
+
}, `Invitation sent to ${email}.`);
|
|
3572
|
+
});
|
|
3573
|
+
registerTeamInvitationsCommand(team, program);
|
|
3574
|
+
team.command("leave").description("Leave a team").option("--team-id <id>", "Team ID").option("-y, --yes", "Skip the confirmation prompt").action(async (options) => {
|
|
3575
|
+
const user = await getUser();
|
|
3576
|
+
const selectedTeam = await getTeam(user, options);
|
|
3577
|
+
await confirmDestructive(`leave team ${selectedTeam.id}`, options);
|
|
3578
|
+
await user.leaveTeam(selectedTeam);
|
|
3579
|
+
printResult(program, { teamId: selectedTeam.id }, `Left team: ${selectedTeam.id}`);
|
|
3580
|
+
});
|
|
3581
|
+
team.command("update").description("Update a team").option("--team-id <id>", "Team ID").option("--display-name <name>", "New team display name").action(async (options) => {
|
|
3582
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3583
|
+
let displayName = options.displayName?.trim();
|
|
3584
|
+
if (!displayName) {
|
|
3585
|
+
if (isNonInteractiveEnv()) throw new CliError("--display-name is required in non-interactive environments (CI).");
|
|
3586
|
+
displayName = (await input({
|
|
3587
|
+
message: "New team display name:",
|
|
3588
|
+
default: selectedTeam.displayName,
|
|
3589
|
+
validate: (value) => value.trim().length > 0 || "Display name cannot be empty."
|
|
3590
|
+
})).trim();
|
|
3591
|
+
}
|
|
3592
|
+
await withTeamPermissionError("update this team", () => selectedTeam.update({ displayName }));
|
|
3593
|
+
printResult(program, {
|
|
3594
|
+
id: selectedTeam.id,
|
|
3595
|
+
displayName
|
|
3596
|
+
}, `Team updated: ${selectedTeam.id} (${displayName})`);
|
|
3597
|
+
});
|
|
3598
|
+
team.command("delete").description("Delete a team").option("--team-id <id>", "Team ID").option("-y, --yes", "Skip the confirmation prompt").action(async (options) => {
|
|
3599
|
+
const selectedTeam = await getTeam(await getUser(), options);
|
|
3600
|
+
await confirmDestructive(`delete team ${selectedTeam.id}`, options);
|
|
3601
|
+
await withTeamPermissionError("delete this team", () => selectedTeam.delete());
|
|
3602
|
+
printResult(program, { teamId: selectedTeam.id }, `Team deleted: ${selectedTeam.id}`);
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
//#endregion
|
|
3326
3606
|
//#region src/index.ts
|
|
3327
3607
|
initSentry();
|
|
3328
3608
|
const program = new Command();
|
|
@@ -3336,6 +3616,7 @@ registerInitCommand(program);
|
|
|
3336
3616
|
registerProjectCommand(program);
|
|
3337
3617
|
registerDevCommand(program);
|
|
3338
3618
|
registerWhoamiCommand(program);
|
|
3619
|
+
registerTeamCommand(program);
|
|
3339
3620
|
registerFixCommand(program);
|
|
3340
3621
|
registerDoctorCommand(program);
|
|
3341
3622
|
async function main() {
|