@tokenoftrust/cli 3.10.3 → 3.10.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "3.10.3",
3
+ "version": "3.10.4",
4
4
  "description": "Token of Trust developer CLI — clone a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Token of Trust",
@@ -913,7 +913,20 @@ export function rendererNativeBindingGaps(runnerDir, { platform = process.platfo
913
913
  * `platform`/`arch` are injectable so this is testable off the host's real arch.
914
914
  */
915
915
  export function rendererCacheHealthy(runnerDir, opts = {}) {
916
- return rendererNativeBindingGaps(runnerDir, opts).length === 0;
916
+ return rendererNativeBindingGaps(runnerDir, opts).length === 0 && rendererDepsResolve(runnerDir);
917
+ }
918
+
919
+ /**
920
+ * Does the renderer's own dependency tree resolve from where it sits? `existsSync`
921
+ * follows links, so a `node_modules` entry whose target is gone (a junction left
922
+ * pointing into a renamed staging directory) reads as absent. A tree with no
923
+ * `apps/storefront/package.json` has nothing to check and passes.
924
+ * @param {string} runnerDir
925
+ */
926
+ export function rendererDepsResolve(runnerDir) {
927
+ const app = join(runnerDir, "apps", "storefront");
928
+ if (!existsSync(join(app, "package.json"))) return true;
929
+ return existsSync(join(app, "node_modules", "astro", "package.json"));
917
930
  }
918
931
 
919
932
  /**
@@ -1266,7 +1279,7 @@ export async function installRunnerTarball(
1266
1279
  // Bounded to one retry so a genuinely-broken source still fails loudly.
1267
1280
  for (let attempt = 1; ; attempt++) {
1268
1281
  const archivePath = /** @type {string} */ (isUrl ? join(tmpdir(), `tot-renderer-${process.pid}-${attempt}.tar.gz`) : localSource);
1269
- const stagingDir = `${runnerDir}.staging-${process.pid}`;
1282
+ const stagingDir = installsInPlace() ? runnerDir : `${runnerDir}.staging-${process.pid}`;
1270
1283
  try {
1271
1284
  if (isUrl) {
1272
1285
  // The fetch itself is otherwise silent (no per-byte output) and can run
@@ -1339,8 +1352,10 @@ export async function installRunnerTarball(
1339
1352
  // Atomic-ish: only rename into the final, discoverable path once install
1340
1353
  // succeeded, so a crashed/interrupted run never leaves a half-built cache
1341
1354
  // entry that a later `tot dev` would treat as ready.
1342
- rmSync(runnerDir, { recursive: true, force: true });
1343
- renameSync(stagingDir, runnerDir);
1355
+ if (stagingDir !== runnerDir) {
1356
+ rmSync(runnerDir, { recursive: true, force: true });
1357
+ renameSync(stagingDir, runnerDir);
1358
+ }
1344
1359
  // Fence a fresh install against npm/cli#4828: if the installer left the wrong
1345
1360
  // arch's native bindings (or none) for this host, DON'T stamp the completion
1346
1361
  // marker — an unmarked tree is never reused, so the next run reinstalls cleanly
@@ -1368,6 +1383,19 @@ export async function installRunnerTarball(
1368
1383
  /** @type {any} */ (err).permanent = true;
1369
1384
  throw err;
1370
1385
  }
1386
+ if (!rendererDepsResolve(runnerDir)) {
1387
+ await emitObstacle("install-failed");
1388
+ const err = new CliError(
1389
+ `the store preview engine installed but its dependencies don't resolve from where it was installed` +
1390
+ `\n details: ${installLog}`,
1391
+ {
1392
+ next: `re-run \`tot start --reset-cache\`; if it repeats, file feedback naming preview engine ${version} and your OS`,
1393
+ exitCode: 2,
1394
+ },
1395
+ );
1396
+ /** @type {any} */ (err).permanent = true;
1397
+ throw err;
1398
+ }
1371
1399
  writeCacheMarker(runnerDir, { version, integrity: contentId });
1372
1400
  return runnerDir;
1373
1401
  } catch (e) {
@@ -1547,9 +1575,9 @@ function extractTarball(archivePath, destDir, { strip = 0 } = {}) {
1547
1575
  * missing or disabled (a locked-down host), pnpm install below falls through to
1548
1576
  * the pnpm-missing obstacle instead of failing here.
1549
1577
  * @param {string} runnerDir
1550
- * @param {{ logPath?: string, spawnFn?: typeof spawnSync }} [opts]
1578
+ * @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any }} [opts]
1551
1579
  */
1552
- export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnSync } = {}) {
1580
+ export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnLauncherSync } = {}) {
1553
1581
  const pkgPath = join(runnerDir, "package.json");
1554
1582
  if (!existsSync(pkgPath)) return;
1555
1583
  let pm;
@@ -1567,7 +1595,7 @@ export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnSync } =
1567
1595
  // WITHOUT the global shim (see runPnpmInstall's fallback). Capture both to the
1568
1596
  // log — a silent corepack failure was why the install error carried no cause.
1569
1597
  for (const args of [["enable"], ["prepare", pm, "--activate"]]) {
1570
- const r = spawnFn("corepack", args, { stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"], shell: launcherNeedsShell() });
1598
+ const r = spawnFn("corepack", args, { stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"] });
1571
1599
  if (fd !== null && (r.error || r.status !== 0)) {
1572
1600
  const rerr = /** @type {any} */ (r.error);
1573
1601
  writeSync(fd, `[tot] corepack ${args.join(" ")} → ${rerr?.code || rerr?.message || `exit ${r.status}`}\n`);
@@ -1598,22 +1626,68 @@ export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnSync } =
1598
1626
  * @param {string} cmd @param {string[]} args @param {object} opts
1599
1627
  * @returns {Promise<{ status: number|null, error: Error|null, signal?: string|null }>}
1600
1628
  */
1629
+ /**
1630
+ * Windows installs the renderer where it will live. pnpm there writes `node_modules`
1631
+ * junctions with ABSOLUTE targets into the directory it installs in, so renaming a
1632
+ * staging directory into place leaves every one dangling. The completion marker is still
1633
+ * written last and an unmarked tree is never reused, so a half install cannot be served.
1634
+ * @param {string} [platform]
1635
+ */
1636
+ export function installsInPlace(platform = process.platform) {
1637
+ return platform === "win32";
1638
+ }
1639
+
1601
1640
  /**
1602
1641
  * On Windows `pnpm`, `corepack` and `npm` are `.cmd` shims, which Node refuses to
1603
- * spawn without a shell (and a bare name ENOENTs). Their args here are fixed
1604
- * literals, so the shell sees no user input.
1642
+ * spawn without a shell.
1643
+ * @param {string} [platform]
1605
1644
  */
1606
1645
  export function launcherNeedsShell(platform = process.platform) {
1607
1646
  return platform === "win32";
1608
1647
  }
1609
1648
 
1649
+ const SAFE_LAUNCHER_ARG = /^[\w@.:+=/-]+$/;
1650
+
1651
+ /**
1652
+ * `[file, args, opts]` to spawn a package-manager launcher. On Windows that is one
1653
+ * command string run through a shell (an args array alongside `shell: true` trips
1654
+ * Node's DEP0190 warning). The shell sees every argument, so each must match a safe
1655
+ * alphabet; anything else throws instead of reaching it.
1656
+ * @param {string} cmd @param {string[]} args @param {object} [opts] @param {string} [platform]
1657
+ * @returns {[string, string[], object]}
1658
+ */
1659
+ export function launcherSpawn(cmd, args, opts = {}, platform = process.platform) {
1660
+ if (!launcherNeedsShell(platform)) return [cmd, args, opts];
1661
+ for (const a of args) {
1662
+ if (!SAFE_LAUNCHER_ARG.test(a)) throw new Error(`refusing to pass an unsafe argument to ${cmd} through a shell: ${a}`);
1663
+ }
1664
+ return [[cmd, ...args].join(" "), [], { ...opts, shell: true }];
1665
+ }
1666
+
1667
+ /**
1668
+ * Is the launcher on PATH? POSIX answers with ENOENT at spawn, so this is always true
1669
+ * there. Through cmd.exe a missing launcher is an ordinary non-zero exit that cannot be
1670
+ * told from a failed install, so Windows asks `where` (a real executable, no shell).
1671
+ * @param {string} cmd @param {string} [platform]
1672
+ */
1673
+ export function launcherAvailable(cmd, platform = process.platform) {
1674
+ if (!launcherNeedsShell(platform)) return true;
1675
+ return spawnSync("where", [cmd], { stdio: "ignore" }).status === 0;
1676
+ }
1677
+
1678
+ /** @param {string} cmd @param {string[]} args @param {object} [opts] */
1679
+ function spawnLauncherSync(cmd, args, opts = {}) {
1680
+ const [file, fileArgs, fileOpts] = launcherSpawn(cmd, args, opts);
1681
+ return spawnSync(file, fileArgs, fileOpts);
1682
+ }
1683
+
1610
1684
  function spawnAsyncResult(cmd, args, opts = {}) {
1611
1685
  return new Promise((resolvePromise) => {
1612
1686
  let settled = false;
1613
1687
  const settle = (v) => { if (!settled) { settled = true; resolvePromise(v); } };
1614
1688
  let child;
1615
1689
  try {
1616
- child = spawn(cmd, args, opts);
1690
+ child = spawn(...launcherSpawn(cmd, args, opts));
1617
1691
  } catch (error) {
1618
1692
  settle({ status: null, error });
1619
1693
  return;
@@ -1625,10 +1699,10 @@ function spawnAsyncResult(cmd, args, opts = {}) {
1625
1699
 
1626
1700
  /**
1627
1701
  * @param {string} runnerDir
1628
- * @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any }} [opts]
1702
+ * @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any, available?: (cmd: string) => boolean }} [opts]
1629
1703
  * @returns {Promise<string>} the launcher that completed the install ("pnpm" | "corepack" | "npm").
1630
1704
  */
1631
- export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult } = {}) {
1705
+ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult, available = launcherAvailable } = {}) {
1632
1706
  const fd = logPath ? openSync(logPath, "a") : null;
1633
1707
  const installArgs = ["install", "--config.dangerouslyAllowAllBuilds=true"];
1634
1708
  // pnpm FIRST: the renderer tree is a bundle of native optional deps (rolldown,
@@ -1653,6 +1727,10 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
1653
1727
  ];
1654
1728
  try {
1655
1729
  for (const { cmd, args } of attempts) {
1730
+ if (!available(cmd)) {
1731
+ if (fd !== null) writeSync(fd, `[tot] ${cmd} not found on PATH — trying the next launcher\n`);
1732
+ continue;
1733
+ }
1656
1734
  // Awaited: the default spawnFn (spawnAsyncResult) runs the launcher
1657
1735
  // NON-BLOCKING so the caller's "installing…" spinner keeps ticking during
1658
1736
  // the (long, one-time) npm install instead of freezing at "(0s)" — a
@@ -1663,15 +1741,20 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
1663
1741
  cwd: runnerDir,
1664
1742
  // Send both streams to the log fd (or swallow them) — never inherit.
1665
1743
  stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"],
1666
- shell: launcherNeedsShell(),
1667
1744
  });
1668
1745
  if (r.status === 0) return cmd; // installed — the launcher that did it
1669
- // Through cmd.exe a missing launcher is exit 9009 ("not recognized"), not ENOENT.
1670
- if (/** @type {any} */ (r.error)?.code === "ENOENT" || (launcherNeedsShell() && r.status === 9009)) {
1746
+ if (/** @type {any} */ (r.error)?.code === "ENOENT") {
1671
1747
  // This launcher isn't on the machine — record it and try the next one.
1672
1748
  if (fd !== null) writeSync(fd, `[tot] ${cmd} not found (ENOENT) — trying the next launcher\n`);
1673
1749
  continue;
1674
1750
  }
1751
+ // corepack is the one launcher that can run and still fail for a reason that has
1752
+ // nothing to do with the install (no write access to its shim directory, no
1753
+ // network to fetch pnpm); npm below is the escape hatch that always exists.
1754
+ if (cmd === "corepack") {
1755
+ if (fd !== null) writeSync(fd, `[tot] corepack exited ${r.status} — trying npm\n`);
1756
+ continue;
1757
+ }
1675
1758
  // The launcher ran; the install itself failed. That's the actionable error.
1676
1759
  // installRunnerTarball retries transient failures once from a clean slate;
1677
1760
  // deterministic broken-release 404s are marked permanent below and skip it.