@tokenoftrust/cli 5.0.1 → 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 +1 -1
- package/src/commands/go-live.mjs +63 -2
- package/src/commands/ship.mjs +38 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "
|
|
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",
|
package/src/commands/go-live.mjs
CHANGED
|
@@ -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
|
}
|
package/src/commands/ship.mjs
CHANGED
|
@@ -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
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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.
|