@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 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$1(value) {
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$1(parsed)) {
897
+ if (isRecord$2(parsed)) {
879
898
  const error = parsed.error;
880
899
  if (typeof error === "string") return error;
881
- if (isRecord$1(error) && typeof error.message === "string") return error.message;
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
- return await withProgress("Creating project", async () => {
1404
- const teams = await user.listTeams();
1405
- if (teams.length === 0) throw new CliError(`No teams found on your account. Create a team at ${opts.dashboardUrl ?? "https://app.hexclave.com"} first.`);
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 user = await withProgress("Loading account", async () => await getInternalUser(sessionAuth));
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 { dashboardUrl } = resolveLoginConfig();
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 }, { resolveLoginConfig, resolveSessionAuth }, { createProjectInteractively }] = await Promise.all([
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 user = await withProgress("Loading account", async () => await getInternalUser(auth));
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
- dashboardUrl
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 == null || typeof raw !== "object") return null;
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 = dashboardVersionDir(version);
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 latestCachedDashboardVersion() {
1880
- const root = dashboardCacheRoot();
1881
- if (!existsSync(root)) return void 0;
1882
- return pickLatestVersion(readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && isDashboardCached(entry.name)).map((entry) => entry.name));
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 tmpZip = join(cacheRoot, `.download-${manifest.version}-${suffix}.zip`);
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(manifest.url, {
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 ${manifest.url}.`);
1922
- await pipeline(Readable.fromWeb(response.body), createWriteStream(tmpZip));
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(tmpZip);
1925
- if (digest !== manifest.sha256) throw new CliError(`Dashboard ${manifest.version} failed its integrity check (expected ${manifest.sha256}, got ${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(tmpZip, { dir: tmpDir });
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), `${manifest.sha256}\n`);
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(tmpZip, { force: true });
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
- if (isDashboardCached(manifest.version)) return {
1966
- root: dashboardVersionDir(manifest.version),
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 = latestCachedDashboardVersion();
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 = latestCachedDashboardVersion();
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() {