@tokenoftrust/cli 2.0.8 → 2.0.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.
- package/package.json +1 -1
- package/src/commands/dev.mjs +54 -2
- package/src/commands/doctor.mjs +29 -2
- package/src/commands/submit.mjs +59 -7
- package/src/obstacle.mjs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.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",
|
package/src/commands/dev.mjs
CHANGED
|
@@ -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 (!
|
|
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
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -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",
|
package/src/commands/submit.mjs
CHANGED
|
@@ -1154,7 +1154,7 @@ function reportCandidate(result, changeId, { quiet = false } = {}) {
|
|
|
1154
1154
|
* Defaults to null when no numeric PR number was known at result-build time.
|
|
1155
1155
|
* Pure — unit-tested.
|
|
1156
1156
|
* @param {{ ok: boolean, ref?: string|null, commit?: string|null, changeId?: string|null,
|
|
1157
|
-
* candidate?: {prNumber?: number, number?: number, state?: string, url?: string}|null,
|
|
1157
|
+
* candidate?: {prNumber?: number, number?: number, state?: string, url?: string, headSha?: string|null}|null,
|
|
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,
|
|
@@ -1165,6 +1165,10 @@ export function buildJsonResult({ ok, ref = null, commit = null, changeId = null
|
|
|
1165
1165
|
ok,
|
|
1166
1166
|
ref,
|
|
1167
1167
|
commit,
|
|
1168
|
+
// The sha the `status`/`reconcile`/`compliance` fields above are reported against —
|
|
1169
|
+
// the candidate head when the candidate names one, else the local `commit`. Kept
|
|
1170
|
+
// beside `commit` rather than replacing it so automation can tell the two apart.
|
|
1171
|
+
candidateHeadSha: candidate ? resolvePreviewStatusSha(commit, candidate) : null,
|
|
1168
1172
|
changeId,
|
|
1169
1173
|
candidate: candidate
|
|
1170
1174
|
? { number: candidate.prNumber ?? candidate.number ?? null, state: candidate.state ?? null, url: candidate.url ?? null }
|
|
@@ -1184,6 +1188,43 @@ export function buildJsonResult({ ok, ref = null, commit = null, changeId = null
|
|
|
1184
1188
|
};
|
|
1185
1189
|
}
|
|
1186
1190
|
|
|
1191
|
+
/**
|
|
1192
|
+
* The sha `preview_status` actually resolves against. The candidate the forge opened
|
|
1193
|
+
* may carry a DIFFERENT head than the commit you pushed (it is rebuilt onto the
|
|
1194
|
+
* current store), and `preview_status` only knows the candidate head — so polling the
|
|
1195
|
+
* local commit returns a misleading `pending` / "no reconcile delivery observed"
|
|
1196
|
+
* instead of the real result. Prefer the candidate head whenever the candidate names
|
|
1197
|
+
* one; fall back to the local commit (no candidate, or an older MCP that omits it).
|
|
1198
|
+
* Pure — unit-tested.
|
|
1199
|
+
* @param {string|null} commit local HEAD sha
|
|
1200
|
+
* @param {{headSha?: string|null}|null} candidate the candidate_open projection
|
|
1201
|
+
*/
|
|
1202
|
+
export function resolvePreviewStatusSha(commit, candidate) {
|
|
1203
|
+
const head = typeof candidate?.headSha === "string" ? candidate.headSha.trim() : "";
|
|
1204
|
+
return head || commit;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* The two-sha notice, printed ONLY when they actually differ (when they match there is
|
|
1209
|
+
* nothing to disambiguate and a second line would be noise). Names which sha the
|
|
1210
|
+
* reconcile result is reported against, so a developer polling by hand — or reading a
|
|
1211
|
+
* failure — knows which one to use. Pure — unit-tested.
|
|
1212
|
+
* @param {string|null} commit local HEAD sha
|
|
1213
|
+
* @param {{headSha?: string|null}|null} candidate the candidate_open projection
|
|
1214
|
+
* @returns {string[]} lines to print (empty when there is no distinction to draw)
|
|
1215
|
+
*/
|
|
1216
|
+
export function formatCandidateHeadNotice(commit, candidate) {
|
|
1217
|
+
const head = resolvePreviewStatusSha(commit, candidate);
|
|
1218
|
+
if (!commit || !head || head === commit) return [];
|
|
1219
|
+
return [
|
|
1220
|
+
"",
|
|
1221
|
+
` ▸ local commit: ${commit.slice(0, 9)} (what you committed here)`,
|
|
1222
|
+
` candidate head: ${head.slice(0, 9)} (what preview_status reports on)`,
|
|
1223
|
+
" They differ because your candidate is rebuilt on the current store. Follow the",
|
|
1224
|
+
" candidate head — the local commit reads as \"never dispatched\", not as the result.",
|
|
1225
|
+
];
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1187
1228
|
/** Print the `--json` result as one pretty-printed object on stdout — a no-op
|
|
1188
1229
|
* unless `args.json` was passed, so call sites can invoke it unconditionally. */
|
|
1189
1230
|
function emitJson(args, payload) {
|
|
@@ -1663,9 +1704,18 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1663
1704
|
} catch { /* best-effort local hint — a miss just re-derives the stable id */ }
|
|
1664
1705
|
}
|
|
1665
1706
|
|
|
1707
|
+
// Poll the sha `preview_status` actually resolves against — the candidate head, not
|
|
1708
|
+
// the local commit, whenever the candidate names its own. Printed first (when they
|
|
1709
|
+
// differ) so the developer reads the result against the right sha.
|
|
1710
|
+
const statusSha = resolvePreviewStatusSha(commit, candidate) ?? commit;
|
|
1711
|
+
const statusShort = statusSha.slice(0, 9);
|
|
1712
|
+
if (!args.json) {
|
|
1713
|
+
for (const line of formatCandidateHeadNotice(commit, candidate)) console.log(line);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1666
1716
|
let status;
|
|
1667
1717
|
if (args.noWait) {
|
|
1668
|
-
status = normalizePreviewStatus(await client.callTool("preview_status", { commit }));
|
|
1718
|
+
status = normalizePreviewStatus(await client.callTool("preview_status", { commit: statusSha }));
|
|
1669
1719
|
} else {
|
|
1670
1720
|
// One in-place status line (TTY: a spinner with an elapsed-seconds counter;
|
|
1671
1721
|
// non-TTY: a ~10s heartbeat) instead of a newline per poll — the wait reads
|
|
@@ -1681,11 +1731,11 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1681
1731
|
// stage text ("still reconciling…") IS truthful and is left as-is below.
|
|
1682
1732
|
let phase = "reconcile";
|
|
1683
1733
|
if (!args.json) {
|
|
1684
|
-
progress = startProgress(`waiting for reconcile of ${
|
|
1685
|
-
stages: [{ afterMs: 45_000, text: `still reconciling ${
|
|
1734
|
+
progress = startProgress(`waiting for reconcile of ${statusShort}…`, {
|
|
1735
|
+
stages: [{ afterMs: 45_000, text: `still reconciling ${statusShort}… (larger changes take longer)` }],
|
|
1686
1736
|
});
|
|
1687
1737
|
}
|
|
1688
|
-
status = await pollPreviewStatus(client,
|
|
1738
|
+
status = await pollPreviewStatus(client, statusSha, {
|
|
1689
1739
|
...(args.watch ? WATCH_POLL : DEFAULT_POLL),
|
|
1690
1740
|
onTick: (s) => {
|
|
1691
1741
|
// Reconcile is done but we're still waiting on a ship decision (--watch):
|
|
@@ -1693,7 +1743,7 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1693
1743
|
if (!args.json && s.status === "reconciled" && !s.shipped && phase !== "ship") {
|
|
1694
1744
|
phase = "ship";
|
|
1695
1745
|
progress.stop();
|
|
1696
|
-
progress = startProgress(`reconciled ${
|
|
1746
|
+
progress = startProgress(`reconciled ${statusShort} — waiting for a ship decision…`);
|
|
1697
1747
|
}
|
|
1698
1748
|
},
|
|
1699
1749
|
});
|
|
@@ -1704,7 +1754,9 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1704
1754
|
}
|
|
1705
1755
|
// --json also skips the browser auto-open (open: !args.noOpen && !args.json)
|
|
1706
1756
|
// — automation doesn't want a browser popping up.
|
|
1707
|
-
|
|
1757
|
+
// `commit: statusSha` — the diagnostic blocks name the sha that was actually polled,
|
|
1758
|
+
// 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 });
|
|
1708
1760
|
emitJson(args, buildJsonResult({ ok: status?.status !== "failed", ref, commit, changeId, candidate, status, previewPrUrl }));
|
|
1709
1761
|
return status?.status === "failed" ? 1 : 0;
|
|
1710
1762
|
} catch (e) {
|
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 } = {}) {
|