@tokenoftrust/cli 1.4.0-rc.9 → 1.4.1

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.
@@ -427,15 +427,18 @@ export async function resolveRendererSource(args, { client } = {}) {
427
427
  * is keyed by version, upgrading the CLI busts the stale-runner cache automatically
428
428
  * (the 2026-07-14 "cached runner (prior tot dev)" staleness).
429
429
  *
430
- * EXACT-FIRST (the CLI requests its OWN version): with lockstep publishing the runner
431
- * is published at the SAME version as the CLI, so the exact match (step 2) is the normal
432
- * path. An explicit pin (`--renderer-version` / TOT_RUNNER_VERSION) always wins. When no
433
- * exact match exists we degrade ONLY DOWNWARD a runner at or below the CLI's major.minor
434
- * (never ahead) and NEVER to a floating dist-tag like `latest` (a channel can point at a
435
- * version this CLI doesn't expect; that floating-tag drift is the exact bug this avoids).
436
- * The caller emits a LOUD skew warning whenever the resolved version isn't the exact CLI
437
- * version, so a mismatch is visible, not silent. If nothing exact/minor/≤-ceiling is
438
- * published, we THROW a release gap to fix by publishing the aligned runner, not paper over.
430
+ * STABLE PATCHES FLOAT within the CLI's own major.minor: a published patch is a
431
+ * compatible maintenance release, and is precisely how a bad runner artifact is
432
+ * replaced (npm tarballs are immutable). Therefore stable CLI 1.4.0 automatically
433
+ * takes runner 1.4.1 instead of remaining pinned forever to a broken 1.4.0 tarball.
434
+ * Prerelease CLIs still prefer their exact prerelease runner because stable-only
435
+ * patch selection deliberately excludes prereleases. An explicit pin
436
+ * (`--renderer-version` / TOT_RUNNER_VERSION) always wins.
437
+ *
438
+ * We still never cross the CLI's major.minor ceiling automatically and never use a
439
+ * floating dist-tag like `latest`: a newer minor can require a newer CLI contract.
440
+ * If no stable same-minor or lower compatible release exists, we THROW rather than
441
+ * silently running a too-new runner.
439
442
  *
440
443
  * PIN-DIRECTION DECISION (ADR 0011 — don't relitigate inline): the runner version
441
444
  * should be DECLARED BY THE PRODUCT, not derived from the CLI's identity. The
@@ -443,11 +446,9 @@ export async function resolveRendererSource(args, { client } = {}) {
443
446
  * 1. explicit flag/env pin — developer intent, always wins
444
447
  * 2. declaredVersion — the STORE's own `.tot/config.json#runnerVersion`
445
448
  * (rust-toolchain.toml-style; the target state)
446
- * 3. exact CLI-version match TRANSITIONAL lockstep rung. Delete it (and the
447
- * lockstep publish regime) once tenant_checkout
448
- * stamps runnerVersion into every checkout
449
- * see ADR 0011 for the exit criteria.
450
- * 4. CLI-minor / ≤-ceiling — degraded-but-safe fallbacks (never a floating tag)
449
+ * 3. exact prerelease match keeps an RC paired with its runner RC
450
+ * 4. newest stable CLI-minor patch — maintenance releases float within x.y
451
+ * 5. ≤-ceiling — degraded-but-safe fallback (never a floating tag)
451
452
  *
452
453
  * Pure (no I/O) for testability.
453
454
  * @param {any} meta npm packument (`dist-tags` + `versions`)
@@ -486,17 +487,19 @@ export function pickRunnerVersion(meta, { cliVersion, explicitPin, declaredVersi
486
487
  return { version: declaredVersion, reason: "declared by the store checkout (runnerVersion)" };
487
488
  }
488
489
 
489
- // 2) EXACT CLI-version match the primary path for a lockstep release: the runner
490
- // is published at the SAME version as the CLI (incl. prereleases like 1.3.0-rc.0,
491
- // which the stable-only minor match below deliberately skips). This is what makes
492
- // `tot@1.3.0-rc.0` pull `runner@1.3.0-rc.0` instead of falling back to stale latest.
493
- // (Same version ⇒ same major.minor always within the ceiling.)
494
- if (versions.includes(cliVersion)) {
490
+ // 2) EXACT PRERELEASE match. Stable releases intentionally continue to the
491
+ // same-minor maintenance selection below, where a corrected immutable artifact
492
+ // (runner 1.4.1 for CLI 1.4.0) can supersede a broken exact-version tarball.
493
+ // RCs cannot safely float across prerelease builds, so they remain exact-paired.
494
+ if (cliVersion.includes("-") && versions.includes(cliVersion)) {
495
495
  return { version: cliVersion, reason: "exact CLI-version match" };
496
496
  }
497
497
 
498
- // 3) CLI-minor match: highest published <major>.<minor>.* (numeric patch order).
499
- // Constrained to the CLI's exact minor, so this is within the ceiling by construction.
498
+ // 3) CLI-minor maintenance match: highest published stable <major>.<minor>.*
499
+ // (numeric patch order), INCLUDING patches newer than the CLI's own patch.
500
+ // Patch releases are compatibility fixes; allowing them to float is what lets us
501
+ // replace a broken immutable runner without asking every developer to know a pin.
502
+ // Constrained to the CLI's exact minor, so it stays within the ceiling.
500
503
  const m = /^(\d+)\.(\d+)\./.exec(cliVersion || "");
501
504
  if (m) {
502
505
  const prefix = `${m[1]}.${m[2]}.`;
@@ -520,11 +523,9 @@ export function pickRunnerVersion(meta, { cliVersion, explicitPin, declaredVersi
520
523
  return { version, reason: `highest ≤ CLI major.minor ${cliMM ? `${cliMM.major}.${cliMM.minor}` : "?"}` };
521
524
  }
522
525
  // NO floating-tag last resort. We deliberately do NOT fall back to the `latest`
523
- // dist-tag: a channel can drift behind/ahead of what THIS CLI expects (the exact
524
- // failure mode that shipped stale CLIs see the copy-paste pin in the storefront
525
- // cockpit). If nothing exact/minor/≤-ceiling is published, that's a release gap to
526
- // fix by publishing the runner at the CLI's version — not something to paper over
527
- // with whatever `latest` happens to point at.
526
+ // dist-tag: a channel can drift behind/ahead of what THIS CLI expects. If nothing
527
+ // stable within the CLI minor or below its ceiling is published, that's a release
528
+ // gap to fix not something to paper over with whatever `latest` points at.
528
529
  throw new Error(
529
530
  `no runner version at or below the CLI's major.minor (${cliMM ? `${cliMM.major}.${cliMM.minor}` : cliVersion})`,
530
531
  );
@@ -536,7 +537,7 @@ export function pickRunnerVersion(meta, { cliVersion, explicitPin, declaredVersi
536
537
  * rust-toolchain.toml of the storefront: the PRODUCT (via tenant_checkout
537
538
  * stamping it server-side) owns which runtime the store runs; the CLI just
538
539
  * resolves it. Returns null when absent/malformed/not-semver — silence is
539
- * correct: an undeclared checkout falls back to the transitional lockstep rung.
540
+ * correct: an undeclared checkout falls back to the compatible CLI-minor rung.
540
541
  * Pure-ish (one file read) + exported for tests.
541
542
  * @param {string|null|undefined} workspaceDir
542
543
  * @returns {string|null}
@@ -605,19 +606,39 @@ export async function resolvePublicRendererSource(args, env = process.env, { dec
605
606
  `using ${version} (${reason}). The store's runnerVersion needs a published release.`,
606
607
  );
607
608
  }
608
- } else if (!explicitPin && version !== CLI_VERSION) {
609
- // No declaration (transitional lockstep regime ADR 0011): exact is the goal.
610
- // Resolving something ELSE means CLI/runner releases are skewed say so LOUDLY
611
- // instead of silently running a mismatched runner.
609
+ } else if (!explicitPin && version !== CLI_VERSION && !isCompatibleMaintenancePatch(CLI_VERSION, version)) {
610
+ // A stable same-minor maintenance patch is the normal self-healing path, not
611
+ // skew. Anything else still deserves a loud warning: it means resolution had
612
+ // to leave the CLI's compatibility line.
612
613
  console.warn(
613
614
  ` ⚠ runner ${version} — no exact @${CLI_VERSION} published (${reason}). ` +
614
615
  `CLI/runner versions are SKEWED; publish the runner at ${CLI_VERSION} to align ` +
615
616
  `(or pin with --renderer-version to silence).`,
616
617
  );
617
618
  }
618
- const tarball = meta?.versions?.[version]?.dist?.tarball;
619
- if (!tarball) throw new Error(`no published ${pkg}@${version} on npm`);
620
- return { kind: "public", version, url: tarball, strip: 1, cacheKey: `public-${version}` };
619
+ const dist = meta?.versions?.[version]?.dist;
620
+ if (!dist?.tarball) throw new Error(`no published ${pkg}@${version} on npm`);
621
+ // `integrity` is the artifact's CONTENT identity (npm dist.integrity, else the
622
+ // legacy shasum) — installRunnerTarball verifies the downloaded bytes against
623
+ // it and records it so a same-version corrected republish busts the cache.
624
+ return {
625
+ kind: "public",
626
+ version,
627
+ url: dist.tarball,
628
+ strip: 1,
629
+ cacheKey: `public-${version}`,
630
+ integrity: dist.integrity || dist.shasum || null,
631
+ };
632
+ }
633
+
634
+ /** Stable x.y patches share the runner/CLI contract; prereleases do not. */
635
+ function isCompatibleMaintenancePatch(cliVersion, runnerVersion) {
636
+ if (!/^\d+\.\d+\.\d+$/.test(String(cliVersion)) || !/^\d+\.\d+\.\d+$/.test(String(runnerVersion))) {
637
+ return false;
638
+ }
639
+ const cliMM = majorMinor(cliVersion);
640
+ const runnerMM = majorMinor(runnerVersion);
641
+ return !!cliMM && !!runnerMM && cliMM.major === runnerMM.major && cliMM.minor === runnerMM.minor;
621
642
  }
622
643
 
623
644
  /**
@@ -637,7 +658,17 @@ export async function resolveEntitledRendererSource(args, { client: providedClie
637
658
  if (!res?.url || !res?.version) {
638
659
  throw new Error(res?.error || "no renderer-artifact URL returned");
639
660
  }
640
- return { kind: "entitled", version: res.version, url: res.url, strip: 0, cacheKey: res.version };
661
+ // Opportunistic content identity: recorded/verified when the MCP declares one
662
+ // (integrity/sha256); a server that doesn't is simply unverified (null), never
663
+ // an error — the cache then busts on version changes only, as before.
664
+ return {
665
+ kind: "entitled",
666
+ version: res.version,
667
+ url: res.url,
668
+ strip: 0,
669
+ cacheKey: res.version,
670
+ integrity: res.integrity || res.sha256 || null,
671
+ };
641
672
  }
642
673
 
643
674
  /**
@@ -676,7 +707,12 @@ export async function ensureRendererArtifact(args, { client: providedClient } =
676
707
 
677
708
  try {
678
709
  const runnerDir = await installRunnerTarball(
679
- { source: credential.url, version: credential.version, isUrl: true },
710
+ {
711
+ source: credential.url,
712
+ version: credential.version,
713
+ isUrl: true,
714
+ integrity: credential.integrity || credential.sha256 || null,
715
+ },
680
716
  { log: (m) => console.error(m) },
681
717
  );
682
718
  setRunnerVersion(credential.version); // telemetry: stamp the entitled runner version, like ensureSampleRenderer
@@ -700,6 +736,66 @@ export function pinnedPublicCacheDir(cacheRoot, version) {
700
736
  return existsSync(join(dir, ".tot-cache-complete")) ? dir : null;
701
737
  }
702
738
 
739
+ /**
740
+ * List the installed package dirs that could carry a platform-native binary,
741
+ * normalised so a scoped package reads as `@scope+name` (mirroring pnpm's virtual
742
+ * store naming) regardless of the on-disk layout. Prefers the pnpm virtual store
743
+ * (`node_modules/.pnpm/*`); falls back to a shallow `node_modules` scan (one level
744
+ * into `@scope/`) for an npm/flat install. Best-effort — returns [] on any error.
745
+ */
746
+ function collectNativePackageDirs(runnerDir) {
747
+ const pnpmDir = join(runnerDir, "node_modules", ".pnpm");
748
+ if (existsSync(pnpmDir)) return readdirSync(pnpmDir);
749
+ const nm = join(runnerDir, "node_modules");
750
+ if (!existsSync(nm)) return [];
751
+ const names = [];
752
+ for (const e of readdirSync(nm, { withFileTypes: true })) {
753
+ if (e.name.startsWith("@") && e.isDirectory()) {
754
+ for (const s of readdirSync(join(nm, e.name))) names.push(`${e.name}+${s}`);
755
+ } else {
756
+ names.push(e.name);
757
+ }
758
+ }
759
+ return names;
760
+ }
761
+
762
+ /**
763
+ * True unless the installed renderer is POISONED for this host's CPU arch — the
764
+ * "the dev server didn't come up" root cause. Platform-native binding packages
765
+ * are named `<family>-<os>-<cpu>[-<abi>]` (e.g. `@esbuild/darwin-arm64`,
766
+ * `lightningcss-linux-x64-gnu`, `@rollup/rollup-win32-x64-msvc`). npm's optional-
767
+ * deps bug (npm/cli#4828) can materialise a DIFFERENT arch's variant than the host
768
+ * needs (e.g. darwin-x64 on an arm64 Mac) — pnpm gets it right. We flag the tree
769
+ * as poisoned exactly when a family ships a binding for our OS but NOT our OS+CPU,
770
+ * which is the precise shape of the `Cannot find native binding` boot crash. A
771
+ * family with no variant for our OS at all is a cross-platform optional dep that's
772
+ * correctly absent, so it never trips the check. Never throws; on any probe error
773
+ * it returns true (trust the cache) so a health probe can't itself break `tot dev`.
774
+ *
775
+ * `platform`/`arch` are injectable so this is testable off the host's real arch.
776
+ */
777
+ export function rendererCacheHealthy(runnerDir, { platform = process.platform, arch = process.arch } = {}) {
778
+ try {
779
+ const re = /^(.*?)[-+](darwin|linux|win32|freebsd|android|openharmony)-([a-z0-9]+)/;
780
+ const families = new Map(); // family -> Set("<os>-<cpu>")
781
+ for (const name of collectNativePackageDirs(runnerDir)) {
782
+ const m = name.match(re);
783
+ if (!m) continue;
784
+ const [, family, os, cpu] = m;
785
+ if (!families.has(family)) families.set(family, new Set());
786
+ families.get(family).add(`${os}-${cpu}`);
787
+ }
788
+ const want = `${platform}-${arch}`;
789
+ for (const variants of families.values()) {
790
+ const hasOurOs = [...variants].some((v) => v.startsWith(`${platform}-`));
791
+ if (hasOurOs && !variants.has(want)) return false; // wrong-arch binary present, ours missing
792
+ }
793
+ return true;
794
+ } catch {
795
+ return true;
796
+ }
797
+ }
798
+
703
799
  /**
704
800
  * Delete every OTHER public-runner cache dir (`public-*` except `keepVersion`)
705
801
  * once we've resolved the version this CLI pins to — so a pre-alignment dir
@@ -791,40 +887,66 @@ export async function ensureSampleRenderer(args, ctx, { env = process.env, cache
791
887
  console.error(`~ renderer: ${src.why}`);
792
888
  return installRunnerTarball(
793
889
  { source: src.source, version: sourceVersionKey(src.source), isUrl: src.isUrl },
794
- { log: (m) => console.error(m) },
890
+ { log: (m) => console.error(m), cacheRoot },
795
891
  );
796
892
  }
797
893
 
798
894
  // kind === "none" — no override, not in the monorepo: fetch the PUBLIC runner
799
- // from npm. Version = what the STORE declares (ADR 0011), else pinned to this
800
- // CLI's version (transitional lockstep). No MCP, no entitlement, no login.
895
+ // from npm. Version = what the STORE declares (ADR 0011), else the newest stable
896
+ // maintenance patch in this CLI's major.minor. No MCP, no entitlement, no login.
801
897
  const explicitPin = args.rendererVersion || env.TOT_RUNNER_VERSION || null;
802
898
  const declared = declaredRunnerVersion(args.workspace || ctx?.workspacePath);
803
899
  const wantVersion = declared || CLI_VERSION;
804
900
 
805
- // Fully-offline fast path: when the WANTED version (declared, else lockstep) is
806
- // already cached, reuse it without touching npm — honouring "don't hit npm when
807
- // the RIGHT version is already cached" without ever reusing a version the
808
- // resolution wouldn't choose. Safe because `public-<version>` only exists if a
809
- // prior run fetched exactly that version. Skipped when an explicit pin is set
810
- // (that must go through resolution).
811
- if (!explicitPin) {
901
+ // Offline-safe fast path for a product-declared exact version: when that version
902
+ // is already cached, reuse it — honouring "don't hit npm when the RIGHT version
903
+ // is already cached" without reusing a version the declaration did not choose.
904
+ // Safe because `public-<version>` only exists if a prior run fetched exactly
905
+ // that version. Skipped when an explicit pin is set (that must go through
906
+ // resolution). One refinement over fully-offline: a QUICK, soft-fail registry
907
+ // probe (publishedRunnerIntegrity) revalidates the cached CONTENT identity when
908
+ // npm is reachable, so a corrected republish under the same version string is
909
+ // picked up automatically; offline/slow/unanswerable → trust the cache exactly
910
+ // as before (the probe can never block or fail the run).
911
+ // A store declaration is an exact product pin, so it is safe to reuse directly.
912
+ // Without a declaration, do not short-circuit on runner@CLI_VERSION: registry
913
+ // resolution may have a newer compatible patch that repairs an immutable bad
914
+ // artifact. The normal resolution path below still reuses its installed cache.
915
+ if (!explicitPin && declared) {
812
916
  const exact = pinnedPublicCacheDir(cacheRoot, wantVersion);
813
917
  if (exact) {
814
918
  // Trust-but-verify: a `public-<version>` dir SHOULD be a current runner,
815
919
  // but if it can't report its own version it predates the --version surface
816
920
  // (a corrupt/half-migrated cache) — force a fresh fetch rather than run a
817
- // runner we can't identify. When it DOES answer, reuse it (fully offline-safe).
818
- if (probeRunnerVersion(exact)) {
921
+ // runner we can't identify. It must ALSO carry native bindings for THIS
922
+ // host's arch (rendererCacheHealthy) — a cache poisoned with the wrong-arch
923
+ // bindings (npm/cli#4828) would otherwise be reused forever and crash astro
924
+ // at boot with the swallowed "dev server didn't come up". When both hold,
925
+ // reuse it (offline-safe).
926
+ if (probeRunnerVersion(exact) && rendererCacheHealthy(exact)) {
927
+ const published = await publishedRunnerIntegrity(env, wantVersion);
928
+ const recorded = readCacheMarker(exact)?.integrity || null;
929
+ if (!published || !recorded || published === recorded) {
930
+ console.error(
931
+ `~ renderer: cached public runner ${wantVersion} (${declared ? "declared by this store" : "matches this CLI"})`,
932
+ );
933
+ setRunnerVersion(wantVersion);
934
+ prunePublicRunnerCache(cacheRoot, wantVersion);
935
+ return exact;
936
+ }
937
+ // Same version string, different published contents — a corrected
938
+ // republish. Fall through to resolution + a fresh install (which also
939
+ // verifies the new bytes against the new integrity).
940
+ console.error(`~ renderer: ${wantVersion} was republished with different contents — refetching the corrected artifact`);
941
+ prunePublicRunnerCache(cacheRoot, null); // drop ALL public-* — none matches what npm now publishes
942
+ } else {
819
943
  console.error(
820
- `~ renderer: cached public runner ${wantVersion} (${declared ? "declared by this store" : "matches this CLI"})`,
944
+ rendererCacheHealthy(exact)
945
+ ? `~ renderer: cached runner at ${exact} can't report a version — refetching (forced upgrade)`
946
+ : `~ renderer: cached runner at ${exact} is missing native bindings for ${process.platform}-${process.arch} — refetching`,
821
947
  );
822
- setRunnerVersion(wantVersion);
823
- prunePublicRunnerCache(cacheRoot, wantVersion);
824
- return exact;
948
+ prunePublicRunnerCache(cacheRoot, null); // drop ALL public-* — none is trustworthy
825
949
  }
826
- console.error(`~ renderer: cached runner at ${exact} can't report a version — refetching (forced upgrade)`);
827
- prunePublicRunnerCache(cacheRoot, null); // drop ALL public-* — none is trustworthy
828
950
  }
829
951
  }
830
952
 
@@ -855,8 +977,8 @@ export async function ensureSampleRenderer(args, ctx, { env = process.env, cache
855
977
  // The store preview engine (public npm ${PUBLIC_RUNNER_PACKAGE}@${pub.version}) — kept
856
978
  // out of the user's way; the setup spinner below is the visible progress.
857
979
  const dir = await installRunnerTarball(
858
- { source: pub.url, version: pub.cacheKey, isUrl: true, strip: pub.strip },
859
- { log: (m) => console.error(m) },
980
+ { source: pub.url, version: pub.cacheKey, isUrl: true, strip: pub.strip, integrity: pub.integrity },
981
+ { log: (m) => console.error(m), cacheRoot },
860
982
  );
861
983
  setRunnerVersion(pub.version); // telemetry: the runner version running this session
862
984
  // The pinned version is now installed under public-<version> — drop any other
@@ -881,69 +1003,269 @@ function sourceVersionKey(source) {
881
1003
  * authenticated (ensureRendererArtifact) and zero-login (ensureSampleRenderer)
882
1004
  * paths so they cache identically.
883
1005
  *
884
- * @param {{ source: string, version: string, isUrl?: boolean }} spec
885
- * @param {{ log?: (m: string) => void }} [opts]
1006
+ * Cache-poisoning invariants (the 2026-08-18 first-run hardening):
1007
+ * promote-on-success only the install runs in a per-attempt staging dir and
1008
+ * is renamed into the canonical slot ONLY after it fully succeeds, so a failed
1009
+ * install can never become the cached artifact a later run resumes from.
1010
+ * • the completion marker is a manifest carrying the source's CONTENT identity
1011
+ * (`integrity`), so a corrected republish under the SAME version string is a
1012
+ * cache miss (rebuild), not a stale hit — no manual `rm -rf` ever required.
1013
+ * • a failed attempt auto-cleans and retries ONCE from a clean slate before
1014
+ * surfacing the error (transient blips heal themselves); deterministic
1015
+ * failures (`e.permanent`) skip the retry and fail loud immediately.
1016
+ *
1017
+ * @param {{ source: string, version: string, isUrl?: boolean, strip?: number, integrity?: string|null }} spec
1018
+ * `integrity` is the source artifact's content identity when the resolver knows
1019
+ * it (npm `dist.integrity`/`dist.shasum`); used to verify the downloaded bytes
1020
+ * and to bust a cached entry whose recorded identity no longer matches.
1021
+ * @param {{ log?: (m: string) => void, cacheRoot?: string }} [opts]
886
1022
  * @returns {Promise<string>} the cached, installed runner tree's root directory.
887
1023
  */
888
- export async function installRunnerTarball({ source, version, isUrl = true, strip = 0 }, { log = (m) => console.error(m) } = {}) {
889
- const runnerDir = join(RENDERER_CACHE_ROOT, version);
890
- const marker = join(runnerDir, ".tot-cache-complete");
891
- if (existsSync(marker)) return runnerDir; // already downloaded + installed
1024
+ export async function installRunnerTarball(
1025
+ { source, version, isUrl = true, strip = 0, integrity = null },
1026
+ { log = (m) => console.error(m), cacheRoot = RENDERER_CACHE_ROOT } = {},
1027
+ ) {
1028
+ const runnerDir = join(cacheRoot, version);
1029
+ // Staging dirs from DEAD runs (crashed/killed installs) must not leak disk
1030
+ // forever — reap them here, the one funnel every install path goes through.
1031
+ sweepStaleStagingDirs(cacheRoot);
1032
+
1033
+ const localSource = isUrl ? null : resolveLocalTarball(source);
1034
+ // The EXPECTED content identity of the source artifact. A local tarball with no
1035
+ // caller-provided integrity is cheap to hash on every run, so a same-path
1036
+ // republish (new contents, same file name) busts the cache too.
1037
+ const expected = integrity || (isUrl ? null : fileIntegrity(localSource));
1038
+
1039
+ const cached = readCacheMarker(runnerDir);
1040
+ if (cached) {
1041
+ if (!rendererCacheHealthy(runnerDir)) {
1042
+ // A cache poisoned with the wrong-arch binaries (npm/cli#4828 — e.g.
1043
+ // darwin-x64 on an arm64 Mac) is otherwise trusted forever, and astro
1044
+ // crashes at boot with `Cannot find native binding`, swallowed as "the dev
1045
+ // server didn't come up". Fall through and rebuild from the source below.
1046
+ log(`~ store preview engine cache is missing native bindings for ${process.platform}-${process.arch} — rebuilding it…`);
1047
+ } else if (expected && cached.integrity && expected !== cached.integrity) {
1048
+ // CONTENT-HASH BUST: same version string, different artifact contents — a
1049
+ // corrected republish. The cached entry is stale by identity, not by label;
1050
+ // rebuild from the corrected source instead of serving the stale cache.
1051
+ log(`~ the preview engine's ${version} artifact changed upstream (same version, new contents) — rebuilding…`);
1052
+ } else {
1053
+ return runnerDir; // already downloaded + installed (and contents still match)
1054
+ }
1055
+ }
892
1056
 
893
1057
  // First run only — set the expectation so the one-time cost doesn't read as a
894
1058
  // hang: this downloads + installs the renderer once, then every later run of
895
1059
  // this version is a no-network cache hit.
896
1060
  log(`~ first run: setting up your store preview (~a minute, one-time — cached after this)…`);
897
- const localSource = isUrl ? null : resolveLocalTarball(source);
898
- const archivePath = isUrl ? join(tmpdir(), `tot-renderer-${process.pid}-${Date.now()}.tar.gz`) : localSource;
899
- try {
900
- if (isUrl) {
901
- // The fetch itself is otherwise silent (no per-byte output) and can run
902
- // tens of seconds on a cold cache — tick a spinner so it never looks hung.
903
- const spin = startProgress("downloading the store preview engine…");
1061
+ // AUTO-CLEAN-AND-RETRY-ONCE: a transient failure (network blip mid-download, a
1062
+ // registry hiccup mid-install) heals itself with one clean re-attempt instead
1063
+ // of stopping a first run at an error only `rm -rf` folklore could clear.
1064
+ // Bounded to one retry so a genuinely-broken source still fails loudly.
1065
+ for (let attempt = 1; ; attempt++) {
1066
+ const archivePath = isUrl ? join(tmpdir(), `tot-renderer-${process.pid}-${attempt}.tar.gz`) : localSource;
1067
+ const stagingDir = `${runnerDir}.staging-${process.pid}`;
1068
+ try {
1069
+ if (isUrl) {
1070
+ // The fetch itself is otherwise silent (no per-byte output) and can run
1071
+ // tens of seconds on a cold cache — tick a spinner so it never looks hung.
1072
+ const spin = startProgress("downloading the store preview engine…");
1073
+ try {
1074
+ await downloadFile(source, archivePath);
1075
+ } finally {
1076
+ spin.stop();
1077
+ }
1078
+ }
1079
+ if (!existsSync(archivePath)) {
1080
+ throw new Error(`renderer tarball not found: ${archivePath}`);
1081
+ }
1082
+ // Refuse to install bytes that don't match the source's declared identity —
1083
+ // a truncated/corrupted download would otherwise be cached as if complete.
1084
+ // (Transient by nature, so the retry above gets a fresh download.)
1085
+ if (expected && !tarballMatchesIntegrity(archivePath, expected)) {
1086
+ throw new Error(`the downloaded preview-engine tarball failed its integrity check (expected ${expected})`);
1087
+ }
1088
+ // The identity recorded in the completion manifest below — what future runs
1089
+ // compare against to detect a same-version republish. Hash the actual bytes
1090
+ // when the resolver couldn't tell us (e.g. the entitled signed-URL path).
1091
+ const contentId = expected || fileIntegrity(archivePath);
1092
+ rmSync(stagingDir, { recursive: true, force: true });
1093
+ mkdirSync(stagingDir, { recursive: true });
1094
+ extractTarball(archivePath, stagingDir, { strip });
1095
+ // Pin the runner install to PUBLIC npm. The moat-free runner has only public
1096
+ // deps, but the HOST's global ~/.npmrc may point `registry` at a private
1097
+ // mirror (an internal proxy that 502s, or one an invited developer can't
1098
+ // reach) — an invited dev's machine config must never decide where the
1099
+ // runner's public deps come from. A project-level .npmrc wins over the user's.
1100
+ writeFileSync(join(stagingDir, ".npmrc"), "registry=https://registry.npmjs.org/\n");
1101
+ // The install is the long, noisy step — tick a spinner while its output goes
1102
+ // to a log, so the terminal shows one clean line instead of the pnpm firehose.
1103
+ // corepack setup logs to the SAME file so its failures aren't invisible (they
1104
+ // were the silent cause of "couldn't set up the store preview engine").
1105
+ const installLog = join(cacheRoot, `${version}.install.log`);
1106
+ ensureCorepackPnpm(stagingDir, { logPath: installLog });
1107
+ const installSpin = startProgress("installing the store preview engine…", {
1108
+ stages: [{ afterMs: 20000, text: "still setting up the preview engine (first run only)…" }],
1109
+ });
904
1110
  try {
905
- await downloadFile(source, archivePath);
1111
+ await runPnpmInstall(stagingDir, { logPath: installLog });
906
1112
  } finally {
907
- spin.stop();
1113
+ installSpin.stop();
908
1114
  }
1115
+ // Atomic-ish: only rename into the final, discoverable path once install
1116
+ // succeeded, so a crashed/interrupted run never leaves a half-built cache
1117
+ // entry that a later `tot dev` would treat as ready.
1118
+ rmSync(runnerDir, { recursive: true, force: true });
1119
+ renameSync(stagingDir, runnerDir);
1120
+ // Fence a fresh install against npm/cli#4828: if the installer left the wrong
1121
+ // arch's native bindings (or none) for this host, DON'T stamp the completion
1122
+ // marker — an unmarked tree is never reused, so the next run reinstalls cleanly
1123
+ // instead of caching the poison and crashing astro at boot. Fail loud + actionable
1124
+ // rather than swallow it as "the dev server didn't come up". Permanent: the
1125
+ // same installer on the same host would just produce the same result, so the
1126
+ // auto-retry is skipped.
1127
+ if (!rendererCacheHealthy(runnerDir)) {
1128
+ await emitObstacle("renderer-native-bindings-missing");
1129
+ const err = new CliError(
1130
+ `the store preview engine installed but is missing its native components for ${process.platform}-${process.arch}`,
1131
+ {
1132
+ 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)",
1133
+ exitCode: 2,
1134
+ },
1135
+ );
1136
+ err.permanent = true;
1137
+ throw err;
1138
+ }
1139
+ writeCacheMarker(runnerDir, { version, integrity: contentId });
1140
+ return runnerDir;
1141
+ } catch (e) {
1142
+ // A failed attempt must never survive on disk — not as staging debris, and
1143
+ // (by promote-on-success) it never reached the canonical slot at all.
1144
+ rmSync(stagingDir, { recursive: true, force: true });
1145
+ if (e?.permanent === true || attempt >= 2) throw e;
1146
+ log(`~ that didn't work (${String(e?.message || e).split("\n")[0]}) — retrying once from a clean slate…`);
1147
+ } finally {
1148
+ if (isUrl) rmSync(archivePath, { force: true });
909
1149
  }
910
- if (!existsSync(archivePath)) {
911
- throw new Error(`renderer tarball not found: ${archivePath}`);
912
- }
913
- const stagingDir = `${runnerDir}.staging-${process.pid}`;
914
- rmSync(stagingDir, { recursive: true, force: true });
915
- mkdirSync(stagingDir, { recursive: true });
916
- extractTarball(archivePath, stagingDir, { strip });
917
- // Pin the runner install to PUBLIC npm. The moat-free runner has only public
918
- // deps, but the HOST's global ~/.npmrc may point `registry` at a private
919
- // mirror (an internal proxy that 502s, or one an invited developer can't
920
- // reach) — an invited dev's machine config must never decide where the
921
- // runner's public deps come from. A project-level .npmrc wins over the user's.
922
- writeFileSync(join(stagingDir, ".npmrc"), "registry=https://registry.npmjs.org/\n");
923
- // The install is the long, noisy step — tick a spinner while its output goes
924
- // to a log, so the terminal shows one clean line instead of the pnpm firehose.
925
- // corepack setup logs to the SAME file so its failures aren't invisible (they
926
- // were the silent cause of "couldn't set up the store preview engine").
927
- const installLog = join(RENDERER_CACHE_ROOT, `${version}.install.log`);
928
- ensureCorepackPnpm(stagingDir, { logPath: installLog });
929
- const installSpin = startProgress("installing the store preview engine…", {
930
- stages: [{ afterMs: 20000, text: "still setting up the preview engine (first run only)…" }],
931
- });
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Read a cache entry's completion marker (`.tot-cache-complete`). Returns the
1155
+ * manifest object (at least `{ integrity: string|null }`), or null when the
1156
+ * marker is absent — i.e. the entry is incomplete/partial and must be treated
1157
+ * as if it didn't exist. A legacy pre-manifest marker (a bare timestamp string)
1158
+ * reads as complete-with-unknown-identity, so existing healthy caches survive
1159
+ * the upgrade without a forced rebuild.
1160
+ */
1161
+ export function readCacheMarker(dir) {
1162
+ try {
1163
+ const raw = readFileSync(join(dir, ".tot-cache-complete"), "utf8");
932
1164
  try {
933
- await runPnpmInstall(stagingDir, { logPath: installLog });
934
- } finally {
935
- installSpin.stop();
1165
+ const m = JSON.parse(raw);
1166
+ if (m && typeof m === "object") return { integrity: null, ...m };
1167
+ } catch {
1168
+ /* legacy timestamp-string marker */
936
1169
  }
937
- // Atomic-ish: only rename into the final, discoverable path once install
938
- // succeeded, so a crashed/interrupted run never leaves a half-built cache
939
- // entry that a later `tot dev` would treat as ready.
940
- rmSync(runnerDir, { recursive: true, force: true });
941
- renameSync(stagingDir, runnerDir);
942
- writeFileSync(marker, new Date().toISOString());
943
- } finally {
944
- if (isUrl) rmSync(archivePath, { force: true });
1170
+ return { integrity: null };
1171
+ } catch {
1172
+ return null; // no marker never treat the entry as installed
1173
+ }
1174
+ }
1175
+
1176
+ /** Stamp a cache entry complete: version + source content identity + when. */
1177
+ function writeCacheMarker(dir, { version, integrity }) {
1178
+ writeFileSync(
1179
+ join(dir, ".tot-cache-complete"),
1180
+ JSON.stringify({ version, integrity: integrity || null, completedAt: new Date().toISOString() }) + "\n",
1181
+ );
1182
+ }
1183
+
1184
+ /** sha512 SRI (`sha512-<base64>`, npm's `dist.integrity` format) of a file; null when unreadable. */
1185
+ function fileIntegrity(path) {
1186
+ try {
1187
+ return `sha512-${createHash("sha512").update(readFileSync(path)).digest("base64")}`;
1188
+ } catch {
1189
+ return null;
1190
+ }
1191
+ }
1192
+
1193
+ /**
1194
+ * Do the tarball's bytes match `expected` — an SRI string (`sha512-<b64>`, npm's
1195
+ * `dist.integrity`) or npm's legacy `dist.shasum` (bare 40-hex sha1)? Unknown
1196
+ * formats and probe errors return true: this check exists to catch corrupted
1197
+ * bytes, never to block an install on a format we can't verify.
1198
+ */
1199
+ export function tarballMatchesIntegrity(archivePath, expected) {
1200
+ try {
1201
+ const want = String(expected).trim();
1202
+ const sri = /^(sha512|sha384|sha256|sha1)-([A-Za-z0-9+/=]+)$/.exec(want);
1203
+ if (sri) {
1204
+ return createHash(sri[1]).update(readFileSync(archivePath)).digest("base64") === sri[2];
1205
+ }
1206
+ if (/^[0-9a-f]{40}$/i.test(want)) {
1207
+ return createHash("sha1").update(readFileSync(archivePath)).digest("hex") === want.toLowerCase();
1208
+ }
1209
+ return true;
1210
+ } catch {
1211
+ return true;
1212
+ }
1213
+ }
1214
+
1215
+ /**
1216
+ * Reap `<entry>.staging-<pid>` dirs left by DEAD processes — failed/killed
1217
+ * installs used to accumulate one orphaned staging tree per attempt, leaking
1218
+ * disk forever. A staging dir whose pid is still alive belongs to a concurrent
1219
+ * `tot dev` mid-install and is left alone. Best-effort: never throws, and never
1220
+ * touches this process's own staging dir (created fresh after this sweep).
1221
+ */
1222
+ export function sweepStaleStagingDirs(cacheRoot, { pidAlive = processAlive } = {}) {
1223
+ try {
1224
+ if (!cacheRoot || !existsSync(cacheRoot)) return;
1225
+ for (const name of readdirSync(cacheRoot)) {
1226
+ const m = /\.staging-(\d+)$/.exec(name);
1227
+ if (!m) continue;
1228
+ const pid = Number(m[1]);
1229
+ if (pid === process.pid || pidAlive(pid)) continue;
1230
+ rmSync(join(cacheRoot, name), { recursive: true, force: true });
1231
+ }
1232
+ } catch {
1233
+ /* best-effort cache hygiene */
1234
+ }
1235
+ }
1236
+
1237
+ /** Is a pid a live process? (signal 0 probe; EPERM = alive but not ours.) */
1238
+ function processAlive(pid) {
1239
+ try {
1240
+ process.kill(pid, 0);
1241
+ return true;
1242
+ } catch (e) {
1243
+ return e?.code === "EPERM";
1244
+ }
1245
+ }
1246
+
1247
+ /**
1248
+ * The registry-declared content identity (`dist.integrity`, else `dist.shasum`)
1249
+ * of the public runner at `version` — or null when npm can't answer QUICKLY
1250
+ * (offline, slow, 4xx/5xx, malformed). Used by ensureSampleRenderer's cached
1251
+ * fast path to detect a same-version republish without ever making the network
1252
+ * a hard dependency: null means "can't verify right now — trust the cache",
1253
+ * preserving the offline-reuse behavior exactly.
1254
+ */
1255
+ export async function publishedRunnerIntegrity(env, version, { timeoutMs = 2000, fetchFn = fetch } = {}) {
1256
+ try {
1257
+ const pkg = env.TOT_RUNNER_PACKAGE || PUBLIC_RUNNER_PACKAGE;
1258
+ const registry = (env.TOT_NPM_REGISTRY || DEFAULT_NPM_REGISTRY).replace(/\/$/, "");
1259
+ const res = await fetchFn(`${registry}/${pkg.replace("/", "%2f")}`, {
1260
+ headers: { accept: "application/json" },
1261
+ signal: AbortSignal.timeout(timeoutMs),
1262
+ });
1263
+ if (!res.ok) return null;
1264
+ const dist = (await res.json())?.versions?.[version]?.dist;
1265
+ return dist?.integrity || dist?.shasum || null;
1266
+ } catch {
1267
+ return null;
945
1268
  }
946
- return runnerDir;
947
1269
  }
948
1270
 
949
1271
  /** Strip an optional file:// prefix from a local tarball path and resolve it absolute. */
@@ -1059,21 +1381,25 @@ function spawnAsyncResult(cmd, args, opts = {}) {
1059
1381
  export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult } = {}) {
1060
1382
  const fd = logPath ? openSync(logPath, "a") : null;
1061
1383
  const installArgs = ["install", "--config.dangerouslyAllowAllBuilds=true"];
1062
- // npm FIRST: it ships with EVERY Node (including 25+, where corepack is no
1063
- // longer bundled), so it's the launcher with zero machine-specific setup —
1064
- // no global pnpm, no corepack shim dance. The runner tree is built to be
1065
- // npm-installable (build-runner.mjs rewrites `workspace:*` "*" and emits an
1066
- // npm `workspaces` field; verified end-to-end with both installers). pnpm and
1067
- // the corepack-pinned pnpm remain as fallbacks for hosts with a broken npm.
1384
+ // pnpm FIRST: the renderer tree is a bundle of native optional deps (rolldown,
1385
+ // esbuild, sharp, @rollup, lightningcss, @tailwindcss/oxide, @astrojs/compiler,
1386
+ // workerd), and npm's optional-deps bug (npm/cli#4828) routinely installs the
1387
+ // WRONG arch's binary or none poisoning the cache so astro crashes at boot with
1388
+ // `Cannot find native binding`. pnpm resolves per-platform optional bindings
1389
+ // correctly (proven end-to-end). ensureCorepackPnpm() ran just above, so on the
1390
+ // Node floor (22.12, which bundles corepack) pnpm is available; `corepack pnpm`
1391
+ // is the shim path if a bare `pnpm` isn't on PATH. npm stays LAST as the escape
1392
+ // hatch for hosts with neither pnpm nor corepack — where rendererCacheHealthy()
1393
+ // then fences a poisoned result rather than shipping a broken cache silently.
1068
1394
  // If a launcher isn't installed at all (ENOENT) we move on; a launcher that
1069
1395
  // RAN but whose install failed is the real error and stops the loop.
1070
1396
  // REQUIRES a runner >= 1.3.4-rc.2 — older runner tarballs still carry
1071
- // `workspace:*` deps npm rejects (harmless here: pickRunnerVersion pins the
1072
- // runner to this CLI's version, so this CLI never installs those).
1397
+ // `workspace:*` deps npm rejects (harmless here: resolution stays on this
1398
+ // CLI's compatible release line, whose runner artifacts are installable).
1073
1399
  const attempts = [
1074
- { cmd: "npm", args: ["install", "--no-audit", "--no-fund"] },
1075
1400
  { cmd: "pnpm", args: installArgs },
1076
1401
  { cmd: "corepack", args: ["pnpm", ...installArgs] },
1402
+ { cmd: "npm", args: ["install", "--no-audit", "--no-fund"] },
1077
1403
  ];
1078
1404
  try {
1079
1405
  for (const { cmd, args } of attempts) {
@@ -1095,22 +1421,33 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
1095
1421
  continue;
1096
1422
  }
1097
1423
  // The launcher ran; the install itself failed. That's the actionable error.
1424
+ // installRunnerTarball retries transient failures once from a clean slate;
1425
+ // deterministic broken-release 404s are marked permanent below and skip it.
1098
1426
  await emitObstacle("install-failed");
1099
- throw new CliError(
1427
+ const brokenRelease = missingPackage404(logPath);
1428
+ const err = new CliError(
1100
1429
  `couldn't set up the store preview engine${pnpmFailureHint(logPath)}` +
1101
1430
  (logPath ? `\n details: ${logPath}` : ""),
1102
- { next: "check the details log above, then re-run `tot start` (it resumes from the cache)" },
1431
+ { next: "check the details log above, then re-run `tot start` (it retries from a clean slate — no cache to clear)" },
1103
1432
  );
1433
+ // A package 404 is deterministic metadata baked into this immutable runner
1434
+ // tarball. Retrying the same bytes wastes another full install; return at once
1435
+ // so the caller can transparently fall back to the compatible public patch.
1436
+ if (brokenRelease) err.permanent = true;
1437
+ throw err;
1104
1438
  }
1105
1439
  // Every launcher ENOENT'd → there's no pnpm on this machine and corepack
1106
1440
  // couldn't provide one (corepack isn't bundled on Node 25+). npm ships with
1107
1441
  // every Node, so `npm i -g pnpm` is the escape hatch that always exists.
1442
+ // Permanent: retrying can't conjure a launcher — skip the clean-slate retry.
1108
1443
  await emitObstacle("pnpm-missing");
1109
- throw new CliError(
1444
+ const err = new CliError(
1110
1445
  "couldn't set up the store preview engine — pnpm isn't available on this machine" +
1111
1446
  (logPath ? `\n details: ${logPath}` : ""),
1112
1447
  { next: "install pnpm with `npm i -g pnpm` (or `corepack enable`), then re-run `tot start`" },
1113
1448
  );
1449
+ err.permanent = true;
1450
+ throw err;
1114
1451
  } finally {
1115
1452
  if (fd !== null) closeSync(fd);
1116
1453
  }
@@ -1124,6 +1461,19 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
1124
1461
  function pnpmFailureHint(logPath) {
1125
1462
  if (!logPath) return " — is pnpm/corepack available on this host?";
1126
1463
  try {
1464
+ // A 404 means the registry answered — a specific package/version doesn't
1465
+ // exist there. Since installs now run from a clean slate every attempt
1466
+ // (promote-on-success + auto-retry), this is a BROKEN RUNNER RELEASE (it
1467
+ // references an unpublished package), not the user's cache — no `rm -rf`
1468
+ // will help. Check this BEFORE the generic ERR_PNPM_FETCH match, since
1469
+ // pnpm's 404 error text also contains "ERR_PNPM_FETCH".
1470
+ const missing404 = missingPackage404(logPath);
1471
+ if (missing404) {
1472
+ return (
1473
+ ` — the preview engine references a package that isn't published (${missing404});` +
1474
+ " that's a broken preview-engine release, not your machine"
1475
+ );
1476
+ }
1127
1477
  const tail = readFileSync(logPath, "utf8").slice(-8000);
1128
1478
  if (/npm\.tokenoftrust\.com|ERR_PNPM_FETCH|502|ECONNREFUSED|ETIMEDOUT|ENOTFOUND/i.test(tail)) {
1129
1479
  return " — the Token of Trust package registry looks unreachable right now; check your connection and retry";
@@ -1134,6 +1484,17 @@ function pnpmFailureHint(logPath) {
1134
1484
  return "";
1135
1485
  }
1136
1486
 
1487
+ /** Return the missing package URL from a pnpm 404, else null. */
1488
+ function missingPackage404(logPath) {
1489
+ if (!logPath) return null;
1490
+ try {
1491
+ const tail = readFileSync(logPath, "utf8").slice(-8000);
1492
+ return tail.match(/ERR_PNPM_FETCH_404[^\n]*GET\s+(\S+)/i)?.[1] || null;
1493
+ } catch {
1494
+ return null;
1495
+ }
1496
+ }
1497
+
1137
1498
  /**
1138
1499
  * Run the cached runner's own scripts/tot-dev.mjs in standalone (--workspace)
1139
1500
  * mode — the exact code path the WS3 spike verified gives native, non-polled