@tokenoftrust/cli 1.4.0-rc.14 → 1.4.0-rc.15
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 +9 -8
- package/package.json +1 -1
- package/src/commands/accept.mjs +225 -159
- package/src/commands/pr.mjs +19 -5
- package/src/commands/ship.mjs +267 -1147
- package/src/plan.mjs +58 -29
package/bin/tot.mjs
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
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
19
|
* 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
20
|
* tot pr list / view / close your candidate PRs ← built (candidate_status/candidate_close; gh-pr-shaped)
|
|
21
21
|
* tot doctor check this machine is ready
|
|
@@ -64,8 +64,8 @@ tot — Token of Trust developer CLI
|
|
|
64
64
|
tot validate lint your store before you submit
|
|
65
65
|
tot dev run your store locally with save→reload
|
|
66
66
|
tot preview push your store to a reviewable preview
|
|
67
|
-
tot ship
|
|
68
|
-
tot accept / tot merge
|
|
67
|
+
tot ship publish the tenant's current green aggregate live (plan → confirm → ship)
|
|
68
|
+
tot accept / tot merge queue a PR into the preview aggregate — operator verb, no go-live (plan → confirm → integrate)
|
|
69
69
|
tot rollback [<version>] instant re-point to a prior live version (list → confirm → rollback)
|
|
70
70
|
tot retire evict a candidate PR's preview to reclaim space — operator verb, rebuildable (plan → confirm → evict)
|
|
71
71
|
tot go-live cut the apex domain over to the storefront (readiness → confirm → cutover)
|
|
@@ -174,10 +174,11 @@ async function dispatch(cmd, rest, ctx) {
|
|
|
174
174
|
return run(rest, ctx);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
// `accept`
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
177
|
+
// `accept` queue-integrates a PR into the protected `preview` aggregate (b08) —
|
|
178
|
+
// NO merge-to-main, NO go-live; going live is `tot ship`, which publishes the
|
|
179
|
+
// whole current GREEN AGGREGATE (b10). Backed by POST /api/changes/integrate
|
|
180
|
+
// (b07's TenantIntegrationQueue.enqueue); see accept.mjs's header. `merge` is a
|
|
181
|
+
// first-class alias, not a teaching nudge: both dispatch straight to the same command.
|
|
181
182
|
if (cmd === "accept" || cmd === "merge") {
|
|
182
183
|
const { run } = await import("../src/commands/accept.mjs");
|
|
183
184
|
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.15",
|
|
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
|
}
|
package/src/commands/pr.mjs
CHANGED
|
@@ -144,18 +144,32 @@ export function formatCandidateLine(c, { active = false } = {}) {
|
|
|
144
144
|
|
|
145
145
|
/**
|
|
146
146
|
* One-line summary of an OPEN operator-queue candidate for `tot pr list --tenant`.
|
|
147
|
-
*
|
|
148
|
-
*
|
|
147
|
+
* Two shapes come off `GET /api/changes`:
|
|
148
|
+
* - BUILT candidate (`built:true`, has a `changeId`): PR # / status / changeId /
|
|
149
|
+
* short head sha / preview URL — the fields an operator needs to pick a PR to ship.
|
|
150
|
+
* - NOT-BUILT forge PR (`built:false` / `changeId:null` / `status:"not-built"`): an
|
|
151
|
+
* open PR with no preview yet. Render it DISTINCTLY — no changeId (there is none),
|
|
152
|
+
* a `[not built]` tag, and a build HINT instead of a preview URL — so an operator
|
|
153
|
+
* sees it's listable but must `tot preview build` before it can ship.
|
|
149
154
|
* Adapts `formatCandidateLine`'s house style to the `GET /api/changes` shape (`status`
|
|
150
155
|
* not `state`; `previewUrl`; a `headSha` to short-render). Pure — unit-tested.
|
|
151
|
-
* @param {{prNumber?:number|null, status?:string|null, changeId
|
|
152
|
-
* headSha?:string|null, previewUrl?:string|null}} c
|
|
156
|
+
* @param {{prNumber?:number|null, status?:string|null, changeId?:string|null,
|
|
157
|
+
* headSha?:string|null, previewUrl?:string|null, built?:boolean}} c
|
|
153
158
|
* @returns {string}
|
|
154
159
|
*/
|
|
155
160
|
export function formatOperatorCandidateLine(c) {
|
|
156
161
|
const pr = typeof c.prNumber === "number" ? `#${c.prNumber}` : "#—";
|
|
157
|
-
const status = c.status ?? "?";
|
|
158
162
|
const head = c.headSha ? c.headSha.slice(0, 8) : "(no head)";
|
|
163
|
+
// A not-built PR: no changeId, so route the operator to build it first.
|
|
164
|
+
const notBuilt = c.built === false || (!c.changeId && c.status === "not-built");
|
|
165
|
+
if (notBuilt) {
|
|
166
|
+
const hint =
|
|
167
|
+
typeof c.prNumber === "number"
|
|
168
|
+
? ` → tot preview build --pr ${c.prNumber}`
|
|
169
|
+
: " → tot preview build";
|
|
170
|
+
return ` PR ${pr} [not built] ${head}${hint}`;
|
|
171
|
+
}
|
|
172
|
+
const status = c.status ?? "?";
|
|
159
173
|
const urlPart = c.previewUrl ? ` ${c.previewUrl}` : "";
|
|
160
174
|
return ` PR ${pr} [${status}] ${c.changeId} ${head}${urlPart}`;
|
|
161
175
|
}
|