@tokenoftrust/cli 1.4.1 → 1.5.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.
Files changed (53) hide show
  1. package/README.md +5 -0
  2. package/bin/tot.mjs +58 -79
  3. package/package.json +6 -1
  4. package/src/activity.mjs +15 -14
  5. package/src/app-scaffold.mjs +4 -4
  6. package/src/auth.mjs +13 -5
  7. package/src/candidate-state.mjs +3 -3
  8. package/src/commands/accept.mjs +65 -38
  9. package/src/commands/app/dev.mjs +8 -4
  10. package/src/commands/app/index.mjs +3 -3
  11. package/src/commands/app/scaffold.mjs +1 -1
  12. package/src/commands/branches.mjs +4 -3
  13. package/src/commands/cleanup.mjs +7 -11
  14. package/src/commands/clone.mjs +23 -20
  15. package/src/commands/dev.mjs +42 -24
  16. package/src/commands/doctor.mjs +4 -4
  17. package/src/commands/git-credential.mjs +2 -2
  18. package/src/commands/go-live.mjs +9 -5
  19. package/src/commands/grants.mjs +7 -5
  20. package/src/commands/hotfix.mjs +1 -1
  21. package/src/commands/ideas.mjs +2 -2
  22. package/src/commands/link.mjs +2 -2
  23. package/src/commands/login.mjs +5 -6
  24. package/src/commands/pr.mjs +33 -19
  25. package/src/commands/preview-build.mjs +6 -6
  26. package/src/commands/preview-doctor.mjs +225 -0
  27. package/src/commands/preview-retry-evidence.mjs +156 -0
  28. package/src/commands/preview.mjs +19 -3
  29. package/src/commands/revert.mjs +5 -5
  30. package/src/commands/rollback.mjs +18 -16
  31. package/src/commands/ship.mjs +29 -12
  32. package/src/commands/start.mjs +61 -51
  33. package/src/commands/submit.mjs +360 -50
  34. package/src/commands/sync.mjs +2 -2
  35. package/src/commands/validate.mjs +4 -3
  36. package/src/commands/whoami.mjs +1 -1
  37. package/src/dev-heartbeat.mjs +3 -2
  38. package/src/dev-logs.mjs +2 -2
  39. package/src/errors.mjs +11 -4
  40. package/src/git-credential.mjs +94 -21
  41. package/src/last-tenant.mjs +1 -1
  42. package/src/mcp.mjs +6 -1
  43. package/src/merge-doctor-report.mjs +208 -0
  44. package/src/oauth.mjs +18 -14
  45. package/src/obstacle-beacon.cjs +2 -2
  46. package/src/obstacle.mjs +1 -1
  47. package/src/plan.mjs +16 -21
  48. package/src/sample.mjs +4 -4
  49. package/src/validate.mjs +135 -15
  50. package/src/vendor/private-apps-devkit.mjs +3 -3
  51. package/src/viewer-session.mjs +118 -0
  52. package/template/private-app/README.md +12 -6
  53. package/src/commands/retire.mjs +0 -203
@@ -0,0 +1,225 @@
1
+ /**
2
+ * `tot preview doctor --tenant <t>` — the merge self-serve DIAGNOSIS in the terminal.
3
+ *
4
+ * Calls the hosted merge-doctor seam `GET /<tenant>/api/preview/merge-doctor`
5
+ * — which composes the SAME two server-side reads the admin Publish tab builds
6
+ * (`GET /api/changes` + `GET /api/changes/branches`) through the PURE `analyzeMerge`
7
+ * taxonomy — and prints its `{ scopeKnown, verdict, findings, counts }` as the compact,
8
+ * ordered (blocker → warn → info) report, each finding carrying who-owns-it and its
9
+ * remedy/action. It composes NO per-PR forge calls of its own: one GET, one answer.
10
+ *
11
+ * READ-ONLY — it mutates nothing, so there is NO plan/confirm gate (unlike `tot
12
+ * accept` / `tot preview build`). It just answers "what's blocking my merges, and
13
+ * what do I do about it?" in seconds.
14
+ *
15
+ * AUTH — the endpoint is session-gated exactly like `GET /api/changes`, so this
16
+ * reaches it by EITHER of the two transports `tot accept` already speaks:
17
+ * - OPERATOR SECRET (operators / headless CI): Bearer + `X-Tot-Owner` on the
18
+ * generic storefront origin (from PREVIEW_RECONCILE_SECRET / GRANTS_ADMIN_SECRET
19
+ * / TOT_OPERATOR_SECRET, or `--secret`).
20
+ * - VIEWER SESSION (an invited developer with no secret): mint a `tot_session`
21
+ * from their own `tot login` on the tenant's own host (`resolveViewerTransport`).
22
+ * A caller who cannot see the accept queue cannot get a diagnosis of it — an auth
23
+ * refusal (401/403) is relayed as a clean house-style failure, not a wrapped verdict.
24
+ *
25
+ * EXIT CODE — mirrors the `scripts/tenant/gitea-merge-doctor.mjs` driver: a report
26
+ * with any blocker exits 1 (so a CI gate fails on a blocked merge), otherwise 0.
27
+ *
28
+ * The rendering helpers are the SHARED port in `../merge-doctor-report.mjs` (a
29
+ * faithful PORT of the analyzer's own `formatReport`/`attentionBanner`/
30
+ * `supportContext` — mergeDoctor.ts §render / the gitea-merge-doctor.mjs mirror);
31
+ * this package is the dependency-free published `@tokenoftrust/cli`, so it cannot
32
+ * import the app/scripts source, and the port is kept in sync with that taxonomy by
33
+ * hand. They live in their own module so `tot accept` / `tot ship` can reuse the same
34
+ * fetch + compact render without a cycle back through this command's transports.
35
+ *
36
+ * Dependency-free (global fetch + the shared viewer/operator transports).
37
+ */
38
+ import { fail } from "../errors.mjs";
39
+ import { resolveOperatorSecret } from "./ship.mjs";
40
+ import { resolveViewerTransport } from "../viewer-session.mjs";
41
+ import {
42
+ DOCTOR_PATH,
43
+ normalizeAnalysis,
44
+ attentionBanner,
45
+ supportContext,
46
+ formatReport,
47
+ } from "../merge-doctor-report.mjs";
48
+
49
+ const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
50
+
51
+ const USAGE = `tot preview doctor — diagnose what's blocking this tenant's merges
52
+
53
+ tot preview doctor diagnose the current checkout's tenant
54
+ tot preview doctor --tenant <t> diagnose <t> (works without a checkout)
55
+ tot preview doctor --json emit the raw { scopeKnown, verdict, findings, counts }
56
+
57
+ Calls the hosted merge-doctor (GET /api/preview/merge-doctor), which composes the
58
+ SAME reads the admin Publish tab builds and runs them through the shared analyzer,
59
+ and prints the ordered report: blockers first, then warnings, then info — each
60
+ finding names who resolves it (you / on-us / housekeeping) and the exact remedy.
61
+ Read-only: it mutates nothing and needs no confirmation.
62
+
63
+ Exit code: 1 if the report has any blocker (so it can gate CI), else 0.
64
+
65
+ Options:
66
+ --tenant <appDomain> Target tenant (e.g. tokenoftrust.com). Defaults to the
67
+ current checkout's tenant when run inside one.
68
+ --url <origin> Storefront origin for the operator-secret path
69
+ (default: env TOT_STOREFRONT_URL or ${DEFAULT_STOREFRONT_URL}).
70
+ --secret <s> Operator secret (prefer the env vars below).
71
+ --json Print the raw analysis JSON instead of the report.
72
+ --help, -h Show this help.
73
+
74
+ Operator secret (from env, first found): PREVIEW_RECONCILE_SECRET,
75
+ GRANTS_ADMIN_SECRET, TOT_OPERATOR_SECRET (or pass --secret). Without one, an
76
+ invited developer's own \`tot login\` session is used.`;
77
+
78
+ /** Parse `tot preview doctor` argv. Pure — unit-testable. */
79
+ export function parsePreviewDoctorArgs(argv) {
80
+ const a = { tenant: null, url: null, secret: null, json: false, help: false };
81
+ for (let i = 0; i < argv.length; i++) {
82
+ const t = argv[i];
83
+ if (t === "--tenant") a.tenant = argv[++i];
84
+ else if (t === "--url") a.url = argv[++i];
85
+ else if (t === "--secret") a.secret = argv[++i];
86
+ else if (t === "--json") a.json = true;
87
+ else if (t === "--help" || t === "-h") a.help = true;
88
+ }
89
+ return a;
90
+ }
91
+
92
+ // The render/normalize helpers (normalizeAnalysis / attentionBanner /
93
+ // supportContext / formatReport) are the shared port in ../merge-doctor-report.mjs,
94
+ // imported at the top of this file.
95
+
96
+ // ── run ────────────────────────────────────────────────────────────────────────
97
+
98
+ /**
99
+ * Fetch + render the merge-doctor report for one tenant. Resolves the SAME two
100
+ * transports `tot accept` uses (operator secret → generic origin + X-Tot-Owner;
101
+ * else viewer session → tenant host + cookie), GETs the hosted seam, and prints the
102
+ * ordered report (or raw JSON). `fetch`/`resolveViewerTransport` are injected so it
103
+ * is unit-tested with no network. Returns the process exit code (blocker → 1).
104
+ *
105
+ * @param {{ tenant:string, secret:string, storefrontUrl?:string|null,
106
+ * json?:boolean, env?:NodeJS.ProcessEnv }} params
107
+ * @param {{ fetch?:typeof fetch, resolveViewerTransport?:typeof resolveViewerTransport }} [deps]
108
+ * @returns {Promise<number>}
109
+ */
110
+ export async function runDoctor(
111
+ { tenant, secret, storefrontUrl = null, json = false, env = process.env },
112
+ deps = {},
113
+ ) {
114
+ const fetchImpl = deps.fetch || globalThis.fetch;
115
+ const resolveViewer = deps.resolveViewerTransport || resolveViewerTransport;
116
+
117
+ // Resolve the transport — operator secret preferred, else the developer's own
118
+ // viewer session on the tenant host.
119
+ let base;
120
+ let authHeaders;
121
+ if (secret) {
122
+ base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
123
+ authHeaders = {
124
+ authorization: `Bearer ${secret}`,
125
+ "x-tot-owner": tenant,
126
+ "x-tot-capability": "ship-on-behalf",
127
+ };
128
+ } else {
129
+ const viewer = await resolveViewer({ tenant, env, fetchImpl });
130
+ if (!viewer.ok) {
131
+ console.error(fail(viewer.message, viewer.hint));
132
+ return 2;
133
+ }
134
+ base = viewer.base;
135
+ authHeaders = viewer.authHeaders;
136
+ }
137
+
138
+ let res;
139
+ try {
140
+ res = await fetchImpl(`${base}${DOCTOR_PATH}`, { method: "GET", headers: authHeaders });
141
+ } catch (e) {
142
+ console.error(
143
+ fail(
144
+ `couldn't reach the merge-doctor at ${base}: ${String(e?.message || e)}`,
145
+ "check --url / your network, then re-run",
146
+ ),
147
+ );
148
+ return 1;
149
+ }
150
+
151
+ let data = {};
152
+ try {
153
+ data = await res.json();
154
+ } catch {
155
+ /* non-JSON / empty body */
156
+ }
157
+
158
+ // Session gating: the endpoint relays the queue read's auth refusal verbatim, so a
159
+ // caller who cannot see the accept queue cannot diagnose it.
160
+ if (res.status === 401 || res.status === 403) {
161
+ console.error(
162
+ fail(
163
+ `not authorized to diagnose ${tenant}'s merges: ${data?.error || `HTTP ${res.status}`}`,
164
+ "you need a live ship-on-behalf grant on this tenant (ask the store owner), or an operator secret authorised for it — then re-run",
165
+ ),
166
+ );
167
+ return 2;
168
+ }
169
+
170
+ if (!res.ok) {
171
+ console.error(
172
+ fail(
173
+ `the merge-doctor did not answer for ${tenant}: ${data?.error || `HTTP ${res.status}`}`,
174
+ "check --tenant / --url, then re-run",
175
+ ),
176
+ );
177
+ return 1;
178
+ }
179
+
180
+ const analysis = normalizeAnalysis(data);
181
+
182
+ if (json) {
183
+ console.log(JSON.stringify(analysis, null, 2));
184
+ } else {
185
+ console.log(formatReport(analysis));
186
+ }
187
+
188
+ // Mirror the scripts driver: any blocker fails the run so a CI gate catches it.
189
+ return analysis.counts.blocker > 0 ? 1 : 0;
190
+ }
191
+
192
+ /**
193
+ * @param {string[]} argv
194
+ * @param {any} ctx — detected CLI context (ctx.tenant when in a checkout)
195
+ */
196
+ export async function run(argv, ctx) {
197
+ const env = process.env;
198
+ const args = parsePreviewDoctorArgs(argv);
199
+ if (args.help) {
200
+ console.log(USAGE);
201
+ return 0;
202
+ }
203
+
204
+ const tenant = (args.tenant || ctx?.tenant || "").trim();
205
+ if (!tenant) {
206
+ console.error(
207
+ fail(
208
+ "no target tenant.",
209
+ "pass --tenant <appDomain> (e.g. --tenant tokenoftrust.com), or run inside a store checkout.",
210
+ ),
211
+ );
212
+ return 2;
213
+ }
214
+
215
+ const storefrontUrl =
216
+ args.url || env.TOT_STOREFRONT_URL || env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL;
217
+
218
+ return await runDoctor({
219
+ tenant,
220
+ secret: resolveOperatorSecret(args.secret, env),
221
+ storefrontUrl,
222
+ json: args.json,
223
+ env,
224
+ });
225
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * `tot preview retry-evidence --tenant <t> --pr <N>` — OPERATOR recovery verb.
3
+ *
4
+ * Re-push evidence for an ALREADY-BUILT candidate whose `change_report_evidence`
5
+ * push to MCP failed (surfaced as `evidence.pushFailed` on the candidate — see
6
+ * `formatOperatorCandidateLine`'s `[evidence push failed]` tag in `tot pr list
7
+ * --tenant`). Calls the session-authenticated `POST
8
+ * /api/changes/candidate-evidence-retry`, which recomputes + re-pushes evidence
9
+ * for the candidate's EXISTING version — no rebuild, no new content, safe to
10
+ * run repeatedly.
11
+ *
12
+ * DISTINCT FROM `tot preview build`: build re-runs the full reconcile against a
13
+ * head sha (for an orphaned/never-built PR). This targets an ALREADY-ready
14
+ * candidate whose content is fine but whose evidence never reached MCP — a
15
+ * narrower, cheaper fix for fb-1787938036559-ppmu4e.
16
+ *
17
+ * AUTH — same headless Bearer-operator-secret path as `tot preview build`:
18
+ * `Authorization: Bearer <secret>`, `X-Tot-Owner: <tenant>`,
19
+ * `X-Tot-Capability: ship-on-behalf`.
20
+ *
21
+ * Non-destructive + idempotent — no confirmation prompt.
22
+ *
23
+ * Dependency-free (global fetch).
24
+ */
25
+ const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
26
+
27
+ /** Parse `tot preview retry-evidence` argv. Pure — unit-testable. */
28
+ export function parseRetryEvidenceArgs(argv) {
29
+ const a = { tenant: null, pr: null, changeId: null, url: null, secret: null, help: false };
30
+ for (let i = 0; i < argv.length; i++) {
31
+ const t = argv[i];
32
+ if (t === "--tenant") a.tenant = argv[++i];
33
+ else if (t === "--pr") a.pr = argv[++i];
34
+ else if (t === "--change-id") a.changeId = argv[++i];
35
+ else if (t === "--url") a.url = argv[++i];
36
+ else if (t === "--secret") a.secret = argv[++i];
37
+ else if (t === "--help" || t === "-h") a.help = true;
38
+ }
39
+ return a;
40
+ }
41
+
42
+ export function renderUsage() {
43
+ return `tot preview retry-evidence — operator recovery: re-push a candidate's evidence to MCP
44
+
45
+ Usage:
46
+ tot preview retry-evidence --tenant <appDomain> --pr <N> [options]
47
+ tot preview retry-evidence --tenant <appDomain> --change-id <id> [options]
48
+
49
+ Re-computes and re-pushes evidence for an ALREADY-BUILT candidate whose MCP
50
+ push failed (shown as "[evidence push failed]" in \`tot pr list --tenant\`).
51
+ Does NOT rebuild or change content — safe to run repeatedly.
52
+
53
+ Options:
54
+ --tenant <appDomain> Target tenant (e.g. tokenoftrust.com). Defaults to the
55
+ current checkout's tenant when run inside one.
56
+ --pr <N> PR number to retry evidence for.
57
+ --change-id <id> Optional explicit candidate id (defaults to pr-<N>).
58
+ --url <origin> Storefront origin. Defaults to $TOT_STOREFRONT_URL or
59
+ ${DEFAULT_STOREFRONT_URL}.
60
+ --secret <s> Operator secret. Prefer the env vars below.
61
+ --help, -h Show this help.
62
+
63
+ Auth (operator secret, from env, first found):
64
+ PREVIEW_RECONCILE_SECRET, GRANTS_ADMIN_SECRET, TOT_OPERATOR_SECRET`;
65
+ }
66
+
67
+ /**
68
+ * @param {string[]} argv
69
+ * @param {any} ctx — detected CLI context (ctx.tenant when in a checkout)
70
+ */
71
+ export async function run(argv, ctx) {
72
+ const args = parseRetryEvidenceArgs(argv);
73
+ if (args.help) {
74
+ console.log(renderUsage());
75
+ return 0;
76
+ }
77
+
78
+ const base = (args.url || process.env.TOT_STOREFRONT_URL || process.env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL)
79
+ .trim()
80
+ .replace(/\/+$/, "");
81
+
82
+ const tenant = (args.tenant || ctx?.tenant || "").trim();
83
+ if (!tenant) {
84
+ console.error(
85
+ "✗ no target tenant.\n\n → next: pass --tenant <appDomain> (e.g. --tenant tokenoftrust.com), " +
86
+ "or run inside a store checkout.",
87
+ );
88
+ return 2;
89
+ }
90
+
91
+ const prRaw = args.pr;
92
+ const pr = prRaw != null && `${prRaw}`.trim() && Number.isFinite(Number(prRaw)) ? Number(prRaw) : null;
93
+ const changeId = (args.changeId || "").trim() || null;
94
+ if (pr == null && !changeId) {
95
+ console.error(
96
+ "✗ no candidate to retry evidence for.\n\n → next: pass --pr <N> (the PR number), or --change-id <id>.",
97
+ );
98
+ return 2;
99
+ }
100
+
101
+ const secret = (args.secret || process.env.PREVIEW_RECONCILE_SECRET || process.env.GRANTS_ADMIN_SECRET || process.env.TOT_OPERATOR_SECRET || "").trim();
102
+ if (!secret) {
103
+ console.error(
104
+ "✗ no operator secret.\n\n → next: set PREVIEW_RECONCILE_SECRET (or GRANTS_ADMIN_SECRET / " +
105
+ "TOT_OPERATOR_SECRET) in the environment.",
106
+ );
107
+ return 2;
108
+ }
109
+
110
+ const body = {
111
+ ...(pr != null ? { pr } : {}),
112
+ ...(changeId ? { changeId } : {}),
113
+ };
114
+
115
+ let res;
116
+ try {
117
+ res = await fetch(`${base}/api/changes/candidate-evidence-retry`, {
118
+ method: "POST",
119
+ headers: {
120
+ "content-type": "application/json",
121
+ authorization: `Bearer ${secret}`,
122
+ "x-tot-owner": tenant,
123
+ "x-tot-capability": "ship-on-behalf",
124
+ },
125
+ body: JSON.stringify(body),
126
+ });
127
+ } catch (e) {
128
+ console.error(`✗ could not reach ${base}: ${e?.message || e}\n\n → next: check --url / your network.`);
129
+ return 1;
130
+ }
131
+
132
+ let data = {};
133
+ try {
134
+ data = await res.json();
135
+ } catch {
136
+ /* non-JSON error body */
137
+ }
138
+
139
+ const label = pr != null ? `PR #${pr}` : changeId;
140
+
141
+ if (res.ok && data?.ok && data?.pushed) {
142
+ console.log(`✓ Evidence re-pushed for ${label} on ${tenant} (promotable: ${data.promotable}).`);
143
+ return 0;
144
+ }
145
+ if (res.ok && data?.ok && !data?.pushed) {
146
+ console.error(`✗ Evidence push still failed for ${label} on ${tenant}.`);
147
+ if (data.error) console.error(` • ${data.error}`);
148
+ console.error("\n → next: check MCP reachability, then re-run this command.");
149
+ return 1;
150
+ }
151
+
152
+ const message = data?.error || `HTTP ${res.status}`;
153
+ console.error(`✗ Retry did not succeed (HTTP ${res.status}).`);
154
+ console.error(` • ${message}`);
155
+ return 1;
156
+ }
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * tot dev run your store locally with save→reload
7
7
  * tot preview push it to a reviewable preview (validate → reconcile → compliance) ← you are here
8
- * tot ship promote a reconciled preview live ← unit u3
8
+ * tot ship promote a reconciled preview live
9
9
  *
10
10
  * The whole preview flow (validate, push the preview ref, open/update the PR-backed
11
11
  * candidate, stream the reconcile/compliance/preview result) lives in submit.mjs — this
@@ -45,12 +45,12 @@ export function postRunHint(code, alias) {
45
45
  /**
46
46
  * @param {string[]} argv
47
47
  * @param {any} ctx
48
- * @param {{ alias?: string|null }} [opts] `alias` is the old verb the developer typed
48
+ * @param {{ alias?: string|null }} [opts] - `alias` is the old verb the developer typed
49
49
  * (`"submit"` / `"deploy"`) when this flow is reached as a teaching alias; null/omitted
50
50
  * for the first-class `tot preview`.
51
51
  */
52
52
  export async function run(argv, ctx, { alias = null } = {}) {
53
- // `tot preview build …` is the OPERATOR build-on-demand subcommand (unit U3)
53
+ // `tot preview build …` is the OPERATOR build-on-demand subcommand —
54
54
  // a distinct verb from the developer preview flow, so it's dispatched BEFORE the
55
55
  // submit-flow arg parse. Only the first-class `tot preview` carries it (not the
56
56
  // `submit`/`deploy` teaching aliases, which are the push-your-checkout flow).
@@ -59,6 +59,22 @@ export async function run(argv, ctx, { alias = null } = {}) {
59
59
  return runBuild(argv.slice(1), ctx);
60
60
  }
61
61
 
62
+ // `tot preview doctor …` is the read-only merge DIAGNOSIS subcommand —
63
+ // a distinct verb from the developer preview flow, dispatched before the submit-
64
+ // flow arg parse (like `build`). Only the first-class `tot preview` carries it.
65
+ if (!alias && argv[0] === "doctor") {
66
+ const { run: runDoctor } = await import("./preview-doctor.mjs");
67
+ return runDoctor(argv.slice(1), ctx);
68
+ }
69
+
70
+ // `tot preview retry-evidence …` is the OPERATOR recovery subcommand for a
71
+ // candidate whose evidence push to MCP failed (fb-1787938036559-ppmu4e) —
72
+ // dispatched before the submit-flow arg parse, like `build`/`doctor`.
73
+ if (!alias && argv[0] === "retry-evidence") {
74
+ const { run: runRetryEvidence } = await import("./preview-retry-evidence.mjs");
75
+ return runRetryEvidence(argv.slice(1), ctx);
76
+ }
77
+
62
78
  const verb = alias || "preview";
63
79
  const args = parseArgs(argv);
64
80
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `tot revert --preview <PR|integration-sha>` (unit b21) — OPERATOR verb: REMOVE
2
+ * `tot revert --preview <PR|integration-sha>` — OPERATOR verb: REMOVE
3
3
  * already-integrated content from the protected `preview` AGGREGATE by creating a
4
4
  * NEW auditable revert commit, then rebuilding the aggregate. It is the explicit,
5
5
  * safe answer to "several candidates are accepted, but one must be removed"
@@ -15,10 +15,10 @@
15
15
  * TARGET — `--preview <target>` names EITHER a PR number (`42` or `#42`) OR an exact
16
16
  * `preview` integration SHA. A numeric target is sent as `prNumber`; anything else as
17
17
  * `integrationSha`. The server resolves either to the exact integration commit to
18
- * undo (unit b21's resolver, inside the tenant-serialized queue lock), and REFUSES a
18
+ * undo (the resolver, inside the tenant-serialized queue lock), and REFUSES a
19
19
  * target that was never integrated or has already been reverted.
20
20
  *
21
- * TRANSPORT — `POST /api/changes/revert` (the ONE call site of b07's
21
+ * TRANSPORT — `POST /api/changes/revert` (the ONE call site of
22
22
  * `TenantIntegrationQueue.enqueueRevert`). Like `tot accept` / `tot ship`, the CLI
23
23
  * reaches it with the OPERATOR-SECRET Bearer transport (`resolveOperatorSecret` +
24
24
  * `X-Tot-Owner` + `x-tot-capability`). The response is the honest terminal aggregate
@@ -183,7 +183,7 @@ export async function runRevert(
183
183
  const base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
184
184
  const label = prNumber != null ? `PR #${prNumber}` : `integration ${integrationSha}`;
185
185
 
186
- // 1. State the EXACT plan (shared U10 affordance) — a NEW revert commit on
186
+ // 1. State the EXACT plan (shared affordance) — a NEW revert commit on
187
187
  // preview, NO force-reset, NO touch to main/live — and gate on a confirm.
188
188
  const planLines = planForAction({
189
189
  action: "revert",
@@ -232,7 +232,7 @@ export async function runRevert(
232
232
  ...(message ? { message } : {}),
233
233
  };
234
234
 
235
- // 3. POST the honest revert path (b07 queue enqueueRevert) and render terminal state.
235
+ // 3. POST the honest revert path (queue enqueueRevert) and render terminal state.
236
236
  let res;
237
237
  try {
238
238
  res = await fetchImpl(`${base}/api/changes/revert`, {
@@ -7,17 +7,17 @@
7
7
  * tot rollback <versionId> re-point the live channel back to that version
8
8
  *
9
9
  * This does NOT reimplement pointer moves. Every selection and every move goes
10
- * through u3's MCP-side live-pointer seam (`promotion_status` /
10
+ * through the MCP-side live-pointer seam (`promotion_status` /
11
11
  * `promotion_rollback`, tot-mcp `src/modules/mcp/change/promotion-pointer-tools.ts`
12
12
  * + `promotion-pointer-store.ts`) — the SAME primitive `tot ship`'s eventual
13
- * orchestrator composes (decision storefront-atomic-accept-is-ship-orchestrator).
14
- * There is no dg4 ship-orchestrator service in-tree yet, so this calls the u3 seam
13
+ * orchestrator composes.
14
+ * There is no ship-orchestrator service in-tree yet, so this calls that seam
15
15
  * DIRECTLY; when the orchestrator lands it should absorb the physical
16
16
  * publish+verify this command currently leaves to it (see the seam's own
17
17
  * "NOT a live claim" caveat, echoed in `reportRolledBack` below) — a follow-on can
18
18
  * route through it instead without changing this file's UX.
19
19
  *
20
- * Fail-closed (decision static-publish-fails-closed-reviewed-sha): rollback selects
20
+ * Fail-closed: rollback selects
21
21
  * an immutable prior digest/versionId recorded by a PRIOR promote; a missing or
22
22
  * ineligible target is refused with a clear next step, never a silent no-op dressed
23
23
  * up as success. The eligibility check and the pointer move both come from the
@@ -41,7 +41,7 @@ import { startProgress } from "../progress.mjs";
41
41
 
42
42
  const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
43
43
 
44
- // The only publish channel the seam is exercised against today (see the u3 tests
44
+ // The only publish channel the seam is exercised against today (see the tests
45
45
  // and `promotion_set`'s own default). A future multi-target store can override with
46
46
  // `--target`; nothing here assumes there's only ever one.
47
47
  const DEFAULT_TARGET = "production";
@@ -63,7 +63,9 @@ const USAGE = `tot rollback [<versionId>] — instant re-point to a prior live v
63
63
 
64
64
  /** Parse `tot rollback` argv. Pure. Deliberately NO --yes/--force (see the header). */
65
65
  export function parseRollbackArgs(argv) {
66
+ /** @type {{ versionId: string|null, target: string|null, mcp: string|null, identity: string|null, help: boolean }} */
66
67
  const a = { versionId: null, target: null, mcp: null, identity: null, help: false };
68
+ /** @type {string[]} */
67
69
  const positional = [];
68
70
  for (let i = 0; i < argv.length; i++) {
69
71
  const t = argv[i];
@@ -138,11 +140,11 @@ export function normalizeRollbackResult(r) {
138
140
  };
139
141
  }
140
142
 
141
- // ─── Revision links (pure, unit u5) ───────────────────────────────────────────────
143
+ // ─── Revision links (pure) ───────────────────────────────────────────────
142
144
 
143
145
  /**
144
146
  * The immutable `/preview/<tenant>/rev/<sha>/` deep-link for a promoted
145
- * `versionId` — u4's route, and per `gitea-only-tenant-content-authority` the
147
+ * `versionId` — this route, and per `gitea-only-tenant-content-authority` the
146
148
  * `versionId` recorded by a promote IS the tenant Gitea sha, so no extra lookup
147
149
  * is needed to build it. `null` when there's no `base` (storefront origin) or no
148
150
  * `versionId` to link, so a caller can render the history line either way.
@@ -177,10 +179,10 @@ export function rollbackCandidates(history, currentVersionId) {
177
179
  }
178
180
 
179
181
  /** Render the `tot rollback` (no args) history listing — the rollback-target
180
- * picker (unit u2), each entry annotated with its immutable `/rev/<sha>` link
181
- * (unit u5) when a storefront origin is available. Pure — unit-tested.
182
- * @param {{ tenant:string, target:string, current:Record<string,unknown>|null,
183
- * candidates:Record<string,unknown>[], revisionBase?:string|null }} input
182
+ * picker, each entry annotated with its immutable `/rev/<sha>` link
183
+ * when a storefront origin is available. Pure — unit-tested.
184
+ * @param {{ tenant:string, target:string, current:any,
185
+ * candidates:any[], revisionBase?:string|null }} input
184
186
  * @returns {string[]}
185
187
  */
186
188
  export function renderHistory({ tenant, target, current, candidates, revisionBase }) {
@@ -209,7 +211,7 @@ export function renderHistory({ tenant, target, current, candidates, revisionBas
209
211
  */
210
212
  export function renderRollbackPreview({ tenant, target, toVersionId, preview }) {
211
213
  const lines = ["", ` This rollback will change the LIVE site for ${tenant}:`];
212
- const current = preview.pointer?.current;
214
+ const current = /** @type {any} */ (preview.pointer?.current);
213
215
  if (current) lines.push(` ${target}: ${current.versionId} → ${toVersionId}`);
214
216
  else lines.push(` ${target}: → ${toVersionId}`);
215
217
  return lines;
@@ -225,7 +227,7 @@ export function renderRollbackPreview({ tenant, target, toVersionId, preview })
225
227
  * @param {{callTool:Function}} client an MCP client (real or mock)
226
228
  * @param {{ tenant:string, target:string, toVersionId:string|null,
227
229
  * revisionBase?:string|null }} params `revisionBase` is the storefront origin
228
- * the listing's `/rev/<sha>` links resolve against (unit u5); omit to list
230
+ * the listing's `/rev/<sha>` links resolve against; omit to list
229
231
  * without links.
230
232
  * @param {{ interactive?:()=>boolean, confirm?:(q:string,d:boolean)=>Promise<boolean>,
231
233
  * progress?:boolean }} [deps]
@@ -258,7 +260,7 @@ export async function runRollback(client, { tenant, target, toVersionId, revisio
258
260
  );
259
261
  return 1;
260
262
  }
261
- const candidates = rollbackCandidates(status.history, status.current?.versionId ?? null);
263
+ const candidates = rollbackCandidates(status.history, /** @type {any} */ (status).current?.versionId ?? null);
262
264
  for (const line of renderHistory({ tenant, target, current: status.current, candidates, revisionBase })) {
263
265
  console.log(line);
264
266
  }
@@ -375,8 +377,8 @@ export async function run(argv, ctx) {
375
377
  const tenant = ctx.tenant;
376
378
  const target = args.target?.trim() || DEFAULT_TARGET;
377
379
  const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
378
- // The storefront origin the listing's /rev/<sha> links resolve against (unit
379
- // u5) — owner == appDomain on this platform, mirroring go-live.mjs's default.
380
+ // The storefront origin the listing's /rev/<sha> links resolve against
381
+ // — owner == appDomain on this platform, mirroring go-live.mjs's default.
380
382
  const revisionBase = env.TOT_STOREFRONT_URL || (tenant ? `https://${tenant}` : null);
381
383
  const client = createMcpClient(baseUrl);
382
384
  try {