@tokenoftrust/cli 2.0.9 → 2.0.11

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": "2.0.9",
3
+ "version": "2.0.11",
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",
@@ -59,7 +59,7 @@ const DEFAULT_DEV_IMAGE =
59
59
  "242086487598.dkr.ecr.us-east-1.amazonaws.com/tot-dev:latest";
60
60
  const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
61
61
  /** Where downloaded+installed renderer-artifact versions are cached, one dir per version. */
62
- const RENDERER_CACHE_ROOT = join(homedir(), ".tot", "cache", "renderer");
62
+ export const RENDERER_CACHE_ROOT = join(homedir(), ".tot", "cache", "renderer");
63
63
  /** The PUBLIC, un-entitled runner published to npm (sample / zero-login mode). */
64
64
  const PUBLIC_RUNNER_PACKAGE = "@tokenoftrust/storefront-runner";
65
65
  const DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
@@ -804,6 +804,31 @@ export function rendererCacheHealthy(runnerDir, { platform = process.platform, a
804
804
  }
805
805
  }
806
806
 
807
+ /**
808
+ * True unless the installed runner package is missing source the Astro config
809
+ * chain needs to even boot — the "renderer resolves but `tot dev` can't load
810
+ * its Astro config" failure mode (fb-1788659971983-kdpj1i: `1.4.2-rc.0` was
811
+ * packed without `scripts/tenant/`, which `apps/storefront/integrations/
812
+ * materialize-guard.mjs` and `apps/storefront/dev-plugins/tenant-hot-reload.mjs`
813
+ * import — both loaded from `astro.config.mjs`). A tarball packing a subset of
814
+ * the tree is a packaging defect a developer can't fix locally, so this is
815
+ * checked the same way as `rendererCacheHealthy`: on every cache hit AND right
816
+ * after a fresh install, never trusted forever once cached. Never throws; on
817
+ * any probe error it returns true (trust the cache) so the probe itself can't
818
+ * break `tot dev`.
819
+ */
820
+ export function rendererArtifactComplete(runnerDir) {
821
+ try {
822
+ return (
823
+ existsSync(join(runnerDir, "apps/storefront/astro.config.mjs")) &&
824
+ existsSync(join(runnerDir, "scripts/tenant")) &&
825
+ readdirSync(join(runnerDir, "scripts/tenant")).length > 0
826
+ );
827
+ } catch {
828
+ return true;
829
+ }
830
+ }
831
+
807
832
  /**
808
833
  * Delete every OTHER public-runner cache dir (`public-*` except `keepVersion`)
809
834
  * once we've resolved the version this CLI pins to — so a pre-alignment dir
@@ -1046,7 +1071,15 @@ export async function installRunnerTarball(
1046
1071
 
1047
1072
  const cached = readCacheMarker(runnerDir);
1048
1073
  if (cached) {
1049
- if (!rendererCacheHealthy(runnerDir)) {
1074
+ if (!rendererArtifactComplete(runnerDir)) {
1075
+ // The PACKAGE itself is missing source its own Astro config chain needs
1076
+ // (fb-1788659971983-kdpj1i) — re-downloading the SAME version reproduces
1077
+ // the same incomplete tarball, so this can't self-heal like the transient
1078
+ // cases below. Fall through anyway (uniform code path); the post-install
1079
+ // check further down turns the reproduced failure into a loud, actionable
1080
+ // error instead of silently caching the broken tree again.
1081
+ log(`~ store preview engine ${version} is missing files its own Astro config needs — rebuilding it…`);
1082
+ } else if (!rendererCacheHealthy(runnerDir)) {
1050
1083
  // A cache poisoned with the wrong-arch binaries (npm/cli#4828 — e.g.
1051
1084
  // darwin-x64 on an arm64 Mac) is otherwise trusted forever, and astro
1052
1085
  // crashes at boot with `Cannot find native binding`, swallowed as "the dev
@@ -1100,6 +1133,25 @@ export async function installRunnerTarball(
1100
1133
  rmSync(stagingDir, { recursive: true, force: true });
1101
1134
  mkdirSync(stagingDir, { recursive: true });
1102
1135
  extractTarball(archivePath, stagingDir, { strip });
1136
+ // Fail fast, BEFORE the slow pnpm install, when the package itself is
1137
+ // missing source its own Astro config chain needs (fb-1788659971983-kdpj1i:
1138
+ // a runner published with an incomplete `files`/packing list — e.g.
1139
+ // `scripts/tenant/` absent from a tarball that ships integrations
1140
+ // importing it). This is a packaging defect in the published artifact
1141
+ // itself, so it can't self-heal by retrying and is marked permanent —
1142
+ // the actionable remedy is pinning a different (working) runner version.
1143
+ if (!rendererArtifactComplete(stagingDir)) {
1144
+ await emitObstacle("renderer-artifact-incomplete");
1145
+ const err = new CliError(
1146
+ `the store preview engine ${version} is missing files its own Astro config needs (this is a broken published runner, not something on your machine)`,
1147
+ {
1148
+ next: `pin a known-good runner: tot dev --renderer-version <version> (or TOT_RUNNER_VERSION=<version>), and file feedback naming ${version} as broken`,
1149
+ exitCode: 2,
1150
+ },
1151
+ );
1152
+ /** @type {any} */ (err).permanent = true;
1153
+ throw err;
1154
+ }
1103
1155
  // Pin the runner install to PUBLIC npm. The moat-free runner has only public
1104
1156
  // deps, but the HOST's global ~/.npmrc may point `registry` at a private
1105
1157
  // mirror (an internal proxy that 502s, or one an invited developer can't
@@ -19,14 +19,14 @@
19
19
  * in the checks list with its usual next command.
20
20
  */
21
21
  import { spawnSync } from "node:child_process";
22
- import { existsSync, mkdirSync } from "node:fs";
22
+ import { existsSync, mkdirSync, readdirSync } from "node:fs";
23
23
  import { homedir } from "node:os";
24
24
  import { join } from "node:path";
25
25
  import { hasLegacyOperatorEnv, legacyOperatorEnvAdvisory } from "../auth.mjs";
26
26
  import { MIN_NODE, nodeMeetsFloor } from "../ensure-node.mjs";
27
27
  import { clientPackages, osLabel } from "../mcp.mjs";
28
28
  import { defaultCredentialsPath, readCredentials, isExpired } from "../token-store.mjs";
29
- import { dockerAvailable, tryStartDocker } from "./dev.mjs";
29
+ import { dockerAvailable, tryStartDocker, RENDERER_CACHE_ROOT, rendererArtifactComplete } from "./dev.mjs";
30
30
  import { loginAndCache } from "./login.mjs";
31
31
 
32
32
  const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
@@ -102,6 +102,33 @@ export function collectChecks(_ctx, env = process.env) {
102
102
  blocking: false,
103
103
  });
104
104
 
105
+ // Inspect any ALREADY-CACHED preview engine(s) for completeness — a version
106
+ // whose published tarball was packed without source its own Astro config
107
+ // needs (fb-1788659971983-kdpj1i) leaves `tot dev` unable to boot even
108
+ // though every check above passes. This stays a filesystem stat, never a
109
+ // spawn (`tot doctor` never runs the runner) — it just reads what's already
110
+ // on disk, so it can only ever report on a version this developer has
111
+ // already hit, not pre-flight one that hasn't been resolved/installed yet.
112
+ try {
113
+ const cacheRoot = RENDERER_CACHE_ROOT;
114
+ const cachedVersions = existsSync(cacheRoot)
115
+ ? readdirSync(cacheRoot).filter(
116
+ (name) => !name.endsWith(".install.log") && !name.includes(".staging-") && existsSync(join(cacheRoot, name, "package.json")),
117
+ )
118
+ : [];
119
+ const broken = cachedVersions.filter((v) => !rendererArtifactComplete(join(cacheRoot, v)));
120
+ if (broken.length) {
121
+ checks.push({
122
+ name: "cached preview engine",
123
+ pass: false,
124
+ detail: `${broken.join(", ")} — missing files its own Astro config needs (broken published runner); pin a different one: tot dev --renderer-version <version>`,
125
+ blocking: false,
126
+ });
127
+ }
128
+ } catch {
129
+ /* best-effort — never fail doctor on a cache-inspection error */
130
+ }
131
+
105
132
  const mcpUrl = env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
106
133
  checks.push({
107
134
  name: "MCP endpoint",
@@ -1158,9 +1158,9 @@ function reportCandidate(result, changeId, { quiet = false } = {}) {
1158
1158
  * status?: {status?: string, reconcile?: object|null, compliance?: object|null,
1159
1159
  * previewUrl?: string|null, shipped?: object|null, dispatched?: boolean|null,
1160
1160
  * notDispatched?: boolean, delivery?: object|null}|null,
1161
- * previewPrUrl?: string|null, error?: string|null, note?: string|null }} input
1161
+ * previewPrUrl?: string|null, error?: string|null, note?: string|null, noChanges?: boolean }} input
1162
1162
  */
1163
- export function buildJsonResult({ ok, ref = null, commit = null, changeId = null, candidate = null, status = null, previewPrUrl = null, error = null, note = null }) {
1163
+ export function buildJsonResult({ ok, ref = null, commit = null, changeId = null, candidate = null, status = null, previewPrUrl = null, error = null, note = null, noChanges = false }) {
1164
1164
  return {
1165
1165
  ok,
1166
1166
  ref,
@@ -1183,6 +1183,13 @@ export function buildJsonResult({ ok, ref = null, commit = null, changeId = null
1183
1183
  forwardFailed: /** @type {any} */ (status)?.forwardFailed ?? false,
1184
1184
  delivery: status?.delivery ?? null,
1185
1185
  previewPrUrl,
1186
+ // Honest-diagnosis parity with the human-readable formatNotDispatchedBlock: a
1187
+ // `--json` caller gets the SAME "why" signal a human sees on the console. Without
1188
+ // this, `candidate:null, notDispatched:true, delivery:null` reads identically for
1189
+ // "no file diff → candidate_open never even ran" and "webhook never registered" /
1190
+ // "wrong tenant scope" — an automation caller (or a human piping --json) had no way
1191
+ // to tell an empty-diff no-op from a genuine platform dispatch failure.
1192
+ ...(noChanges ? { noChanges: true } : {}),
1186
1193
  ...(error ? { error } : {}),
1187
1194
  ...(note ? { note } : {}),
1188
1195
  };
@@ -1756,8 +1763,9 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
1756
1763
  // — automation doesn't want a browser popping up.
1757
1764
  // `commit: statusSha` — the diagnostic blocks name the sha that was actually polled,
1758
1765
  // so a "never dispatched" hint points at a sha `preview_status` knows about.
1759
- reportStatus(status, tenant, { open: !args.noOpen && !args.json, quiet: args.json, commit: statusSha, ref, verb, noChanges: patchEntries.length === 0 });
1760
- emitJson(args, buildJsonResult({ ok: status?.status !== "failed", ref, commit, changeId, candidate, status, previewPrUrl }));
1766
+ const noChanges = patchEntries.length === 0;
1767
+ reportStatus(status, tenant, { open: !args.noOpen && !args.json, quiet: args.json, commit: statusSha, ref, verb, noChanges });
1768
+ emitJson(args, buildJsonResult({ ok: status?.status !== "failed", ref, commit, changeId, candidate, status, previewPrUrl, noChanges }));
1761
1769
  return status?.status === "failed" ? 1 : 0;
1762
1770
  } catch (e) {
1763
1771
  progress?.stop();
package/src/obstacle.mjs CHANGED
@@ -19,7 +19,7 @@ import { defaultCredentialsPath, readCredentials } from "./token-store.mjs";
19
19
  * Best-effort obstacle beacon for a post-login failure. No-op (silent) when no
20
20
  * bridge credential is cached — the developer signed in with a build that didn't
21
21
  * carry the activity flags, or ran a bare `tot login`.
22
- * @param {"pnpm-missing"|"install-failed"|"clone-failed"|"renderer-native-bindings-missing"} kind
22
+ * @param {"pnpm-missing"|"install-failed"|"clone-failed"|"renderer-native-bindings-missing"|"renderer-artifact-incomplete"} kind
23
23
  * @param {{ have?: string, need?: string, env?: NodeJS.ProcessEnv }} [opts]
24
24
  */
25
25
  export async function emitObstacle(kind, { have, need, env = process.env } = {}) {
package/src/validate.mjs CHANGED
@@ -742,7 +742,12 @@ function walk(dir, pred) {
742
742
 
743
743
  // --- link / asset extraction -------------------------------------------------
744
744
  const HREF_RE = /\bhref\s*=\s*"([^"]*)"/gi;
745
- const SRC_RE = /\b(?:src|srcset)\s*=\s*"([^"]*)"/gi;
745
+ const SRC_RE = /\bsrc\s*=\s*"([^"]*)"/gi;
746
+ // `srcset` carries a comma-separated list of `url descriptor` pairs (e.g.
747
+ // "a.webp 480w, b.webp 800w"), not a single URL — each candidate must be split
748
+ // off its descriptor before being checked as an asset. Mirrors
749
+ // scripts/publish/lib/asset-reachability.mjs's srcset handling.
750
+ const SRCSET_RE = /\bsrcset\s*=\s*"([^"]*)"/gi;
746
751
  const STYLE_OPEN_WITH_ATTRS_RE = /<style\s+[^>]*>/i;
747
752
  /** An in-page skip link: an anchor to a #main-ish target, or one carrying a skip class. */
748
753
  const SKIP_LINK_RE = /<a\b[^>]*(?:class="[^"]*\bskip[-\w]*\b[^"]*"|href="#(?:main|content|main-content)\b")/i;
@@ -974,6 +979,14 @@ export function validateTenant(tenantDir, opts = {}) {
974
979
  const f = checkAsset(m[1].trim(), r, publicDir, opts.tenantId || config?.tenant);
975
980
  if (f) findings.push(f);
976
981
  }
982
+ for (const m of html.matchAll(SRCSET_RE)) {
983
+ for (const candidate of m[1].split(",")) {
984
+ const url = candidate.trim().split(/\s+/)[0];
985
+ if (!url) continue;
986
+ const f = checkAsset(url, r, publicDir, opts.tenantId || config?.tenant);
987
+ if (f) findings.push(f);
988
+ }
989
+ }
977
990
  }
978
991
 
979
992
  // 5. git conflict markers — advisory (never blocks), but LOUD: a half-resolved