@tokenoftrust/cli 3.9.0 → 3.10.0
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/rollback.mjs +73 -8
- package/src/commands/ship.mjs +19 -0
- package/src/plan.mjs +8 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.10.0",
|
|
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",
|
|
@@ -39,6 +39,7 @@ import { fail } from "../errors.mjs";
|
|
|
39
39
|
import { scopeToTenant } from "../tenant-scope.mjs";
|
|
40
40
|
import { isInteractive, promptYesNo } from "../prompt.mjs";
|
|
41
41
|
import { startProgress } from "../progress.mjs";
|
|
42
|
+
import { resolveViewerTransport } from "../viewer-session.mjs";
|
|
42
43
|
|
|
43
44
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
44
45
|
|
|
@@ -179,19 +180,56 @@ export function rollbackCandidates(history, currentVersionId) {
|
|
|
179
180
|
.reverse();
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Ledger/CDN mismatch shown before a rollback, or null when they match.
|
|
185
|
+
* Null shows neither the warning nor a corrected target.
|
|
186
|
+
* @typedef {{ reason: string, live: string|null, served: string|null, correctedTarget: string } | null} RollbackLiveDrift
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
/** Lines naming a ledger/CDN mismatch. Empty when there is none.
|
|
190
|
+
* @param {RollbackLiveDrift} liveDrift
|
|
191
|
+
* @returns {string[]}
|
|
192
|
+
*/
|
|
193
|
+
export function liveDriftLines(liveDrift) {
|
|
194
|
+
if (!liveDrift?.reason || !liveDrift?.correctedTarget) return [];
|
|
195
|
+
return [
|
|
196
|
+
` ⚠ ${liveDrift.reason}: ledger live is ${liveDrift.live ?? "nothing"}; the CDN serves ${liveDrift.served ?? "an unknown revision"}.`,
|
|
197
|
+
` corrected rollback target: ${liveDrift.correctedTarget}`,
|
|
198
|
+
];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The ship plan's CDN drift warning, or null. A missing or matching plan is null —
|
|
203
|
+
* the list then shows neither the warning nor a corrected target.
|
|
204
|
+
* @param {any} body
|
|
205
|
+
* @returns {RollbackLiveDrift}
|
|
206
|
+
*/
|
|
207
|
+
export function liveDriftFromShipPlan(body) {
|
|
208
|
+
const warning = body?.warning;
|
|
209
|
+
const corrected = warning?.correctedRollbackTarget?.aggregateSha;
|
|
210
|
+
if (!warning || typeof warning.reason !== "string" || typeof corrected !== "string") return null;
|
|
211
|
+
return {
|
|
212
|
+
reason: warning.reason,
|
|
213
|
+
live: typeof warning.live === "string" ? warning.live : null,
|
|
214
|
+
served: typeof warning.served === "string" ? warning.served : null,
|
|
215
|
+
correctedTarget: corrected,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
182
219
|
/** Render the `tot rollback` (no args) history listing — the rollback-target
|
|
183
220
|
* picker, each entry annotated with its immutable `/rev/<sha>` link
|
|
184
221
|
* when a storefront origin is available. Pure — unit-tested.
|
|
185
222
|
* @param {{ tenant:string, target:string, current:any,
|
|
186
|
-
* candidates:any[], revisionBase?:string|null }} input
|
|
223
|
+
* candidates:any[], revisionBase?:string|null, liveDrift?: RollbackLiveDrift }} input
|
|
187
224
|
* @returns {string[]}
|
|
188
225
|
*/
|
|
189
|
-
export function renderHistory({ tenant, target, current, candidates, revisionBase }) {
|
|
226
|
+
export function renderHistory({ tenant, target, current, candidates, revisionBase, liveDrift = null }) {
|
|
190
227
|
const lines = ["", ` Promotion history for ${tenant} on "${target}":`];
|
|
191
228
|
if (current) {
|
|
192
229
|
const link = revisionUrl(revisionBase, tenant, current.versionId);
|
|
193
230
|
lines.push(` live now: ${current.versionId} (pinned tree ${current.digest})${link ? ` ${link}` : ""}`);
|
|
194
231
|
}
|
|
232
|
+
lines.push(...liveDriftLines(liveDrift));
|
|
195
233
|
if (!candidates.length) {
|
|
196
234
|
lines.push(" (no prior version recorded yet — nothing to roll back to)");
|
|
197
235
|
return lines;
|
|
@@ -207,11 +245,12 @@ export function renderHistory({ tenant, target, current, candidates, revisionBas
|
|
|
207
245
|
|
|
208
246
|
/** Render the "what goes live" block for a rollback preview. Pure — unit-tested.
|
|
209
247
|
* @param {{ tenant:string, target:string, toVersionId:string,
|
|
210
|
-
* preview:ReturnType<typeof normalizeRollbackPreview
|
|
248
|
+
* preview:ReturnType<typeof normalizeRollbackPreview>, liveDrift?: RollbackLiveDrift }} input
|
|
211
249
|
* @returns {string[]}
|
|
212
250
|
*/
|
|
213
|
-
export function renderRollbackPreview({ tenant, target, toVersionId, preview }) {
|
|
251
|
+
export function renderRollbackPreview({ tenant, target, toVersionId, preview, liveDrift = null }) {
|
|
214
252
|
const lines = ["", ` This rollback will change the LIVE site for ${tenant}:`];
|
|
253
|
+
lines.push(...liveDriftLines(liveDrift));
|
|
215
254
|
const current = /** @type {any} */ (preview.pointer?.current);
|
|
216
255
|
if (current) lines.push(` ${target}: ${current.versionId} → ${toVersionId}`);
|
|
217
256
|
else lines.push(` ${target}: → ${toVersionId}`);
|
|
@@ -231,12 +270,21 @@ export function renderRollbackPreview({ tenant, target, toVersionId, preview })
|
|
|
231
270
|
* the listing's `/rev/<sha>` links resolve against; omit to list
|
|
232
271
|
* without links.
|
|
233
272
|
* @param {{ interactive?:()=>boolean, confirm?:(q:string,d:boolean)=>Promise<boolean>,
|
|
234
|
-
* progress?:boolean }} [deps]
|
|
273
|
+
* progress?:boolean, readLiveDrift?:()=>Promise<ReturnType<typeof liveDriftFromShipPlan>> }} [deps]
|
|
235
274
|
* @returns {Promise<number>} process exit code
|
|
236
275
|
*/
|
|
237
276
|
export async function runRollback(scoped, { tenant, target, toVersionId, revisionBase }, deps = {}) {
|
|
238
277
|
const interactive = deps.interactive || isInteractive;
|
|
239
278
|
const confirm = deps.confirm || promptYesNo;
|
|
279
|
+
/** @type {RollbackLiveDrift} */
|
|
280
|
+
let liveDrift = null;
|
|
281
|
+
if (deps.readLiveDrift) {
|
|
282
|
+
try {
|
|
283
|
+
liveDrift = await deps.readLiveDrift();
|
|
284
|
+
} catch {
|
|
285
|
+
liveDrift = null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
240
288
|
|
|
241
289
|
// 0. No target given — list promotion history as the set of rollback targets.
|
|
242
290
|
if (!toVersionId) {
|
|
@@ -262,7 +310,7 @@ export async function runRollback(scoped, { tenant, target, toVersionId, revisio
|
|
|
262
310
|
return 1;
|
|
263
311
|
}
|
|
264
312
|
const candidates = rollbackCandidates(status.history, /** @type {any} */ (status).current?.versionId ?? null);
|
|
265
|
-
for (const line of renderHistory({ tenant, target, current: status.current, candidates, revisionBase })) {
|
|
313
|
+
for (const line of renderHistory({ tenant, target, current: status.current, candidates, revisionBase, liveDrift })) {
|
|
266
314
|
console.log(line);
|
|
267
315
|
}
|
|
268
316
|
return 0;
|
|
@@ -297,7 +345,7 @@ export async function runRollback(scoped, { tenant, target, toVersionId, revisio
|
|
|
297
345
|
}
|
|
298
346
|
|
|
299
347
|
// 2. ALWAYS render what would change, then require ONE explicit y/N confirm.
|
|
300
|
-
for (const line of renderRollbackPreview({ tenant, target, toVersionId, preview })) console.log(line);
|
|
348
|
+
for (const line of renderRollbackPreview({ tenant, target, toVersionId, preview, liveDrift })) console.log(line);
|
|
301
349
|
|
|
302
350
|
// NON-TTY: refuse rather than auto-confirm — nothing rolls back without a human yes.
|
|
303
351
|
if (!interactive()) {
|
|
@@ -354,6 +402,21 @@ function reportRolledBack(result, { tenant, target, toVersionId }) {
|
|
|
354
402
|
return 0;
|
|
355
403
|
}
|
|
356
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Best-effort read of the ship plan's CDN drift. A refusal, a network fault, or a
|
|
407
|
+
* matching ledger returns null — the list still renders, and it shows neither a
|
|
408
|
+
* warning nor a corrected target.
|
|
409
|
+
* @param {{ tenant: string, env: NodeJS.ProcessEnv, fetchImpl?: typeof fetch }} args
|
|
410
|
+
*/
|
|
411
|
+
async function readShipPlanDrift({ tenant, env, fetchImpl = globalThis.fetch }) {
|
|
412
|
+
const storefrontUrl = env.TOT_STOREFRONT_URL || env.STOREFRONT_BASE_URL || null;
|
|
413
|
+
const viewer = await resolveViewerTransport({ tenant, env, fetchImpl, storefrontUrl });
|
|
414
|
+
if (!viewer.ok) return null;
|
|
415
|
+
const res = await fetchImpl(`${viewer.base}/api/changes/ship`, { method: "GET", headers: viewer.authHeaders });
|
|
416
|
+
if (!res.ok) return null;
|
|
417
|
+
return liveDriftFromShipPlan(await res.json());
|
|
418
|
+
}
|
|
419
|
+
|
|
357
420
|
/**
|
|
358
421
|
* @param {string[]} argv
|
|
359
422
|
* @param {any} ctx
|
|
@@ -386,7 +449,9 @@ export async function run(argv, ctx) {
|
|
|
386
449
|
await establishSession(client, { env, prefer: args.identity || undefined });
|
|
387
450
|
const scoped = await scopeToTenant(client, tenant);
|
|
388
451
|
|
|
389
|
-
return await runRollback(scoped, { tenant, target, toVersionId: args.versionId, revisionBase }
|
|
452
|
+
return await runRollback(scoped, { tenant, target, toVersionId: args.versionId, revisionBase }, {
|
|
453
|
+
readLiveDrift: () => readShipPlanDrift({ tenant, env }),
|
|
454
|
+
});
|
|
390
455
|
} catch (e) {
|
|
391
456
|
if (e instanceof AuthUnavailableError) {
|
|
392
457
|
console.error(fail("sign in to roll back", e.hint || "run `tot login`, then re-run `tot rollback`"));
|
package/src/commands/ship.mjs
CHANGED
|
@@ -245,6 +245,23 @@ export function liveUrlFor(tenant) {
|
|
|
245
245
|
* paywall:{allowed:boolean,message:string|null}, alreadyShipped:boolean} |
|
|
246
246
|
* {ok:false, reason:string, message:string}}
|
|
247
247
|
*/
|
|
248
|
+
/** The ship-plan CDN drift warning, or null when the ledger and the CDN name the same release. */
|
|
249
|
+
function normalizeLiveDrift(warning) {
|
|
250
|
+
if (!warning || typeof warning !== "object" || typeof warning.reason !== "string") return null;
|
|
251
|
+
const target = warning.correctedRollbackTarget;
|
|
252
|
+
if (!target || typeof target !== "object" || typeof target.aggregateSha !== "string") return null;
|
|
253
|
+
return {
|
|
254
|
+
reason: warning.reason,
|
|
255
|
+
live: typeof warning.live === "string" ? warning.live : null,
|
|
256
|
+
served: typeof warning.served === "string" ? warning.served : null,
|
|
257
|
+
correctedRollbackTarget: {
|
|
258
|
+
receiptId: typeof target.receiptId === "string" ? target.receiptId : "",
|
|
259
|
+
aggregateSha: target.aggregateSha,
|
|
260
|
+
artifactDigest: typeof target.artifactDigest === "string" ? target.artifactDigest : "",
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
248
265
|
export function normalizeShipPlan(data) {
|
|
249
266
|
const o = data && typeof data === "object" ? data : {};
|
|
250
267
|
if (o.ok === true) {
|
|
@@ -257,6 +274,7 @@ export function normalizeShipPlan(data) {
|
|
|
257
274
|
integrationRunId: typeof o.integrationRunId === "string" ? o.integrationRunId : null,
|
|
258
275
|
receiptId: typeof o.receiptId === "string" ? o.receiptId : null,
|
|
259
276
|
rollbackTarget: o.rollbackTarget && typeof o.rollbackTarget === "object" ? o.rollbackTarget : null,
|
|
277
|
+
warning: normalizeLiveDrift(o.warning),
|
|
260
278
|
paywall:
|
|
261
279
|
o.paywall && typeof o.paywall === "object"
|
|
262
280
|
? {
|
|
@@ -546,6 +564,7 @@ export async function runShip(
|
|
|
546
564
|
artifactDigest: plan.artifactDigest,
|
|
547
565
|
includedPrs: plan.includedPrs,
|
|
548
566
|
rollbackTarget: plan.rollbackTarget,
|
|
567
|
+
liveDrift: plan.warning,
|
|
549
568
|
paywall: plan.paywall,
|
|
550
569
|
targets: { live: liveUrl },
|
|
551
570
|
})
|
package/src/plan.mjs
CHANGED
|
@@ -59,6 +59,7 @@ function targetLabel({ pr, changeId }) {
|
|
|
59
59
|
* artifactDigest?: string|null,
|
|
60
60
|
* includedPrs?: Array<{ prNumber?: number|null, changeId?: string|null, headSha?: string|null }>|null,
|
|
61
61
|
* rollbackTarget?: { receiptId: string, aggregateSha: string } | null,
|
|
62
|
+
* liveDrift?: { reason: string, live?: string|null, served?: string|null } | null,
|
|
62
63
|
* paywall?: { allowed: boolean, message?: string|null } | null,
|
|
63
64
|
* refs?: Array<{ ref: string, sha?: string|null, reason?: string|null }>|null,
|
|
64
65
|
* bypassedPrs?: Array<{ prNumber?: number|null, changeId?: string|null, headSha?: string|null }>|null,
|
|
@@ -80,6 +81,7 @@ export function planForAction({
|
|
|
80
81
|
artifactDigest = null,
|
|
81
82
|
includedPrs = null,
|
|
82
83
|
rollbackTarget = null,
|
|
84
|
+
liveDrift = null,
|
|
83
85
|
paywall = null,
|
|
84
86
|
refs = null,
|
|
85
87
|
bypassedPrs = null,
|
|
@@ -135,9 +137,14 @@ export function planForAction({
|
|
|
135
137
|
lines.push(` - ${prLabel}${shortSha}`);
|
|
136
138
|
}
|
|
137
139
|
}
|
|
140
|
+
if (liveDrift?.reason) {
|
|
141
|
+
lines.push(
|
|
142
|
+
` ⚠ ${liveDrift.reason}: the CDN serves ${liveDrift.served ?? "an unknown revision"}, not the ledger's live ${liveDrift.live ?? "nothing"}.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
138
145
|
lines.push(
|
|
139
146
|
rollbackTarget
|
|
140
|
-
? ` rollback to: ${rollbackTarget.aggregateSha} (receipt ${rollbackTarget.receiptId})`
|
|
147
|
+
? ` rollback to: ${rollbackTarget.aggregateSha} (receipt ${rollbackTarget.receiptId})${liveDrift?.reason ? " — corrected target" : ""}`
|
|
141
148
|
: " rollback to: (none — first-ever ship)",
|
|
142
149
|
);
|
|
143
150
|
if (paywall && paywall.allowed === false) {
|