@tokenoftrust/cli 6.0.0 → 6.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
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",
@@ -10,6 +10,8 @@
10
10
  * tot go-live show apex readiness, then (if ready) connect the apex
11
11
  * tot go-live --rehearsal dispatch a DRY-RUN cutover (never moves DNS state)
12
12
  * tot go-live --rollback restore the captured prior DNS records (the one-line revert)
13
+ * tot go-live --acknowledge-uncaptured-images
14
+ * acknowledge the live bundle's uncaptured images, then continue
13
15
  *
14
16
  * The contract, deliberately strict — this is the apex DNS cutover of the live
15
17
  * site (mirrors `tot ship` / `tot rollback`):
@@ -49,6 +51,10 @@ const USAGE = `tot go-live — cut the apex domain over to the storefront
49
51
  tot go-live --url <origin> storefront origin (default: env TOT_STOREFRONT_URL / ${DEFAULT_STOREFRONT_ORIGIN})
50
52
  tot go-live --owner <domain> owner/appDomain to act on (default: env TOT_STOREFRONT_OWNER / checkout tenant)
51
53
  tot go-live --no-open don't open the store in your browser afterward
54
+ tot go-live --acknowledge-uncaptured-images
55
+ acknowledge the live bundle's uncaptured images and continue.
56
+ They keep loading from your source store's URLs and break
57
+ when that store closes. Asks for its own y/N confirmation.
52
58
 
53
59
  go-live drives the SAME apex cutover the admin Domain button does, gated by the
54
60
  same server readiness gate. It ALWAYS shows you the readiness itemization and
@@ -69,6 +75,7 @@ export function parseGoLiveArgs(argv) {
69
75
  owner: null,
70
76
  identity: null,
71
77
  noOpen: false,
78
+ acknowledgeImages: false,
72
79
  help: false,
73
80
  };
74
81
  for (let i = 0; i < argv.length; i++) {
@@ -80,6 +87,7 @@ export function parseGoLiveArgs(argv) {
80
87
  else if (t === "--owner") a.owner = argv[++i];
81
88
  else if (t === "--identity") a.identity = argv[++i];
82
89
  else if (t === "--no-open") a.noOpen = true;
90
+ else if (t === "--acknowledge-uncaptured-images") a.acknowledgeImages = true;
83
91
  else if (t === "--help" || t === "-h") a.help = true;
84
92
  else if (t === "rollback") a.action = "rollback";
85
93
  else if (t === "connect") a.action = "connect";
@@ -108,6 +116,7 @@ export function normalizeReadiness(r) {
108
116
  label: typeof c.label === "string" ? c.label : c.id || "?",
109
117
  ok: c.ok === true,
110
118
  detail: typeof c.detail === "string" ? c.detail : "",
119
+ remedy: typeof c.remedy === "string" ? c.remedy : "",
111
120
  }))
112
121
  : [];
113
122
  return {
@@ -123,10 +132,30 @@ export function normalizeReadiness(r) {
123
132
  : checks.filter((c) => !c.ok).map((c) => c.detail),
124
133
  appDomain: typeof o.appDomain === "string" ? o.appDomain : null,
125
134
  imageTypes: normalizeImageTypes(o.imageTypes),
135
+ imageAcknowledgement: normalizeImageAcknowledgementScope(rd.imageAcknowledgement),
126
136
  domainState: rd && o.domain && typeof o.domain === "object" ? o.domain : null,
127
137
  };
128
138
  }
129
139
 
140
+ /**
141
+ * The live bundle's uncaptured images an owner can acknowledge (`readiness.imageAcknowledgement`), or
142
+ * null when the server reports none. Pure.
143
+ * @param {any} raw
144
+ * @returns {{ digest: string, uncaptured: number, acknowledged: boolean } | null}
145
+ */
146
+ export function normalizeImageAcknowledgementScope(raw) {
147
+ if (!raw || typeof raw !== "object") return null;
148
+ if (typeof raw.digest !== "string" || !Number.isInteger(raw.uncaptured) || raw.uncaptured < 1) return null;
149
+ return { digest: raw.digest, uncaptured: raw.uncaptured, acknowledged: !!raw.acknowledged };
150
+ }
151
+
152
+ /** What `--acknowledge-uncaptured-images` tells the owner before asking. */
153
+ export const UNCAPTURED_IMAGES_WARNING = [
154
+ " These images were not captured into the platform's image store. If you continue, they keep",
155
+ " loading from your source store's URLs, and they break when that store closes. Keep the source",
156
+ " store running, or replace each image and publish again.",
157
+ ];
158
+
130
159
  /**
131
160
  * Normalise a `POST /api/domain/dispatch` response to { dispatched, runId, error }.
132
161
  * `dispatched` is true only when the server confirms it fired the CI dispatch. Pure.
@@ -269,6 +298,7 @@ export async function pollRunTerminal(http, { attempts = 8, delayMs = 2000, slee
269
298
  *
270
299
  * @param {{ get:(path:string)=>Promise<any>, post:(path:string,body:any)=>Promise<any> }} http
271
300
  * @param {{ appDomain:string, action:"connect"|"rollback", rehearsal:boolean, noOpen?:boolean,
301
+ * acknowledgeImages?:boolean,
272
302
  * dnsSetup?: { domainConnectDeclared?: boolean, ciRoleCanChangeZone?: boolean, nameserversAtRegistrar?: boolean } }} params
273
303
  * @param {{ interactive?:()=>boolean, confirm?:(q:string,d:boolean)=>Promise<boolean>,
274
304
  * poll?:typeof pollRunTerminal, openUrl?:(u:string)=>boolean, progress?:boolean,
@@ -276,7 +306,7 @@ export async function pollRunTerminal(http, { attempts = 8, delayMs = 2000, slee
276
306
  * `base`/`identity` only name where and as whom a failed readiness read was made.
277
307
  * @returns {Promise<number>} process exit code
278
308
  */
279
- export async function runGoLive(http, { appDomain, action, rehearsal, noOpen, dnsSetup }, deps = {}) {
309
+ export async function runGoLive(http, { appDomain, action, rehearsal, noOpen, dnsSetup, acknowledgeImages }, deps = {}) {
280
310
  const interactive = deps.interactive || isInteractive;
281
311
  const confirm = deps.confirm || promptYesNo;
282
312
  const poll = deps.poll || pollRunTerminal;
@@ -293,6 +323,37 @@ export async function runGoLive(http, { appDomain, action, rehearsal, noOpen, dn
293
323
  const domain = appDomain || readiness.appDomain;
294
324
  for (const line of renderReadiness({ appDomain: domain, readiness, action })) console.log(line);
295
325
 
326
+ // The owner may acknowledge the live bundle's uncaptured images: the server records exactly the
327
+ // scope it reported, and the census keeps the true count.
328
+ const scope = readiness.imageAcknowledgement;
329
+ if (acknowledgeImages && action === "connect") {
330
+ if (!scope || scope.acknowledged) {
331
+ console.log(" No unacknowledged uncaptured images on the live bundle.");
332
+ } else {
333
+ if (!interactive()) {
334
+ console.error(fail("acknowledging uncaptured images needs an interactive terminal", "run it from a terminal (there is intentionally no --yes)"));
335
+ return 2;
336
+ }
337
+ for (const line of ["", ...UNCAPTURED_IMAGES_WARNING]) console.log(line);
338
+ const ok = await confirm(`\n Acknowledge ${scope.uncaptured} uncaptured image reference(s) on ${domain} and continue?`, false);
339
+ if (!ok) {
340
+ console.log(" Cancelled — nothing changed.");
341
+ return 0;
342
+ }
343
+ try {
344
+ await http.post("/api/domain/readiness", { digest: scope.digest, uncaptured: scope.uncaptured });
345
+ readiness = normalizeReadiness(await http.get("/api/domain/readiness"));
346
+ } catch (e) {
347
+ console.error(fail(`the acknowledgement was refused: ${String(e?.message || e)}`, "re-run `tot go-live` to see the current image census, then acknowledge again"));
348
+ return 1;
349
+ }
350
+ console.log(` Acknowledged ${scope.uncaptured} uncaptured image reference(s).`);
351
+ for (const line of renderReadiness({ appDomain: domain, readiness, action })) console.log(line);
352
+ }
353
+ } else if (action === "connect" && scope && !scope.acknowledged) {
354
+ console.log(" To continue with these images as they are: tot go-live --acknowledge-uncaptured-images");
355
+ }
356
+
296
357
  // A store that does not declare Connect apex has nothing to rehearse, confirm,
297
358
  // or dispatch. Image rows above are the whole checklist. DNS stays with the zone holder.
298
359
  if (readiness.domainConnect !== true) {
@@ -661,7 +722,7 @@ export async function run(argv, ctx) {
661
722
  const http = createStorefrontHttp(transport.base, { token: transport.token, owner, headers: transport.headers });
662
723
  return await runGoLive(
663
724
  http,
664
- { appDomain: owner, action: /** @type {any} */ (args.action), rehearsal: args.rehearsal, noOpen: args.noOpen },
725
+ { appDomain: owner, action: /** @type {any} */ (args.action), rehearsal: args.rehearsal, noOpen: args.noOpen, acknowledgeImages: args.acknowledgeImages },
665
726
  { openUrl: (u) => openBrowser(u), base: transport.base, identity: transport.identity },
666
727
  );
667
728
  }
@@ -309,6 +309,12 @@ export function normalizeShipResult(data) {
309
309
  };
310
310
  }
311
311
 
312
+ /** What accepting uncaptured images means, shown before and after the acceptance. */
313
+ export const UNCAPTURED_IMAGES_LINES = [
314
+ " These images were not captured into the platform's image store. They keep loading from your",
315
+ " source store's URLs, and they break when that store closes.",
316
+ ];
317
+
312
318
  /** A clear next step per plan/ship refusal reason. Pure. */
313
319
  export function refusalNextStep(reason) {
314
320
  switch (reason) {
@@ -324,6 +330,8 @@ export function refusalNextStep(reason) {
324
330
  return "upgrade the storefront subscription to enable go-live";
325
331
  case "golive_unconfirmed":
326
332
  return "re-run `tot ship` and confirm the plan";
333
+ case "checks_blocked":
334
+ return 'replace or remove the named images and publish again, or accept them with `tot ship --accept-quality-findings "<reason>"`; accepted images keep loading from your source store\'s URLs and break when that store closes';
327
335
  case "compliance_review":
328
336
  return 'approve the warnings with `tot ship --accept-quality-findings "<reason>"`, or fix them in `.tot/config.json` and publish again';
329
337
  default:
@@ -606,24 +614,41 @@ export async function runShip(
606
614
  // The server refuses it unless the current durable problem is quality-only and the reason is
607
615
  // substantive. Only after that accountable record exists do we re-dispatch the release.
608
616
  if (acceptQualityReason !== null) {
617
+ const postAccept = (extra = {}) => fetchImpl(`${base}/api/changes/accept-issue`, {
618
+ method: "POST",
619
+ headers: { "content-type": "application/json", ...authHeaders },
620
+ body: JSON.stringify({
621
+ operation: "publication",
622
+ reason: acceptQualityReason,
623
+ ...(plan.ok && plan.artifactDigest ? { revision: plan.artifactDigest } : {}),
624
+ ...extra,
625
+ }),
626
+ });
609
627
  let acceptRes;
628
+ let acceptData;
610
629
  try {
611
- acceptRes = await fetchImpl(`${base}/api/changes/accept-issue`, {
612
- method: "POST",
613
- headers: { "content-type": "application/json", ...authHeaders },
614
- body: JSON.stringify({
615
- operation: "publication",
616
- reason: acceptQualityReason,
617
- ...(plan.ok && plan.artifactDigest ? { revision: plan.artifactDigest } : {}),
618
- }),
619
- });
630
+ acceptRes = await postAccept();
631
+ acceptData = await readJsonSafe(acceptRes);
632
+ // Images that could not be captured: confirm the exact count, then accept that count.
633
+ if (acceptData?.reason === "scope-required" && Number.isInteger(acceptData.unresolvedImageReferences)) {
634
+ const count = acceptData.unresolvedImageReferences;
635
+ const scope = await confirmPlan([
636
+ ` uncaptured images: ${count}${Number.isInteger(acceptData.totalImageReferences) ? ` of ${acceptData.totalImageReferences}` : ""} image reference(s)`,
637
+ ...UNCAPTURED_IMAGES_LINES,
638
+ ], { yes, question: `Publish with ${count} uncaptured image reference(s) as they are?` });
639
+ if (!scope.confirmed) {
640
+ console.log(" Ship cancelled — nothing changed.");
641
+ return scope.reason === "non-tty" ? 2 : 0;
642
+ }
643
+ acceptRes = await postAccept({ acceptedUnresolvedImageCount: count });
644
+ acceptData = await readJsonSafe(acceptRes);
645
+ }
620
646
  } catch (e) {
621
647
  console.error(
622
648
  fail(`couldn't record the quality acceptance at ${base}: ${String(e?.message || e)}`, "check --url / your network, then re-run"),
623
649
  );
624
650
  return 1;
625
651
  }
626
- const acceptData = await readJsonSafe(acceptRes);
627
652
  if (!acceptRes.ok || acceptData?.ok !== true) {
628
653
  console.error(
629
654
  fail(
@@ -637,6 +662,9 @@ export async function runShip(
637
662
  const count = Array.isArray(accepted.findingIds) ? accepted.findingIds.length : 0;
638
663
  const what = accepted.problemCode === "compliance-review" ? "compliance warning(s)" : "quality finding(s)";
639
664
  console.log(` ✓ ${count ? `${count} ` : ""}${what} accepted for this revision; continuing the publish.`);
665
+ if (Array.isArray(accepted.findingIds) && accepted.findingIds.some((id) => typeof id === "string" && id.startsWith("IMAGE:"))) {
666
+ for (const line of UNCAPTURED_IMAGES_LINES) console.log(line);
667
+ }
640
668
  }
641
669
 
642
670
  // Each approved space goes live through its own reviewed transition, under the confirm above.