@tokenoftrust/cli 2.2.9 → 2.2.10

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/commands/dev.mjs +110 -38
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "2.2.9",
3
+ "version": "2.2.10",
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",
@@ -760,65 +760,127 @@ export function pinnedPublicCacheDir(cacheRoot, version) {
760
760
  }
761
761
 
762
762
  /**
763
- * List the installed package dirs that could carry a platform-native binary,
764
- * normalised so a scoped package reads as `@scope+name` (mirroring pnpm's virtual
765
- * store naming) regardless of the on-disk layout. Prefers the pnpm virtual store
766
- * (`node_modules/.pnpm/*`); falls back to a shallow `node_modules` scan (one level
767
- * into `@scope/`) for an npm/flat install. Best-effort — returns [] on any error.
763
+ * List the installed packages that could carry a platform-native binary, as
764
+ * `{ key, pkgJson }`: `key` is normalised so a scoped package reads as
765
+ * `@scope+name` (pnpm's virtual-store naming) regardless of the on-disk layout,
766
+ * and `pkgJson` is where that package's own manifest lives. Prefers the pnpm
767
+ * virtual store (`node_modules/.pnpm/<name>@<ver>/node_modules/<name>`); falls back
768
+ * to a shallow `node_modules` scan (one level into `@scope/`) for an npm/flat
769
+ * install. Best-effort — returns [] on any error.
768
770
  */
769
771
  function collectNativePackageDirs(runnerDir) {
770
- const pnpmDir = join(runnerDir, "node_modules", ".pnpm");
771
- if (existsSync(pnpmDir)) return readdirSync(pnpmDir);
772
772
  const nm = join(runnerDir, "node_modules");
773
+ const pnpmDir = join(nm, ".pnpm");
774
+ if (existsSync(pnpmDir)) {
775
+ return readdirSync(pnpmDir).map((entry) => {
776
+ const m = /^(@[^+]+)\+([^@]+)@/.exec(entry) || /^([^@]+)@/.exec(entry);
777
+ const pkgName = !m ? null : m[2] !== undefined ? `${m[1]}/${m[2]}` : m[1];
778
+ return {
779
+ key: entry,
780
+ pkgJson: pkgName ? join(pnpmDir, entry, "node_modules", pkgName, "package.json") : null,
781
+ };
782
+ });
783
+ }
773
784
  if (!existsSync(nm)) return [];
774
- const names = [];
785
+ const out = [];
775
786
  for (const e of readdirSync(nm, { withFileTypes: true })) {
776
787
  if (e.name.startsWith("@") && e.isDirectory()) {
777
- for (const s of readdirSync(join(nm, e.name))) names.push(`${e.name}+${s}`);
788
+ for (const s of readdirSync(join(nm, e.name))) {
789
+ out.push({ key: `${e.name}+${s}`, pkgJson: join(nm, e.name, s, "package.json") });
790
+ }
778
791
  } else {
779
- names.push(e.name);
792
+ out.push({ key: e.name, pkgJson: join(nm, e.name, "package.json") });
780
793
  }
781
794
  }
782
- return names;
795
+ return out;
783
796
  }
784
797
 
785
798
  /**
786
- * True unless the installed renderer is POISONED for this host's CPU arch — the
787
- * "the dev server didn't come up" root cause. Platform-native binding packages
788
- * are named `<family>-<os>-<cpu>[-<abi>]` (e.g. `@esbuild/darwin-arm64`,
789
- * `lightningcss-linux-x64-gnu`, `@rollup/rollup-win32-x64-msvc`). npm's optional-
790
- * deps bug (npm/cli#4828) can materialise a DIFFERENT arch's variant than the host
791
- * needs (e.g. darwin-x64 on an arm64 Mac) — pnpm gets it right. We flag the tree
792
- * as poisoned exactly when a family ships a binding for our OS but NOT our OS+CPU,
793
- * which is the precise shape of the `Cannot find native binding` boot crash. A
794
- * family with no variant for our OS at all is a cross-platform optional dep that's
795
- * correctly absent, so it never trips the check. Never throws; on any probe error
796
- * it returns true (trust the cache) so a health probe can't itself break `tot dev`.
799
+ * The `<os>-<cpu>` targets a package's own manifest declares through its `os` and
800
+ * `cpu` fields — the authority npm and pnpm themselves install by. Null when the
801
+ * manifest is unreadable or declares no positive os AND cpu (negations such as
802
+ * `"!win32"` exclude rather than name a target).
803
+ */
804
+ function declaredNativeTargets(pkgJsonPath) {
805
+ if (!pkgJsonPath) return null;
806
+ try {
807
+ const j = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
808
+ const positive = (v) =>
809
+ (Array.isArray(v) ? v : typeof v === "string" ? [v] : []).filter(
810
+ (x) => typeof x === "string" && x && !x.startsWith("!"),
811
+ );
812
+ const os = positive(j.os);
813
+ const cpu = positive(j.cpu);
814
+ if (!os.length || !cpu.length) return null;
815
+ return os.flatMap((o) => cpu.map((c) => `${o}-${c}`));
816
+ } catch {
817
+ return null;
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Package-name spellings of an OS / CPU that differ from Node's `process.platform`
823
+ * / `process.arch`. `@cloudflare/workerd-linux-64` is linux-x64; reading the name
824
+ * token literally makes every x64 host look like it is missing workerd.
825
+ */
826
+ const NATIVE_OS_ALIASES = { windows: "win32" };
827
+ const NATIVE_CPU_ALIASES = { 64: "x64", amd64: "x64", x86_64: "x64", aarch64: "arm64", 32: "ia32", x86: "ia32", i386: "ia32" };
828
+
829
+ /**
830
+ * Every native-binding family that ships a variant for this host's OS but not for
831
+ * its OS+CPU — the exact shape of astro's `Cannot find native binding` boot crash.
832
+ * Platform-native packages are named `<family>-<os>-<cpu>[-<abi>]` (e.g.
833
+ * `@esbuild/darwin-arm64`, `lightningcss-linux-x64-gnu`, `@cloudflare/workerd-linux-64`);
834
+ * the name only GROUPS a package into its family. Which target a package is for
835
+ * comes from its manifest's `os`/`cpu` fields, falling back to the name tokens
836
+ * (aliases normalised) only when the manifest is unreadable. npm's optional-deps bug
837
+ * (npm/cli#4828) can materialise a DIFFERENT arch's variant than the host needs
838
+ * (e.g. darwin-x64 on an arm64 Mac). A family with no variant for our OS at all is
839
+ * a cross-platform optional dep that's correctly absent, so it never counts.
797
840
  *
798
- * `platform`/`arch` are injectable so this is testable off the host's real arch.
841
+ * @returns {{ family: string, want: string, found: string[] }[]} — `family` in
842
+ * npm spelling (`@scope/name`), `found` the targets installed for our OS.
843
+ * [] on any probe error, so a health probe can't itself break `tot dev`.
799
844
  */
800
- export function rendererCacheHealthy(runnerDir, { platform = process.platform, arch = process.arch } = {}) {
845
+ export function rendererNativeBindingGaps(runnerDir, { platform = process.platform, arch = process.arch } = {}) {
801
846
  try {
802
- const re = /^(.*?)[-+](darwin|linux|win32|freebsd|android|openharmony)-([a-z0-9]+)/;
847
+ const re = /^(.*?)[-+](darwin|linux|win32|windows|freebsd|android|openharmony)-([a-z0-9_]+)/;
803
848
  const families = new Map(); // family -> Set("<os>-<cpu>")
804
- for (const name of collectNativePackageDirs(runnerDir)) {
805
- const m = name.match(re);
849
+ for (const { key, pkgJson } of collectNativePackageDirs(runnerDir)) {
850
+ const m = key.match(re);
806
851
  if (!m) continue;
807
852
  const [, family, os, cpu] = m;
853
+ const targets = declaredNativeTargets(pkgJson) || [
854
+ `${NATIVE_OS_ALIASES[os] || os}-${NATIVE_CPU_ALIASES[cpu] || cpu}`,
855
+ ];
808
856
  if (!families.has(family)) families.set(family, new Set());
809
- families.get(family).add(`${os}-${cpu}`);
857
+ for (const t of targets) families.get(family).add(t);
810
858
  }
811
859
  const want = `${platform}-${arch}`;
812
- for (const variants of families.values()) {
813
- const hasOurOs = [...variants].some((v) => v.startsWith(`${platform}-`));
814
- if (hasOurOs && !variants.has(want)) return false; // wrong-arch binary present, ours missing
860
+ const gaps = [];
861
+ for (const [family, variants] of families) {
862
+ const ours = [...variants].filter((v) => v.startsWith(`${platform}-`));
863
+ if (ours.length && !variants.has(want)) {
864
+ gaps.push({ family: family.replace(/^(@[^+]+)\+/, "$1/"), want, found: ours.sort() });
865
+ }
815
866
  }
816
- return true;
867
+ return gaps;
817
868
  } catch {
818
- return true;
869
+ return [];
819
870
  }
820
871
  }
821
872
 
873
+ /**
874
+ * True unless the installed renderer is POISONED for this host's CPU arch — the
875
+ * "the dev server didn't come up" root cause (see rendererNativeBindingGaps).
876
+ * Never throws; on any probe error it returns true (trust the cache).
877
+ *
878
+ * `platform`/`arch` are injectable so this is testable off the host's real arch.
879
+ */
880
+ export function rendererCacheHealthy(runnerDir, opts = {}) {
881
+ return rendererNativeBindingGaps(runnerDir, opts).length === 0;
882
+ }
883
+
822
884
  /**
823
885
  * True unless the installed runner package is missing source the Astro config
824
886
  * chain needs to even boot — the "renderer resolves but `tot dev` can't load
@@ -1217,8 +1279,9 @@ export async function installRunnerTarball(
1217
1279
  const installSpin = startProgress("installing the store preview engine…", {
1218
1280
  stages: [{ afterMs: 20000, text: "still setting up the preview engine (first run only)…" }],
1219
1281
  });
1282
+ let installer;
1220
1283
  try {
1221
- await runPnpmInstall(stagingDir, { logPath: installLog });
1284
+ installer = await runPnpmInstall(stagingDir, { logPath: installLog });
1222
1285
  } finally {
1223
1286
  installSpin.stop();
1224
1287
  }
@@ -1234,12 +1297,20 @@ export async function installRunnerTarball(
1234
1297
  // rather than swallow it as "the dev server didn't come up". Permanent: the
1235
1298
  // same installer on the same host would just produce the same result, so the
1236
1299
  // auto-retry is skipped.
1237
- if (!rendererCacheHealthy(runnerDir)) {
1300
+ const gaps = rendererNativeBindingGaps(runnerDir);
1301
+ if (gaps.length) {
1238
1302
  await emitObstacle("renderer-native-bindings-missing");
1239
1303
  const err = new CliError(
1240
- `the store preview engine installed but is missing its native components for ${process.platform}-${process.arch}`,
1304
+ `the store preview engine installed but is missing its native components for ${process.platform}-${process.arch}` +
1305
+ ` (${gaps.map((g) => `${g.family}: only ${g.found.join(", ")} installed`).join("; ")})` +
1306
+ `\n details: ${installLog}`,
1241
1307
  {
1242
- next: "install pnpm (`npm i -g pnpm`, or `corepack enable`) and re-run `tot start` — pnpm installs the platform-native bits npm can skip (npm/cli#4828)",
1308
+ // Only npm (the last-resort launcher) has the optional-deps bug that
1309
+ // installing pnpm cures; a pnpm-installed gap is not the developer's to fix.
1310
+ next:
1311
+ installer === "npm"
1312
+ ? "install pnpm (`npm i -g pnpm`, or `corepack enable`) and re-run `tot start` — pnpm installs the platform-native bits npm can skip (npm/cli#4828)"
1313
+ : `re-run \`tot start --reset-cache\`; if it repeats, file feedback naming preview engine ${version} and the packages above — this is not something on your machine`,
1243
1314
  exitCode: 2,
1244
1315
  },
1245
1316
  );
@@ -1495,6 +1566,7 @@ function spawnAsyncResult(cmd, args, opts = {}) {
1495
1566
  /**
1496
1567
  * @param {string} runnerDir
1497
1568
  * @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any }} [opts]
1569
+ * @returns {Promise<string>} the launcher that completed the install ("pnpm" | "corepack" | "npm").
1498
1570
  */
1499
1571
  export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult } = {}) {
1500
1572
  const fd = logPath ? openSync(logPath, "a") : null;
@@ -1532,7 +1604,7 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
1532
1604
  // Send both streams to the log fd (or swallow them) — never inherit.
1533
1605
  stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"],
1534
1606
  });
1535
- if (r.status === 0) return; // installed
1607
+ if (r.status === 0) return cmd; // installed — the launcher that did it
1536
1608
  if (/** @type {any} */ (r.error)?.code === "ENOENT") {
1537
1609
  // This launcher isn't on the machine — record it and try the next one.
1538
1610
  if (fd !== null) writeSync(fd, `[tot] ${cmd} not found (ENOENT) — trying the next launcher\n`);