@tokenoftrust/cli 1.4.0-rc.14 → 1.4.0-rc.16
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/bin/tot.mjs +66 -8
- package/package.json +1 -1
- package/src/commands/accept.mjs +225 -159
- package/src/commands/branches.mjs +296 -0
- package/src/commands/cleanup.mjs +268 -0
- package/src/commands/hotfix.mjs +428 -0
- package/src/commands/pr.mjs +19 -5
- package/src/commands/revert.mjs +322 -0
- package/src/commands/ship.mjs +268 -1147
- package/src/commands/submit.mjs +457 -94
- package/src/commands/sync.mjs +192 -0
- package/src/plan.mjs +133 -31
package/bin/tot.mjs
CHANGED
|
@@ -14,10 +14,13 @@
|
|
|
14
14
|
* tot validate lint your store before you submit ← built
|
|
15
15
|
* tot dev run your store locally with save→reload ← built (monorepo: host astro; standalone: runs the published runner image)
|
|
16
16
|
* tot preview push your store to a reviewable preview ← built (validate + push preview ref; MCP preview_status read-back). `submit`/`deploy` are teaching aliases.
|
|
17
|
-
* tot ship
|
|
18
|
-
* tot accept / tot merge
|
|
17
|
+
* tot ship publish the current green aggregate live ← built (GET/POST /api/changes/ship → b09 orchestrator; exact plan + y/N confirm; waits for VERIFIED live truth)
|
|
18
|
+
* tot accept / tot merge queue a PR into the preview aggregate ← built (b08: plan + y/N confirm → POST /api/changes/integrate = b07 queue enqueue; NO merge-to-main, NO go-live; refuses non-TTY without --yes)
|
|
19
|
+
* tot sync fetch `preview` + merge it into your branch ← built (b16: the common accept-conflict recovery path; local-only, stops safely on conflict)
|
|
19
20
|
* tot rollback [<version>] instant re-point to a prior live version ← built (u3 promotion_status/promotion_rollback seam; diff + y/N confirm; refuses non-TTY / ineligible)
|
|
20
21
|
* tot pr list / view / close your candidate PRs ← built (candidate_status/candidate_close; gh-pr-shaped)
|
|
22
|
+
* tot branches full branch-cleanup report (every branch, not just open PRs) ← built (b14: candidate_list)
|
|
23
|
+
* tot cleanup owner-confirmed branch GC (deletes terminal branches only) ← built (b14: candidate_list + candidate_delete; --dry-run classifies only)
|
|
21
24
|
* tot doctor check this machine is ready
|
|
22
25
|
* tot ideas copy-paste AI prompts that reliably wow
|
|
23
26
|
* tot feedback send a note to ToT + your recent CLI activity ← built (activity-log.mjs → feedback_submit MCP tool)
|
|
@@ -64,12 +67,17 @@ tot — Token of Trust developer CLI
|
|
|
64
67
|
tot validate lint your store before you submit
|
|
65
68
|
tot dev run your store locally with save→reload
|
|
66
69
|
tot preview push your store to a reviewable preview
|
|
67
|
-
tot ship
|
|
68
|
-
tot accept / tot merge
|
|
70
|
+
tot ship publish the tenant's current green aggregate live (plan → confirm → ship)
|
|
71
|
+
tot accept / tot merge queue a PR into the preview aggregate — operator verb, no go-live (plan → confirm → integrate)
|
|
72
|
+
tot sync fetch \`preview\` and merge it into your local branch (accept-conflict recovery)
|
|
69
73
|
tot rollback [<version>] instant re-point to a prior live version (list → confirm → rollback)
|
|
74
|
+
tot revert --preview <PR|sha> remove already-integrated content from the preview aggregate via a new revert commit — no force-reset (plan → confirm → revert)
|
|
75
|
+
tot hotfix --pr <N> OWNER-ONLY exception: release an urgent fix from main to live, bypassing unshipped preview work (plan → confirm → release → auto forward-integrate)
|
|
70
76
|
tot retire evict a candidate PR's preview to reclaim space — operator verb, rebuildable (plan → confirm → evict)
|
|
71
77
|
tot go-live cut the apex domain over to the storefront (readiness → confirm → cutover)
|
|
72
78
|
tot pr list / view / close your candidate PRs
|
|
79
|
+
tot branches full branch-cleanup report — every branch, any PR state, cleanup eligibility
|
|
80
|
+
tot cleanup owner-confirmed branch GC (--dry-run to classify only; deletes terminal branches only)
|
|
73
81
|
tot doctor check this machine is ready
|
|
74
82
|
tot ideas copy-paste AI prompts that reliably wow
|
|
75
83
|
tot feedback "<msg>" send feedback to Token of Trust (attaches recent activity)
|
|
@@ -174,20 +182,52 @@ async function dispatch(cmd, rest, ctx) {
|
|
|
174
182
|
return run(rest, ctx);
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
// `accept`
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
185
|
+
// `accept` queue-integrates a PR into the protected `preview` aggregate (b08) —
|
|
186
|
+
// NO merge-to-main, NO go-live; going live is `tot ship`, which publishes the
|
|
187
|
+
// whole current GREEN AGGREGATE (b10). Backed by POST /api/changes/integrate
|
|
188
|
+
// (b07's TenantIntegrationQueue.enqueue); see accept.mjs's header. `merge` is a
|
|
189
|
+
// first-class alias, not a teaching nudge: both dispatch straight to the same command.
|
|
181
190
|
if (cmd === "accept" || cmd === "merge") {
|
|
182
191
|
const { run } = await import("../src/commands/accept.mjs");
|
|
183
192
|
return run(rest, ctx);
|
|
184
193
|
}
|
|
185
194
|
|
|
195
|
+
// `sync` is the common accept-conflict recovery path (P1 item 12): fetches the
|
|
196
|
+
// protected `preview` branch and merges it into the developer's local branch so
|
|
197
|
+
// they can resolve locally, then re-`tot preview`. Purely local — no push, no
|
|
198
|
+
// MCP call, never touches the shared `preview`/`main` refs themselves. See
|
|
199
|
+
// sync.mjs's header for the full contract.
|
|
200
|
+
if (cmd === "sync") {
|
|
201
|
+
const { run } = await import("../src/commands/sync.mjs");
|
|
202
|
+
return run(rest, ctx);
|
|
203
|
+
}
|
|
204
|
+
|
|
186
205
|
if (cmd === "rollback") {
|
|
187
206
|
const { run } = await import("../src/commands/rollback.mjs");
|
|
188
207
|
return run(rest, ctx);
|
|
189
208
|
}
|
|
190
209
|
|
|
210
|
+
// `revert --preview <PR|integration-sha>` removes already-integrated content from
|
|
211
|
+
// the protected `preview` aggregate via a NEW auditable revert commit (b21) — NO
|
|
212
|
+
// force-reset, NO touch to main/live. Backed by POST /api/changes/revert (b07's
|
|
213
|
+
// TenantIntegrationQueue.enqueueRevert). To undo something already LIVE, that's
|
|
214
|
+
// `tot rollback`, not this. See revert.mjs's header.
|
|
215
|
+
if (cmd === "revert") {
|
|
216
|
+
const { run } = await import("../src/commands/revert.mjs");
|
|
217
|
+
return run(rest, ctx);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// `hotfix --pr <N>` is the OWNER-ONLY EXCEPTION lane (b22): release an urgent fix
|
|
221
|
+
// from `main` to live while `preview` still holds other unshipped work, EXCLUDING
|
|
222
|
+
// that unshipped preview head, then automatically forward-integrate main → preview
|
|
223
|
+
// and re-validate. It is DELIBERATELY a distinct verb — never a `--base main` flag
|
|
224
|
+
// on `tot ship` (which publishes the whole green preview aggregate). Backed by
|
|
225
|
+
// GET/POST /api/changes/hotfix → b22's HotfixOrchestrator. See hotfix.mjs's header.
|
|
226
|
+
if (cmd === "hotfix") {
|
|
227
|
+
const { run } = await import("../src/commands/hotfix.mjs");
|
|
228
|
+
return run(rest, ctx);
|
|
229
|
+
}
|
|
230
|
+
|
|
191
231
|
// `retire` evicts a candidate PR's hosted preview to reclaim space (unit U7) —
|
|
192
232
|
// a DISTINCT operator verb from `accept`/reject: retire touches no change
|
|
193
233
|
// lifecycle and is reversible-by-rebuild (`tot preview build`). See retire.mjs.
|
|
@@ -206,6 +246,24 @@ async function dispatch(cmd, rest, ctx) {
|
|
|
206
246
|
return run(rest, ctx);
|
|
207
247
|
}
|
|
208
248
|
|
|
249
|
+
// `branches` is the FULL branch-cleanup report (every branch, any PR state) —
|
|
250
|
+
// distinct from `tot pr list`, which only shows OPEN PRs (P1 item 9). Read-only;
|
|
251
|
+
// always safe to run, and the report `tot cleanup` reuses before deleting anything.
|
|
252
|
+
if (cmd === "branches") {
|
|
253
|
+
const { run } = await import("../src/commands/branches.mjs");
|
|
254
|
+
return run(rest, ctx);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// `cleanup` is owner-confirmed BRANCH GC (P1 items 9/10) — an irreversible git-ref
|
|
258
|
+
// delete, distinct from `tot retire`'s hosted-preview eviction (rebuildable, no
|
|
259
|
+
// branch touched). `--dry-run` classifies and prints; it never deletes. Age alone
|
|
260
|
+
// never makes a branch eligible; main/preview are always kept; orphans are
|
|
261
|
+
// quarantined, never auto-deleted. See cleanup.mjs's header.
|
|
262
|
+
if (cmd === "cleanup") {
|
|
263
|
+
const { run } = await import("../src/commands/cleanup.mjs");
|
|
264
|
+
return run(rest, ctx);
|
|
265
|
+
}
|
|
266
|
+
|
|
209
267
|
if (cmd === "app") {
|
|
210
268
|
const { run } = await import("../src/commands/app/index.mjs");
|
|
211
269
|
return run(rest, ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "1.4.0-rc.
|
|
3
|
+
"version": "1.4.0-rc.16",
|
|
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/accept.mjs
CHANGED
|
@@ -1,84 +1,78 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tot accept --tenant <t> --pr <N>` (alias: `tot merge`) — OPERATOR verb:
|
|
3
|
-
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* explicit, deploy-free half of that, for an operator who just wants the merge.)
|
|
2
|
+
* `tot accept --tenant <t> --pr <N>` (alias: `tot merge`) — OPERATOR verb: QUEUE a
|
|
3
|
+
* PR's integration into the protected `preview` AGGREGATE. This SUPERSEDES the
|
|
4
|
+
* retired `tot accept` = merge-PR→main semantics (operator-console U5): accepting a
|
|
5
|
+
* change no longer merges it to main — it enqueues it into the tenant's shared
|
|
6
|
+
* `preview` aggregate, where b07's tenant-serialized queue merges it (b06
|
|
7
|
+
* `candidate_accept`, preview-base only), rebuilds the aggregate, and moves the
|
|
8
|
+
* shared preview pointer ONLY when combined evidence is green.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the
|
|
15
|
-
*
|
|
16
|
-
* transport, no new wire contract.
|
|
10
|
+
* DISTINCT from `tot ship`: accept is NOT go-live. It touches NO `main` and NO live
|
|
11
|
+
* channel; a green aggregate is promoted live only by a later `tot ship`. And it is
|
|
12
|
+
* NOT `tot preview build` (which materializes ONE candidate's own preview in
|
|
13
|
+
* isolation) — accept lands the candidate into the SHARED aggregate other reviewers
|
|
14
|
+
* see.
|
|
17
15
|
*
|
|
18
|
-
* TARGET RESOLUTION — `--pr N --tenant t` names the PR
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* because it reads the DEVELOPER'S OWN local git HEAD — an operator targeting an
|
|
23
|
-
* arbitrary PR has no local checkout of it to read). So this verb requires the
|
|
24
|
-
* change-RECORD id explicitly via `--change-id` (the `chg_<uuid>` a prior
|
|
25
|
-
* change_open/change_ready minted — e.g. from the PR author's own `tot ship`
|
|
26
|
-
* review step, or the `/admin` confirm flow) and an optional `--head-sha`
|
|
27
|
-
* (passed through as `expectedHeadSha` — an optimistic-concurrency guard so
|
|
28
|
-
* accept refuses if the PR moved since the operator looked at it).
|
|
16
|
+
* TARGET RESOLUTION — `--pr N --tenant t` names the PR; the server resolves it to its
|
|
17
|
+
* candidate from the tenant's ReviewEnvironment index (unit b04 — tenant + PR, NO
|
|
18
|
+
* chg-id). An explicit `--change-id` is still accepted (targets a specific record),
|
|
19
|
+
* but is no longer REQUIRED — the whole point of b08 is that a PR number is enough.
|
|
29
20
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
21
|
+
* TRANSPORT — the honest accept path is `POST /api/changes/integrate` (the ONE call
|
|
22
|
+
* site of b07's `TenantIntegrationQueue.enqueue`). Like `tot ship --pr` (U16) and
|
|
23
|
+
* `tot pr list --tenant` (U17), the CLI reaches it with the OPERATOR-SECRET Bearer
|
|
24
|
+
* transport (`resolveOperatorSecret` + `X-Tot-Owner` + `x-tot-capability`), since the
|
|
25
|
+
* CLI holds no storefront cookie. The response is the honest `IntegrateOutcome` —
|
|
26
|
+
* `queueState` / `runState` / `pointerMoved` / `aggregateSha` / `statusMessage` —
|
|
27
|
+
* which this verb renders VERBATIM, never a bare "merged".
|
|
33
28
|
*
|
|
34
|
-
* HUMAN GATE —
|
|
35
|
-
* this
|
|
36
|
-
* which
|
|
37
|
-
*
|
|
38
|
-
* `--yes` is
|
|
39
|
-
* is deliberately NO default-yes, and a non-TTY without `--yes` is refused
|
|
40
|
-
* rather than silently proceeding (mirrors `tot ship`'s non-TTY refusal).
|
|
29
|
+
* HUMAN GATE — integrating into the shared preview is a decision a human makes, so
|
|
30
|
+
* this ALWAYS states the EXACT plan (shared `planForAction`, unit U10: which PR,
|
|
31
|
+
* which tenant, "queue for integration into the preview aggregate — NO merge, NO
|
|
32
|
+
* go-live") and requires an explicit confirm. `--yes` is an explicit affirmative; a
|
|
33
|
+
* non-TTY without `--yes` is refused (mirrors `tot ship`'s non-TTY refusal).
|
|
41
34
|
*
|
|
42
|
-
* Dependency-free (
|
|
35
|
+
* Dependency-free (global fetch + the shared plan module).
|
|
43
36
|
*/
|
|
44
|
-
import { createMcpClient } from "../mcp.mjs";
|
|
45
|
-
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
46
37
|
import { fail } from "../errors.mjs";
|
|
47
38
|
import { planForAction, printPlanAndConfirm } from "../plan.mjs";
|
|
48
|
-
|
|
39
|
+
// Reuse `tot ship`'s operator-secret precedence verbatim so accept + ship + pr-list
|
|
40
|
+
// speak ONE operator-auth contract, not three.
|
|
41
|
+
import { resolveOperatorSecret } from "./ship.mjs";
|
|
49
42
|
|
|
50
|
-
const
|
|
43
|
+
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
51
44
|
|
|
52
|
-
const USAGE = `tot accept —
|
|
45
|
+
const USAGE = `tot accept — queue a PR's integration into the preview aggregate (alias: tot merge)
|
|
53
46
|
|
|
54
|
-
tot accept --tenant <t> --pr <N>
|
|
55
|
-
tot merge --tenant <t> --pr <N>
|
|
47
|
+
tot accept --tenant <t> --pr <N>
|
|
48
|
+
tot merge --tenant <t> --pr <N> (same command)
|
|
56
49
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
50
|
+
Queues the given PR for integration into the tenant's protected \`preview\`
|
|
51
|
+
aggregate: the serialized queue merges it into \`preview\` (preview-base only),
|
|
52
|
+
rebuilds the aggregate, runs combined evidence, and moves the shared preview
|
|
53
|
+
pointer ONLY when green. Accepting does NOT merge to main and does NOT go live —
|
|
54
|
+
a green aggregate is promoted live later by \`tot ship\`.
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
for an explicit confirm. There is no default-yes; a non-TTY
|
|
64
|
-
refused rather than silently proceeding.
|
|
56
|
+
Integrating into the shared preview is a human decision: this ALWAYS prints the
|
|
57
|
+
exact plan and asks for an explicit confirm. There is no default-yes; a non-TTY
|
|
58
|
+
without --yes is refused rather than silently proceeding.
|
|
65
59
|
|
|
66
60
|
Options:
|
|
67
61
|
--tenant <appDomain> Target tenant (e.g. tokenoftrust.com). Defaults to the
|
|
68
62
|
current checkout's tenant when run inside one.
|
|
69
|
-
--pr <N> PR number to
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
the id a prior change_open/change_ready minted (e.g.
|
|
73
|
-
from the author's own \`tot ship\` review step, or the
|
|
74
|
-
/admin confirm flow).
|
|
63
|
+
--pr <N> PR number to integrate. The server resolves it to its
|
|
64
|
+
candidate from the tenant's queue (no --change-id needed).
|
|
65
|
+
--change-id <id> Target a specific change record instead of a PR number.
|
|
75
66
|
--head-sha <sha> Optional expected PR head sha (expectedHeadSha) — an
|
|
76
67
|
optimistic-concurrency guard against a PR that moved.
|
|
77
|
-
--
|
|
78
|
-
--
|
|
68
|
+
--url <origin> storefront origin (default: env TOT_STOREFRONT_URL)
|
|
69
|
+
--secret <s> operator secret (prefer the env vars below)
|
|
79
70
|
--yes, -y Skip the interactive confirm (still an explicit human
|
|
80
71
|
affirmative — there is no default-yes).
|
|
81
|
-
--help, -h Show this help
|
|
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).`;
|
|
82
76
|
|
|
83
77
|
/** Parse `tot accept` / `tot merge` argv. Pure — unit-testable. */
|
|
84
78
|
export function parseAcceptArgs(argv) {
|
|
@@ -87,7 +81,8 @@ export function parseAcceptArgs(argv) {
|
|
|
87
81
|
pr: null,
|
|
88
82
|
changeId: null,
|
|
89
83
|
headSha: null,
|
|
90
|
-
|
|
84
|
+
url: null,
|
|
85
|
+
secret: null,
|
|
91
86
|
identity: null,
|
|
92
87
|
yes: false,
|
|
93
88
|
help: false,
|
|
@@ -98,7 +93,8 @@ export function parseAcceptArgs(argv) {
|
|
|
98
93
|
else if (t === "--pr") a.pr = argv[++i];
|
|
99
94
|
else if (t === "--change-id") a.changeId = argv[++i];
|
|
100
95
|
else if (t === "--head-sha" || t === "--head") a.headSha = argv[++i];
|
|
101
|
-
else if (t === "--
|
|
96
|
+
else if (t === "--url") a.url = argv[++i];
|
|
97
|
+
else if (t === "--secret") a.secret = argv[++i];
|
|
102
98
|
else if (t === "--identity") a.identity = argv[++i];
|
|
103
99
|
else if (t === "--yes" || t === "-y") a.yes = true;
|
|
104
100
|
else if (t === "--help" || t === "-h") a.help = true;
|
|
@@ -106,22 +102,162 @@ export function parseAcceptArgs(argv) {
|
|
|
106
102
|
return a;
|
|
107
103
|
}
|
|
108
104
|
|
|
109
|
-
/**
|
|
110
|
-
|
|
105
|
+
/**
|
|
106
|
+
* Normalise a `POST /api/changes/integrate` body to the honest terminal aggregate
|
|
107
|
+
* fields this verb renders. Reads defensively so a plausible field rename degrades
|
|
108
|
+
* rather than crashes. Pure — unit-tested.
|
|
109
|
+
* @param {any} data
|
|
110
|
+
* @returns {{ ok:boolean, queueState:string|null, runState:string|null,
|
|
111
|
+
* pointerMoved:boolean, aggregateSha:string|null, statusMessage:string|null,
|
|
112
|
+
* reason:string|null, changeId:string|null, prNumber:number|null, error:string|null, raw:any }}
|
|
113
|
+
*/
|
|
114
|
+
export function normalizeIntegrateResponse(data) {
|
|
115
|
+
const o = data && typeof data === "object" ? data : {};
|
|
116
|
+
return {
|
|
117
|
+
ok: o.ok === true,
|
|
118
|
+
queueState: typeof o.queueState === "string" ? o.queueState : null,
|
|
119
|
+
runState: typeof o.runState === "string" ? o.runState : null,
|
|
120
|
+
pointerMoved: o.pointerMoved === true,
|
|
121
|
+
aggregateSha: typeof o.aggregateSha === "string" ? o.aggregateSha : null,
|
|
122
|
+
statusMessage: typeof o.statusMessage === "string" ? o.statusMessage : null,
|
|
123
|
+
reason: typeof o.reason === "string" ? o.reason : null,
|
|
124
|
+
changeId: typeof o.changeId === "string" ? o.changeId : null,
|
|
125
|
+
prNumber: typeof o.prNumber === "number" ? o.prNumber : null,
|
|
126
|
+
error: typeof o.error === "string" ? o.error : null,
|
|
127
|
+
raw: data,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Render the terminal aggregate status in house style — the honest queue/run state,
|
|
133
|
+
* NEVER a bare "merged". Pure given its inputs; returns the process exit code.
|
|
134
|
+
* @param {ReturnType<typeof normalizeIntegrateResponse>} result
|
|
135
|
+
* @param {{ tenant:string, label:string }} ctx
|
|
136
|
+
* @returns {number}
|
|
137
|
+
*/
|
|
138
|
+
export function reportIntegrated(result, { tenant, label }) {
|
|
139
|
+
const state = `${result.queueState ?? "?"}/${result.runState ?? "—"}`;
|
|
140
|
+
if (result.ok) {
|
|
141
|
+
console.log(`\n ✓ queued ${label} into ${tenant}'s preview aggregate — it is GREEN.`);
|
|
142
|
+
console.log(` aggregate: ${state}${result.aggregateSha ? ` (${result.aggregateSha})` : ""}`);
|
|
143
|
+
if (result.pointerMoved) console.log(" the shared preview pointer moved to this aggregate.");
|
|
144
|
+
console.log(" → next: `tot ship` to promote this green aggregate live.");
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
// Honest non-green: the candidate did NOT land in the shippable aggregate.
|
|
148
|
+
console.log(`\n ✗ ${label} did NOT integrate into ${tenant}'s preview aggregate.`);
|
|
149
|
+
console.log(` aggregate: ${state}${result.reason ? ` (${result.reason})` : ""}`);
|
|
150
|
+
if (result.statusMessage) console.log(` why: ${result.statusMessage}`);
|
|
151
|
+
console.log(" → next: fix the candidate (re-`tot preview`), then re-run `tot accept`.");
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The accept-means-integrate flow after args are parsed: state the exact plan,
|
|
157
|
+
* confirm, then POST `/api/changes/integrate` (operator-secret transport) and render
|
|
158
|
+
* the honest terminal aggregate status. `fetch`/`confirmPlan` injected so it is
|
|
159
|
+
* unit-tested with no network/TTY.
|
|
160
|
+
*
|
|
161
|
+
* @param {{ tenant:string, pr:number|null, changeId:string|null, headSha:string|null,
|
|
162
|
+
* secret:string, storefrontUrl?:string|null, yes?:boolean }} params
|
|
163
|
+
* @param {{ fetch?:typeof fetch, confirmPlan?:typeof printPlanAndConfirm }} [deps]
|
|
164
|
+
* @returns {Promise<number>} process exit code
|
|
165
|
+
*/
|
|
166
|
+
export async function runIntegrate(
|
|
167
|
+
{ tenant, pr, changeId, headSha, secret, storefrontUrl = null, yes = false },
|
|
168
|
+
deps = {},
|
|
169
|
+
) {
|
|
170
|
+
const fetchImpl = deps.fetch || globalThis.fetch;
|
|
171
|
+
const confirmPlan = deps.confirmPlan || printPlanAndConfirm;
|
|
172
|
+
const base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
|
|
111
173
|
const label = pr != null ? `PR #${pr}` : changeId;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
174
|
+
|
|
175
|
+
// 1. State the EXACT plan (shared U10 affordance) — queue-integrate-into-preview,
|
|
176
|
+
// NO merge, NO go-live — and gate on an explicit confirm.
|
|
177
|
+
const planLines = planForAction({ action: "accept", tenant, pr, changeId, headSha });
|
|
178
|
+
const { confirmed, reason } = await confirmPlan(planLines, {
|
|
179
|
+
yes,
|
|
180
|
+
question: `Queue ${label} for integration into ${tenant}'s preview aggregate?`,
|
|
181
|
+
});
|
|
182
|
+
if (!confirmed) {
|
|
183
|
+
if (reason === "non-tty") {
|
|
184
|
+
console.error(
|
|
185
|
+
fail(
|
|
186
|
+
"refusing to integrate without confirmation on a non-TTY.",
|
|
187
|
+
"re-run with --yes (an explicit human affirmative), or from an interactive terminal.",
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
return 2;
|
|
191
|
+
}
|
|
192
|
+
console.log("Aborted — nothing was integrated.");
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 2. Operator-secret transport — the CLI holds no storefront cookie, so the
|
|
197
|
+
// Bearer + X-Tot-Owner path is its route (same as `tot ship --pr`).
|
|
198
|
+
if (!secret) {
|
|
199
|
+
console.error(
|
|
200
|
+
fail(
|
|
201
|
+
"integrating a PR is an OPERATOR action — it needs an operator secret",
|
|
202
|
+
"set PREVIEW_RECONCILE_SECRET (or GRANTS_ADMIN_SECRET / TOT_OPERATOR_SECRET), or pass --secret",
|
|
203
|
+
),
|
|
117
204
|
);
|
|
118
|
-
return
|
|
205
|
+
return 2;
|
|
119
206
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
207
|
+
const authHeaders = {
|
|
208
|
+
"content-type": "application/json",
|
|
209
|
+
authorization: `Bearer ${secret}`,
|
|
210
|
+
"x-tot-owner": tenant,
|
|
211
|
+
"x-tot-capability": "ship-on-behalf",
|
|
212
|
+
};
|
|
213
|
+
const requestBody = {
|
|
214
|
+
repo: tenant,
|
|
215
|
+
...(pr != null ? { prNumber: pr } : {}),
|
|
216
|
+
...(changeId ? { changeId } : {}),
|
|
217
|
+
...(headSha ? { expectedHeadSha: headSha } : {}),
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// 3. POST the honest accept path (b07 queue enqueue) and render the terminal state.
|
|
221
|
+
let res;
|
|
222
|
+
try {
|
|
223
|
+
res = await fetchImpl(`${base}/api/changes/integrate`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: authHeaders,
|
|
226
|
+
body: JSON.stringify(requestBody),
|
|
227
|
+
});
|
|
228
|
+
} catch (e) {
|
|
229
|
+
console.error(
|
|
230
|
+
fail(`couldn't reach the integration queue at ${base}: ${String(e?.message || e)}`, "check --url / your network, then re-run"),
|
|
231
|
+
);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
let data = {};
|
|
236
|
+
try {
|
|
237
|
+
data = await res.json();
|
|
238
|
+
} catch {
|
|
239
|
+
/* non-JSON / empty body */
|
|
240
|
+
}
|
|
241
|
+
const result = normalizeIntegrateResponse(data);
|
|
242
|
+
|
|
243
|
+
// A pre-flight error (auth, unknown tenant, candidate not found) is an HTTP 4xx
|
|
244
|
+
// with `{ error }` and no queue verdict — surface it distinctly from a red run.
|
|
245
|
+
if (!res.ok && result.queueState == null) {
|
|
246
|
+
const msg = result.error || `HTTP ${res.status}`;
|
|
247
|
+
console.error(
|
|
248
|
+
fail(
|
|
249
|
+
`the integration queue refused the request: ${msg}`,
|
|
250
|
+
res.status === 401 || res.status === 403
|
|
251
|
+
? "check the operator secret and that it's authorised for this tenant"
|
|
252
|
+
: res.status === 404
|
|
253
|
+
? `check that ${label} has a built candidate in ${tenant}'s queue (\`tot pr list --tenant ${tenant}\`)`
|
|
254
|
+
: "check --tenant / --url / --pr, then re-run",
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return reportIntegrated(result, { tenant, label });
|
|
125
261
|
}
|
|
126
262
|
|
|
127
263
|
/**
|
|
@@ -148,100 +284,30 @@ export async function run(argv, ctx) {
|
|
|
148
284
|
}
|
|
149
285
|
|
|
150
286
|
const prRaw = args.pr;
|
|
151
|
-
const pr =
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
);
|
|
156
|
-
return 2;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const changeId = (args.changeId || "").trim();
|
|
160
|
-
if (!changeId) {
|
|
161
|
-
// u9: auto-resolve PR→changeId via forge PR-read (candidate_status) — until
|
|
162
|
-
// then an operator targeting a specific PR must supply the change-record id
|
|
163
|
-
// directly (there's no cross-tenant forge lookup this CLI can do for a PR
|
|
164
|
-
// that isn't the caller's own checkout).
|
|
287
|
+
const pr =
|
|
288
|
+
prRaw != null && `${prRaw}`.trim() && Number.isFinite(Number(prRaw)) ? Number(prRaw) : null;
|
|
289
|
+
const changeId = (args.changeId || "").trim() || null;
|
|
290
|
+
if (pr == null && !changeId) {
|
|
165
291
|
console.error(
|
|
166
292
|
fail(
|
|
167
|
-
|
|
168
|
-
"pass --
|
|
169
|
-
"or run `tot ship` from the PR author's own checkout instead.",
|
|
293
|
+
"no PR or change to integrate.",
|
|
294
|
+
"pass --pr <N> (the PR number to queue), or --change-id <id> to target a specific record.",
|
|
170
295
|
),
|
|
171
296
|
);
|
|
172
297
|
return 2;
|
|
173
298
|
}
|
|
174
299
|
|
|
175
300
|
const headSha = (args.headSha || "").trim() || null;
|
|
301
|
+
const storefrontUrl =
|
|
302
|
+
args.url || env.TOT_STOREFRONT_URL || env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL;
|
|
176
303
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const { confirmed, reason } = await printPlanAndConfirm(planLines, {
|
|
304
|
+
return await runIntegrate({
|
|
305
|
+
tenant,
|
|
306
|
+
pr,
|
|
307
|
+
changeId,
|
|
308
|
+
headSha,
|
|
309
|
+
secret: resolveOperatorSecret(args.secret, env),
|
|
310
|
+
storefrontUrl,
|
|
185
311
|
yes: args.yes,
|
|
186
|
-
question: "Merge this into main?",
|
|
187
312
|
});
|
|
188
|
-
if (!confirmed) {
|
|
189
|
-
if (reason === "non-tty") {
|
|
190
|
-
console.error(
|
|
191
|
-
fail(
|
|
192
|
-
"refusing to merge without confirmation on a non-TTY.",
|
|
193
|
-
"re-run with --yes (an explicit human affirmative), or from an interactive terminal.",
|
|
194
|
-
),
|
|
195
|
-
);
|
|
196
|
-
return 2;
|
|
197
|
-
}
|
|
198
|
-
console.log("Aborted — nothing was merged.");
|
|
199
|
-
return 1;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
203
|
-
const client = createMcpClient(baseUrl);
|
|
204
|
-
try {
|
|
205
|
-
await establishSession(client, { env, prefer: args.identity || undefined });
|
|
206
|
-
// Bind the active tenant so change_accept/change_status read the right scope.
|
|
207
|
-
await client.callTool("client_switch", { tenant });
|
|
208
|
-
|
|
209
|
-
let accept;
|
|
210
|
-
try {
|
|
211
|
-
accept = normalizeChangeResult(
|
|
212
|
-
await client.callTool("change_accept", {
|
|
213
|
-
id: changeId,
|
|
214
|
-
tenant,
|
|
215
|
-
dryRun: false,
|
|
216
|
-
...(headSha ? { expectedHeadSha: headSha } : {}),
|
|
217
|
-
// Stable per (changeId, head): a re-run after a blip returns the
|
|
218
|
-
// original accept instead of double-merging.
|
|
219
|
-
idempotencyKey: `accept-${changeId}-${headSha || pr || "pr"}`,
|
|
220
|
-
}),
|
|
221
|
-
);
|
|
222
|
-
} catch (e) {
|
|
223
|
-
console.error(
|
|
224
|
-
fail(
|
|
225
|
-
`the accept pipeline refused to merge this change: ${String(e?.message || e)}`,
|
|
226
|
-
"check its review/reconcile status, then re-run `tot accept`",
|
|
227
|
-
),
|
|
228
|
-
);
|
|
229
|
-
return 1;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const status = await pollChangeShipped(client, { id: changeId, tenant });
|
|
233
|
-
return reportAccepted(accept, status, { tenant, pr, changeId });
|
|
234
|
-
} catch (e) {
|
|
235
|
-
if (e instanceof AuthUnavailableError) {
|
|
236
|
-
console.error(fail("sign in to accept a change", e.hint || "run `tot login`, then re-run `tot accept`"));
|
|
237
|
-
return 1;
|
|
238
|
-
}
|
|
239
|
-
console.error(
|
|
240
|
-
fail(
|
|
241
|
-
`couldn't reach the accept service: ${String(e?.message || e)}`,
|
|
242
|
-
"check your connection and that you're signed in, then re-run",
|
|
243
|
-
),
|
|
244
|
-
);
|
|
245
|
-
return 1;
|
|
246
|
-
}
|
|
247
313
|
}
|