@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "2.0.25",
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",
@@ -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: the CLI holds no storefront cookie (its OAuth login is with the
40
- * MCP, a different trust boundary), so `/api/changes/ship` is reached with a
41
- * Bearer OPERATOR SECRET + `X-Tot-Owner` — the SAME transport the retired
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
- Operator secret (from env, first found): PREVIEW_RECONCILE_SECRET,
68
- GRANTS_ADMIN_SECRET, TOT_OPERATOR_SECRET (or pass --secret).
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 }} params
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 }} [deps]
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({ tenant, secret, storefrontUrl = null, yes = false, noOpen = false }, deps = {}) {
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 base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
317
-
318
- if (!secret) {
319
- console.error(
320
- fail(
321
- "shipping is an OPERATOR action — it needs an operator secret",
322
- "set PREVIEW_RECONCILE_SECRET (or GRANTS_ADMIN_SECRET / TOT_OPERATOR_SECRET), or pass --secret",
323
- ),
324
- );
325
- return 2;
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
  );