@tokenoftrust/cli 2.0.25 → 2.0.26
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/ship.mjs +82 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.26",
|
|
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/ship.mjs
CHANGED
|
@@ -36,11 +36,9 @@
|
|
|
36
36
|
* `promote_failed` / `record_failed` (a partial failure, recoverable by
|
|
37
37
|
* re-running `tot ship`, never a false live claim).
|
|
38
38
|
*
|
|
39
|
-
* TRANSPORT:
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* `tot ship <N> --tenant` operator-by-PR path used. No MCP client, no git: ship
|
|
43
|
-
* is now pure HTTP (dependency-free — global `fetch` only).
|
|
39
|
+
* TRANSPORT: operators/CI may use a Bearer operator secret. A signed-in owner or
|
|
40
|
+
* developer instead mints a storefront viewer session from their OWN `tot login`
|
|
41
|
+
* identity; the server still enforces the live owner / `ship-on-behalf` grant.
|
|
44
42
|
*/
|
|
45
43
|
import { fail } from "../errors.mjs";
|
|
46
44
|
import { planForAction, printPlanAndConfirm } from "../plan.mjs";
|
|
@@ -52,6 +50,7 @@ import { emitActivity } from "../activity.mjs";
|
|
|
52
50
|
// sees WHICH changes are keeping the aggregate from going green without a separate
|
|
53
51
|
// `tot preview doctor` run.
|
|
54
52
|
import { autoSurfaceDoctor } from "../merge-doctor-report.mjs";
|
|
53
|
+
import { resolveViewerTransport } from "../viewer-session.mjs";
|
|
55
54
|
|
|
56
55
|
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
57
56
|
|
|
@@ -62,10 +61,15 @@ const USAGE = `tot ship — publish the tenant's CURRENT GREEN AGGREGATE to live
|
|
|
62
61
|
tot ship --yes confirm non-interactively (skip the [y/N] prompt)
|
|
63
62
|
tot ship --url <origin> storefront origin (default: env TOT_STOREFRONT_URL)
|
|
64
63
|
tot ship --secret <s> operator secret (prefer the env vars below)
|
|
64
|
+
tot ship --accept-quality-findings <reason>
|
|
65
|
+
accept this revision's overridable quality findings,
|
|
66
|
+
record the required reason, then continue publishing
|
|
65
67
|
tot ship --no-open don't open the live URL in your browser
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
Authentication: a signed-in owner/developer with a live ship-on-behalf grant
|
|
70
|
+
can use their own \`tot login\` session. Operators/CI may instead provide a
|
|
71
|
+
secret via PREVIEW_RECONCILE_SECRET, GRANTS_ADMIN_SECRET,
|
|
72
|
+
TOT_OPERATOR_SECRET, or --secret.
|
|
69
73
|
|
|
70
74
|
\`tot ship\` has ONE meaning: it publishes the tenant's shared preview
|
|
71
75
|
aggregate — the batch of PRs that integrated cleanly and went GREEN — to
|
|
@@ -88,6 +92,7 @@ export function parseShipArgs(argv) {
|
|
|
88
92
|
noOpen: false,
|
|
89
93
|
url: null,
|
|
90
94
|
secret: null,
|
|
95
|
+
acceptQualityReason: null,
|
|
91
96
|
help: false,
|
|
92
97
|
};
|
|
93
98
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -97,6 +102,7 @@ export function parseShipArgs(argv) {
|
|
|
97
102
|
else if (t === "--no-open") a.noOpen = true;
|
|
98
103
|
else if (t === "--url") a.url = argv[++i];
|
|
99
104
|
else if (t === "--secret") a.secret = argv[++i];
|
|
105
|
+
else if (t === "--accept-quality-findings") a.acceptQualityReason = argv[++i];
|
|
100
106
|
else if (t === "--help" || t === "-h") a.help = true;
|
|
101
107
|
}
|
|
102
108
|
return a;
|
|
@@ -305,32 +311,46 @@ async function readJsonSafe(res) {
|
|
|
305
311
|
* injected so it's unit-tested with no live network/TTY.
|
|
306
312
|
*
|
|
307
313
|
* @param {{ tenant:string, secret:string, storefrontUrl?:string|null,
|
|
308
|
-
* yes?:boolean, noOpen?:boolean
|
|
314
|
+
* yes?:boolean, noOpen?:boolean, acceptQualityReason?:string|null,
|
|
315
|
+
* env?:NodeJS.ProcessEnv }} params
|
|
309
316
|
* @param {{ fetch?:typeof fetch, confirmPlan?:typeof printPlanAndConfirm,
|
|
310
|
-
* openUrl?:(u:string)=>boolean, progress?:boolean
|
|
317
|
+
* openUrl?:(u:string)=>boolean, progress?:boolean,
|
|
318
|
+
* resolveViewerTransport?:typeof resolveViewerTransport }} [deps]
|
|
311
319
|
* @returns {Promise<number>} process exit code
|
|
312
320
|
*/
|
|
313
|
-
export async function runShip(
|
|
321
|
+
export async function runShip(
|
|
322
|
+
{
|
|
323
|
+
tenant,
|
|
324
|
+
secret,
|
|
325
|
+
storefrontUrl = null,
|
|
326
|
+
yes = false,
|
|
327
|
+
noOpen = false,
|
|
328
|
+
acceptQualityReason = null,
|
|
329
|
+
env = process.env,
|
|
330
|
+
},
|
|
331
|
+
deps = {},
|
|
332
|
+
) {
|
|
314
333
|
const fetchImpl = deps.fetch || globalThis.fetch;
|
|
315
334
|
const confirmPlan = deps.confirmPlan || printPlanAndConfirm;
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
335
|
+
const resolveViewer = deps.resolveViewerTransport || resolveViewerTransport;
|
|
336
|
+
let base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
|
|
337
|
+
let authHeaders;
|
|
338
|
+
if (secret) {
|
|
339
|
+
authHeaders = {
|
|
340
|
+
authorization: `Bearer ${secret}`,
|
|
341
|
+
"x-tot-owner": tenant,
|
|
342
|
+
"x-tot-capability": "ship-on-behalf",
|
|
343
|
+
};
|
|
344
|
+
} else {
|
|
345
|
+
const viewer = await resolveViewer({ tenant, env, fetchImpl, storefrontUrl });
|
|
346
|
+
if (!viewer.ok) {
|
|
347
|
+
console.error(fail(viewer.message, viewer.hint));
|
|
348
|
+
return 2;
|
|
349
|
+
}
|
|
350
|
+
base = viewer.base;
|
|
351
|
+
authHeaders = viewer.authHeaders;
|
|
326
352
|
}
|
|
327
353
|
|
|
328
|
-
const authHeaders = {
|
|
329
|
-
authorization: `Bearer ${secret}`,
|
|
330
|
-
"x-tot-owner": tenant,
|
|
331
|
-
"x-tot-capability": "ship-on-behalf",
|
|
332
|
-
};
|
|
333
|
-
|
|
334
354
|
// Auto-surface the merge-doctor on a refused/failed ship. Best-effort:
|
|
335
355
|
// fetches the hosted doctor over the SAME operator transport and prints the COMPACT
|
|
336
356
|
// summary; never throws and never touches the exit code. A clean/unreachable doctor
|
|
@@ -387,6 +407,10 @@ export async function runShip({ tenant, secret, storefrontUrl = null, yes = fals
|
|
|
387
407
|
paywall: plan.paywall,
|
|
388
408
|
targets: { live: liveUrl },
|
|
389
409
|
});
|
|
410
|
+
if (acceptQualityReason) {
|
|
411
|
+
planLines.push(" quality findings: accept this exact revision and continue");
|
|
412
|
+
planLines.push(` acceptance reason: ${acceptQualityReason.trim() || "(missing)"}`);
|
|
413
|
+
}
|
|
390
414
|
const { confirmed, reason } = await confirmPlan(planLines, {
|
|
391
415
|
yes,
|
|
392
416
|
question: `Ship ${tenant}'s current green aggregate live?`,
|
|
@@ -405,6 +429,36 @@ export async function runShip({ tenant, secret, storefrontUrl = null, yes = fals
|
|
|
405
429
|
return 0;
|
|
406
430
|
}
|
|
407
431
|
|
|
432
|
+
// This is the same revision-bound acceptance as the Publish dialog, not a raw validator bypass.
|
|
433
|
+
// The server refuses it unless the current durable problem is quality-only and the reason is
|
|
434
|
+
// substantive. Only after that accountable record exists do we re-dispatch the release.
|
|
435
|
+
if (acceptQualityReason !== null) {
|
|
436
|
+
let acceptRes;
|
|
437
|
+
try {
|
|
438
|
+
acceptRes = await fetchImpl(`${base}/api/changes/accept-issue`, {
|
|
439
|
+
method: "POST",
|
|
440
|
+
headers: { "content-type": "application/json", ...authHeaders },
|
|
441
|
+
body: JSON.stringify({ operation: "publication", reason: acceptQualityReason }),
|
|
442
|
+
});
|
|
443
|
+
} catch (e) {
|
|
444
|
+
console.error(
|
|
445
|
+
fail(`couldn't record the quality acceptance at ${base}: ${String(e?.message || e)}`, "check --url / your network, then re-run"),
|
|
446
|
+
);
|
|
447
|
+
return 1;
|
|
448
|
+
}
|
|
449
|
+
const acceptData = await readJsonSafe(acceptRes);
|
|
450
|
+
if (!acceptRes.ok || acceptData?.ok !== true) {
|
|
451
|
+
console.error(
|
|
452
|
+
fail(
|
|
453
|
+
acceptData?.error || acceptData?.message || `quality acceptance was refused (HTTP ${acceptRes.status})`,
|
|
454
|
+
"review the current publish findings; only quality-only findings on this exact revision can be accepted",
|
|
455
|
+
),
|
|
456
|
+
);
|
|
457
|
+
return 1;
|
|
458
|
+
}
|
|
459
|
+
console.log(" ✓ quality findings accepted for this revision; continuing the publish.");
|
|
460
|
+
}
|
|
461
|
+
|
|
408
462
|
// 3. SHIP — the human go-live gate (confirmGoLive) plus the reviewed pin, so a
|
|
409
463
|
// STALE plan (the aggregate moved / the artifact changed since GET) is
|
|
410
464
|
// refused rather than shipping something nobody reviewed.
|
|
@@ -528,6 +582,8 @@ export async function run(argv, ctx) {
|
|
|
528
582
|
storefrontUrl,
|
|
529
583
|
yes: args.yes,
|
|
530
584
|
noOpen: args.noOpen,
|
|
585
|
+
acceptQualityReason: args.acceptQualityReason,
|
|
586
|
+
env,
|
|
531
587
|
},
|
|
532
588
|
{ openUrl: (u) => openBrowser(u) },
|
|
533
589
|
);
|