@tokenoftrust/cli 1.4.0-rc.2 → 1.4.0-rc.21
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/README.md +12 -9
- package/bin/tot.mjs +219 -44
- package/package.json +7 -2
- package/src/activity.mjs +379 -0
- package/src/app-scaffold.mjs +2 -2
- package/src/auth.mjs +13 -5
- package/src/candidate-state.mjs +137 -0
- package/src/commands/accept.mjs +736 -0
- package/src/commands/app/dev.mjs +7 -3
- package/src/commands/app/index.mjs +2 -2
- package/src/commands/branches.mjs +297 -0
- package/src/commands/cleanup.mjs +269 -0
- package/src/commands/clone.mjs +713 -0
- package/src/commands/dev.mjs +441 -93
- package/src/commands/doctor.mjs +4 -3
- package/src/commands/git-credential.mjs +180 -0
- package/src/commands/go-live.mjs +486 -0
- package/src/commands/grants.mjs +14 -7
- package/src/commands/hotfix.mjs +428 -0
- package/src/commands/link.mjs +225 -0
- package/src/commands/login.mjs +12 -8
- package/src/commands/pr.mjs +425 -0
- package/src/commands/preview-build.mjs +225 -0
- package/src/commands/preview.mjs +80 -0
- package/src/commands/retire.mjs +203 -0
- package/src/commands/revert.mjs +322 -0
- package/src/commands/rollback.mjs +403 -0
- package/src/commands/ship.mjs +517 -0
- package/src/commands/start.mjs +91 -29
- package/src/commands/submit.mjs +1360 -131
- package/src/commands/sync.mjs +203 -0
- package/src/commands/validate.mjs +11 -5
- package/src/commands/whoami.mjs +6 -2
- package/src/context.mjs +2 -2
- package/src/dev-heartbeat.mjs +2 -1
- package/src/errors.mjs +8 -4
- package/src/git-credential.mjs +185 -0
- package/src/mcp.mjs +6 -1
- package/src/no-gitea-links.test.mjs +55 -0
- package/src/oauth.mjs +26 -11
- package/src/obstacle-beacon.cjs +3 -3
- package/src/obstacle.mjs +1 -1
- package/src/plan.mjs +262 -0
- package/src/sample.mjs +30 -4
- package/src/validate.mjs +56 -0
- package/src/viewer-session.mjs +118 -0
- package/src/commands/checkout.mjs +0 -330
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
9
|
+
*
|
|
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.
|
|
15
|
+
*
|
|
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.
|
|
20
|
+
*
|
|
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".
|
|
28
|
+
*
|
|
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).
|
|
34
|
+
*
|
|
35
|
+
* Dependency-free (global fetch + the shared plan module).
|
|
36
|
+
*/
|
|
37
|
+
import { fail } from "../errors.mjs";
|
|
38
|
+
import { planForAction, printPlanAndConfirm } from "../plan.mjs";
|
|
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";
|
|
42
|
+
// Automate-first conflict recovery (workstream tot-merge-conflict-resolution-ux, unit
|
|
43
|
+
// u5): the ONE-CLICK `--refresh` path calls the `candidate_refresh` MCP tool (u1) —
|
|
44
|
+
// which has NO operator-secret HTTP route, so it is reached over the MCP client the
|
|
45
|
+
// same way `tot pr` reaches `candidate_status`/`candidate_close`.
|
|
46
|
+
import { createMcpClient } from "../mcp.mjs";
|
|
47
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
48
|
+
// No-operator-secret path: mint a viewer session from the developer's OWN `tot`
|
|
49
|
+
// login and integrate as themselves (server gates on their live ship-on-behalf grant).
|
|
50
|
+
import { resolveViewerTransport } from "../viewer-session.mjs";
|
|
51
|
+
|
|
52
|
+
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
53
|
+
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
54
|
+
|
|
55
|
+
// The three refresh strategies `candidate_refresh` (u1) accepts, mirroring the admin
|
|
56
|
+
// panel's one-click resolver (keep-mine / keep-current / smart-merge). `merge` is the
|
|
57
|
+
// DEFAULT: a real forge three-way merge that refuses cleanly (with the diverged file
|
|
58
|
+
// list) on a genuine same-line overlap rather than silently clobbering either side —
|
|
59
|
+
// exactly the automate-first posture (auto-resolve where safe, name the conflict when
|
|
60
|
+
// not, never a raw rebase instruction).
|
|
61
|
+
export const REFRESH_STRATEGIES = ["ours", "theirs", "merge"];
|
|
62
|
+
const DEFAULT_REFRESH_STRATEGY = "merge";
|
|
63
|
+
|
|
64
|
+
const USAGE = `tot accept — queue a PR's integration into the preview aggregate (alias: tot merge)
|
|
65
|
+
|
|
66
|
+
tot accept --tenant <t> --pr <N>
|
|
67
|
+
tot merge --tenant <t> --pr <N> (same command)
|
|
68
|
+
|
|
69
|
+
Queues the given PR for integration into the tenant's protected \`preview\`
|
|
70
|
+
aggregate: the serialized queue merges it into \`preview\` (preview-base only),
|
|
71
|
+
rebuilds the aggregate, runs combined evidence, and moves the shared preview
|
|
72
|
+
pointer ONLY when green. Accepting does NOT merge to main and does NOT go live —
|
|
73
|
+
a green aggregate is promoted live later by \`tot ship\`.
|
|
74
|
+
|
|
75
|
+
Integrating into the shared preview is a human decision: this ALWAYS prints the
|
|
76
|
+
exact plan and asks for an explicit confirm. There is no default-yes; a non-TTY
|
|
77
|
+
without --yes is refused rather than silently proceeding.
|
|
78
|
+
|
|
79
|
+
If the preview branch has moved under your change so it is no longer mergeable,
|
|
80
|
+
accept does NOT hand you a rebase — it offers to REBUILD the change on the current
|
|
81
|
+
preview tip for you. Pass --refresh to do it in one step (no local git), choosing
|
|
82
|
+
how to resolve any file that changed on both sides with --strategy.
|
|
83
|
+
|
|
84
|
+
Options:
|
|
85
|
+
--tenant <appDomain> Target tenant (e.g. tokenoftrust.com). Defaults to the
|
|
86
|
+
current checkout's tenant when run inside one.
|
|
87
|
+
--pr <N> PR number to integrate. The server resolves it to its
|
|
88
|
+
candidate from the tenant's queue (no --change-id needed).
|
|
89
|
+
--change-id <id> Target a specific change record instead of a PR number.
|
|
90
|
+
--head-sha <sha> Optional expected PR head sha (expectedHeadSha) — an
|
|
91
|
+
optimistic-concurrency guard against a PR that moved.
|
|
92
|
+
--refresh If the candidate isn't mergeable, rebuild it on the current
|
|
93
|
+
preview tip (via candidate_refresh) and then integrate — no
|
|
94
|
+
local rebase. Needs your \`tot login\` sign-in + promote access.
|
|
95
|
+
--strategy <s> How --refresh resolves a file changed on BOTH sides:
|
|
96
|
+
"merge" (real 3-way merge, refuses on a genuine conflict —
|
|
97
|
+
the default), "ours" (keep your version), "theirs" (keep
|
|
98
|
+
preview's version).
|
|
99
|
+
--url <origin> storefront origin (default: env TOT_STOREFRONT_URL)
|
|
100
|
+
--mcp <url> MCP base URL for --refresh (default: env TOT_MCP_URL)
|
|
101
|
+
--secret <s> operator secret (prefer the env vars below)
|
|
102
|
+
--yes, -y Skip the interactive confirm (still an explicit human
|
|
103
|
+
affirmative — there is no default-yes).
|
|
104
|
+
--help, -h Show this help.
|
|
105
|
+
|
|
106
|
+
Operator secret (from env, first found): PREVIEW_RECONCILE_SECRET,
|
|
107
|
+
GRANTS_ADMIN_SECRET, TOT_OPERATOR_SECRET (or pass --secret).`;
|
|
108
|
+
|
|
109
|
+
/** Parse `tot accept` / `tot merge` argv. Pure — unit-testable. */
|
|
110
|
+
export function parseAcceptArgs(argv) {
|
|
111
|
+
const a = {
|
|
112
|
+
tenant: null,
|
|
113
|
+
pr: null,
|
|
114
|
+
changeId: null,
|
|
115
|
+
headSha: null,
|
|
116
|
+
url: null,
|
|
117
|
+
mcp: null,
|
|
118
|
+
secret: null,
|
|
119
|
+
identity: null,
|
|
120
|
+
refresh: false,
|
|
121
|
+
strategy: null,
|
|
122
|
+
yes: false,
|
|
123
|
+
help: false,
|
|
124
|
+
};
|
|
125
|
+
for (let i = 0; i < argv.length; i++) {
|
|
126
|
+
const t = argv[i];
|
|
127
|
+
if (t === "--tenant") a.tenant = argv[++i];
|
|
128
|
+
else if (t === "--pr") a.pr = argv[++i];
|
|
129
|
+
else if (t === "--change-id") a.changeId = argv[++i];
|
|
130
|
+
else if (t === "--head-sha" || t === "--head") a.headSha = argv[++i];
|
|
131
|
+
else if (t === "--url") a.url = argv[++i];
|
|
132
|
+
else if (t === "--mcp") a.mcp = argv[++i];
|
|
133
|
+
else if (t === "--secret") a.secret = argv[++i];
|
|
134
|
+
else if (t === "--identity") a.identity = argv[++i];
|
|
135
|
+
else if (t === "--refresh") a.refresh = true;
|
|
136
|
+
else if (t === "--strategy") a.strategy = argv[++i];
|
|
137
|
+
else if (t === "--yes" || t === "-y") a.yes = true;
|
|
138
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
139
|
+
}
|
|
140
|
+
return a;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Normalise a `POST /api/changes/integrate` body to the honest terminal aggregate
|
|
145
|
+
* fields this verb renders. Reads defensively so a plausible field rename degrades
|
|
146
|
+
* rather than crashes. Pure — unit-tested.
|
|
147
|
+
* @param {any} data
|
|
148
|
+
* @returns {{ ok:boolean, queueState:string|null, runState:string|null,
|
|
149
|
+
* pointerMoved:boolean, aggregateSha:string|null, statusMessage:string|null,
|
|
150
|
+
* reason:string|null, changeId:string|null, prNumber:number|null, error:string|null, raw:any }}
|
|
151
|
+
*/
|
|
152
|
+
export function normalizeIntegrateResponse(data) {
|
|
153
|
+
const o = data && typeof data === "object" ? data : {};
|
|
154
|
+
return {
|
|
155
|
+
ok: o.ok === true,
|
|
156
|
+
queueState: typeof o.queueState === "string" ? o.queueState : null,
|
|
157
|
+
runState: typeof o.runState === "string" ? o.runState : null,
|
|
158
|
+
pointerMoved: o.pointerMoved === true,
|
|
159
|
+
aggregateSha: typeof o.aggregateSha === "string" ? o.aggregateSha : null,
|
|
160
|
+
statusMessage: typeof o.statusMessage === "string" ? o.statusMessage : null,
|
|
161
|
+
reason: typeof o.reason === "string" ? o.reason : null,
|
|
162
|
+
changeId: typeof o.changeId === "string" ? o.changeId : null,
|
|
163
|
+
prNumber: typeof o.prNumber === "number" ? o.prNumber : null,
|
|
164
|
+
error: typeof o.error === "string" ? o.error : null,
|
|
165
|
+
raw: data,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A human label for a candidate: its PR number when known, else its changeId. Pure. */
|
|
170
|
+
function candidateLabel(pr, changeId) {
|
|
171
|
+
return pr != null ? `PR #${pr}` : `change ${changeId}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Read the forge `mergeable` verdict for a candidate out of a `GET /api/changes`
|
|
176
|
+
* body (b07's operator queue — its BUILT entries carry the full ReviewEnvironment
|
|
177
|
+
* shape, incl. `mergeable`). This is the mergeable PREFLIGHT source: one read over the
|
|
178
|
+
* SAME operator-secret transport accept already uses, so a not-mergeable candidate is
|
|
179
|
+
* caught BEFORE a doomed integrate round-trips to the forge. Best-effort by design —
|
|
180
|
+
* `known:false` (proceed as normal) whenever the candidate isn't found or isn't built
|
|
181
|
+
* yet (no `mergeable`), so this never turns a transient listing gap into a false
|
|
182
|
+
* refusal. Pure — unit-tested.
|
|
183
|
+
* @param {any} data
|
|
184
|
+
* @param {{ pr:number|null, changeId:string|null }} target
|
|
185
|
+
* @returns {{ known:boolean, mergeable:boolean|null, changeId:string|null, prNumber:number|null }}
|
|
186
|
+
*/
|
|
187
|
+
export function readCandidateVerdict(data, { pr, changeId }) {
|
|
188
|
+
const list = Array.isArray(data)
|
|
189
|
+
? data
|
|
190
|
+
: data && Array.isArray(data.changes)
|
|
191
|
+
? data.changes
|
|
192
|
+
: [];
|
|
193
|
+
const wantPr =
|
|
194
|
+
typeof pr === "number"
|
|
195
|
+
? pr
|
|
196
|
+
: pr != null && `${pr}`.trim() && Number.isFinite(Number(pr))
|
|
197
|
+
? Number(pr)
|
|
198
|
+
: null;
|
|
199
|
+
const wantId = typeof changeId === "string" && changeId ? changeId : null;
|
|
200
|
+
const match =
|
|
201
|
+
list.find(
|
|
202
|
+
(c) =>
|
|
203
|
+
c &&
|
|
204
|
+
typeof c === "object" &&
|
|
205
|
+
((wantId && c.changeId === wantId) || (wantPr != null && c.prNumber === wantPr)),
|
|
206
|
+
) || null;
|
|
207
|
+
if (!match) return { known: false, mergeable: null, changeId: wantId, prNumber: wantPr };
|
|
208
|
+
const mergeable = match.mergeable === true ? true : match.mergeable === false ? false : null;
|
|
209
|
+
return {
|
|
210
|
+
known: mergeable !== null,
|
|
211
|
+
mergeable,
|
|
212
|
+
changeId: typeof match.changeId === "string" ? match.changeId : wantId,
|
|
213
|
+
prNumber: typeof match.prNumber === "number" ? match.prNumber : wantPr,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Normalise a `candidate_refresh` (u1) result read back over the MCP client.
|
|
219
|
+
* `status:"committed"` is the ONLY success (the candidate was rebuilt and is now
|
|
220
|
+
* mergeable); a `merge_failed` refusal carries `unresolved` (the diverged files);
|
|
221
|
+
* any other status is an honest refusal/error surfaced by its `message`. Reads
|
|
222
|
+
* defensively — a plausible field rename degrades, never crashes. Pure — unit-tested.
|
|
223
|
+
* @param {any} data
|
|
224
|
+
*/
|
|
225
|
+
export function normalizeRefreshResult(data) {
|
|
226
|
+
const o = data && typeof data === "object" ? data : {};
|
|
227
|
+
const ok = o.status === "committed";
|
|
228
|
+
return {
|
|
229
|
+
ok,
|
|
230
|
+
status: typeof o.status === "string" ? o.status : ok ? "committed" : "error",
|
|
231
|
+
changeId: typeof o.changeId === "string" ? o.changeId : null,
|
|
232
|
+
prNumber: typeof o.prNumber === "number" ? o.prNumber : null,
|
|
233
|
+
mergeable: typeof o.mergeable === "boolean" ? o.mergeable : null,
|
|
234
|
+
strategy: typeof o.strategy === "string" ? o.strategy : null,
|
|
235
|
+
refreshedFiles: Array.isArray(o.refreshedFiles)
|
|
236
|
+
? o.refreshedFiles.filter((x) => typeof x === "string")
|
|
237
|
+
: [],
|
|
238
|
+
unresolved: Array.isArray(o.unresolved)
|
|
239
|
+
? o.unresolved.filter((x) => typeof x === "string")
|
|
240
|
+
: [],
|
|
241
|
+
message: typeof o.message === "string" ? o.message : null,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The automate-first OFFER printed when a candidate isn't mergeable and --refresh was
|
|
247
|
+
* NOT passed: the one-click rebuild, never a raw git/rebase instruction and never the
|
|
248
|
+
* banned "resolve the conflict, then retry". Pure — returns the lines to print.
|
|
249
|
+
* @param {string} tenant
|
|
250
|
+
* @param {{ pr:number|null, changeId:string|null }} target
|
|
251
|
+
* @returns {string[]}
|
|
252
|
+
*/
|
|
253
|
+
export function notMergeableOfferLines(tenant, { pr, changeId }) {
|
|
254
|
+
const sel = pr != null ? `--pr ${pr}` : `--change-id ${changeId}`;
|
|
255
|
+
const cmd = `tot accept --tenant ${tenant} ${sel}`;
|
|
256
|
+
return [
|
|
257
|
+
" Rebuild it automatically on the current preview tip — no local checkout, one command:",
|
|
258
|
+
` ${cmd} --refresh smart 3-way merge (the default)`,
|
|
259
|
+
` ${cmd} --refresh --strategy=ours keep your version on any clash`,
|
|
260
|
+
` ${cmd} --refresh --strategy=theirs take preview's version on any clash`,
|
|
261
|
+
" Nothing was integrated.",
|
|
262
|
+
];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Run `candidate_refresh` (u1) over the MCP client — the ONE-CLICK rebuild of a
|
|
267
|
+
* not-mergeable candidate onto the current preview tip. `candidate_refresh` has no
|
|
268
|
+
* operator-secret HTTP route, so this reaches it exactly as `tot pr` reaches
|
|
269
|
+
* `candidate_status`: an OAuth developer session (`tot login`) + `client_switch` to
|
|
270
|
+
* bind the tenant scope. `createClient`/`establishSession` are injected so it's
|
|
271
|
+
* unit-tested with no live MCP.
|
|
272
|
+
*
|
|
273
|
+
* @param {{ tenant:string, changeId:string, strategy:string, mcpUrl?:string|null,
|
|
274
|
+
* identity?:string|null, env?:NodeJS.ProcessEnv }} params
|
|
275
|
+
* @param {{ createClient?:typeof createMcpClient, establishSession?:typeof establishSession }} [deps]
|
|
276
|
+
* @returns {Promise<ReturnType<typeof normalizeRefreshResult> & { hint?:string|null }>}
|
|
277
|
+
*/
|
|
278
|
+
export async function runRefresh(
|
|
279
|
+
{ tenant, changeId, strategy, mcpUrl = null, identity = null, env = process.env },
|
|
280
|
+
deps = {},
|
|
281
|
+
) {
|
|
282
|
+
const create = deps.createClient || createMcpClient;
|
|
283
|
+
const establish = deps.establishSession || establishSession;
|
|
284
|
+
const baseUrl = mcpUrl || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
285
|
+
const client = create(baseUrl);
|
|
286
|
+
try {
|
|
287
|
+
await establish(client, { env, prefer: identity || undefined });
|
|
288
|
+
// Bind the active tenant so candidate_refresh resolves the right scope (mirrors
|
|
289
|
+
// `tot pr`). Best-effort — the tool also takes `repo` explicitly.
|
|
290
|
+
try {
|
|
291
|
+
await client.callTool("client_switch", { tenant });
|
|
292
|
+
} catch {
|
|
293
|
+
/* best-effort scope bind */
|
|
294
|
+
}
|
|
295
|
+
const raw = await client.callTool("candidate_refresh", { repo: tenant, changeId, strategy });
|
|
296
|
+
return normalizeRefreshResult(raw);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
if (e instanceof AuthUnavailableError) {
|
|
299
|
+
return { ...normalizeRefreshResult(null), status: "auth", message: e.message, hint: e.hint };
|
|
300
|
+
}
|
|
301
|
+
return { ...normalizeRefreshResult(null), status: "error", message: String(e?.message || e) };
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Render a `candidate_refresh` FAILURE in the automate-first house style — a genuine
|
|
307
|
+
* conflict names the diverged files and offers the ours/theirs escape hatch; an auth
|
|
308
|
+
* gap points to `tot login`; any other refusal surfaces the MCP's own message. NEVER
|
|
309
|
+
* prints raw git/rebase or the banned "resolve the conflict, then retry". Returns the
|
|
310
|
+
* process exit code (always 1). Pure given console.
|
|
311
|
+
*/
|
|
312
|
+
function reportRefreshFailure(rr, { tenant, pr, changeId, strategy }) {
|
|
313
|
+
const sel = pr != null ? `--pr ${pr}` : `--change-id ${changeId}`;
|
|
314
|
+
const cmd = `tot accept --tenant ${tenant} ${sel}`;
|
|
315
|
+
if (rr.status === "auth") {
|
|
316
|
+
console.error(
|
|
317
|
+
fail(
|
|
318
|
+
"auto-refresh needs your Token of Trust sign-in",
|
|
319
|
+
rr.hint || "run `tot login`, then re-run with --refresh",
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
if (rr.status === "merge_failed") {
|
|
325
|
+
console.log(
|
|
326
|
+
`\n ✗ couldn't auto-merge ${candidateLabel(pr, changeId)} onto ${tenant}'s current preview with --strategy=${strategy}.`,
|
|
327
|
+
);
|
|
328
|
+
if (rr.unresolved.length) {
|
|
329
|
+
console.log(" These files changed on both sides and need your call:");
|
|
330
|
+
for (const f of rr.unresolved) console.log(` - ${f}`);
|
|
331
|
+
}
|
|
332
|
+
console.log(" Re-run choosing which side wins on those files:");
|
|
333
|
+
console.log(` ${cmd} --refresh --strategy=ours keep your version`);
|
|
334
|
+
console.log(` ${cmd} --refresh --strategy=theirs take preview's version`);
|
|
335
|
+
console.log(" Nothing was integrated.");
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
// Any other refusal (not_found / not_open / base_not_found / a capability guard /
|
|
339
|
+
// an unexpected error): surface the MCP's own message honestly — never dressed up.
|
|
340
|
+
console.error(
|
|
341
|
+
fail(
|
|
342
|
+
rr.message || `couldn't refresh ${candidateLabel(pr, changeId)} (${rr.status})`,
|
|
343
|
+
"check the candidate is open and that you have promote access for this store",
|
|
344
|
+
),
|
|
345
|
+
);
|
|
346
|
+
return 1;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Decide + act on a NOT-mergeable candidate (the automate-first fork): without
|
|
351
|
+
* --refresh, print the one-click offer and stop (never a rebase); with --refresh,
|
|
352
|
+
* rebuild via `candidate_refresh` and, on success, hand back the (possibly new) PR
|
|
353
|
+
* handle so the caller integrates the rebuilt candidate. Returns
|
|
354
|
+
* `{ integrate:false, code }` to stop, or `{ integrate:true, pr, changeId }` to
|
|
355
|
+
* proceed.
|
|
356
|
+
*/
|
|
357
|
+
async function handleNotMergeable(
|
|
358
|
+
{ tenant, pr, changeId, strategy, refresh, mcpUrl, identity, env },
|
|
359
|
+
deps,
|
|
360
|
+
) {
|
|
361
|
+
if (!refresh) {
|
|
362
|
+
console.log(
|
|
363
|
+
`\n ✗ ${candidateLabel(pr, changeId)} isn't mergeable into ${tenant}'s preview — the preview branch moved under it.`,
|
|
364
|
+
);
|
|
365
|
+
for (const line of notMergeableOfferLines(tenant, { pr, changeId })) console.log(line);
|
|
366
|
+
return { integrate: false, code: 1 };
|
|
367
|
+
}
|
|
368
|
+
if (!changeId) {
|
|
369
|
+
// candidate_refresh keys off the stable changeId; a bare PR number we couldn't
|
|
370
|
+
// resolve to one (not built yet / listing gap) can't be auto-rebuilt.
|
|
371
|
+
console.error(
|
|
372
|
+
fail(
|
|
373
|
+
`can't auto-refresh ${candidateLabel(pr, changeId)} — its change id isn't resolved yet`,
|
|
374
|
+
`check it's built (\`tot pr list --tenant ${tenant}\`), or pass --change-id`,
|
|
375
|
+
),
|
|
376
|
+
);
|
|
377
|
+
return { integrate: false, code: 1 };
|
|
378
|
+
}
|
|
379
|
+
const rr = await runRefresh({ tenant, changeId, strategy, mcpUrl, identity, env }, deps);
|
|
380
|
+
if (!rr.ok) {
|
|
381
|
+
return { integrate: false, code: reportRefreshFailure(rr, { tenant, pr, changeId, strategy }) };
|
|
382
|
+
}
|
|
383
|
+
console.log(
|
|
384
|
+
`\n ✓ rebuilt ${candidateLabel(rr.prNumber ?? pr, changeId)} on ${tenant}'s current preview (${rr.strategy ?? strategy}) — now mergeable.`,
|
|
385
|
+
);
|
|
386
|
+
if (rr.refreshedFiles.length) console.log(` reapplied: ${rr.refreshedFiles.join(", ")}`);
|
|
387
|
+
// The rebuild opens a FRESH PR (prNumber may change; changeId is stable), so
|
|
388
|
+
// integrate by the new PR number + changeId and drop the now-stale head sha.
|
|
389
|
+
return { integrate: true, pr: rr.prNumber ?? pr, changeId };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** POST `/api/changes/integrate` and normalise the outcome. Returns { res, result }. */
|
|
393
|
+
async function postIntegrate({ base, authHeaders, body }, fetchImpl) {
|
|
394
|
+
const res = await fetchImpl(`${base}/api/changes/integrate`, {
|
|
395
|
+
method: "POST",
|
|
396
|
+
headers: { "content-type": "application/json", ...authHeaders },
|
|
397
|
+
body: JSON.stringify(body),
|
|
398
|
+
});
|
|
399
|
+
let data = {};
|
|
400
|
+
try {
|
|
401
|
+
data = await res.json();
|
|
402
|
+
} catch {
|
|
403
|
+
/* non-JSON / empty body */
|
|
404
|
+
}
|
|
405
|
+
return { res, result: normalizeIntegrateResponse(data) };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The integrate request body for a target. Pure. */
|
|
409
|
+
function integrateBody({ tenant, pr, changeId, headSha }) {
|
|
410
|
+
return {
|
|
411
|
+
repo: tenant,
|
|
412
|
+
...(pr != null ? { prNumber: pr } : {}),
|
|
413
|
+
...(changeId ? { changeId } : {}),
|
|
414
|
+
...(headSha ? { expectedHeadSha: headSha } : {}),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Render the terminal aggregate status in house style — the honest queue/run state,
|
|
420
|
+
* NEVER a bare "merged". Pure given its inputs; returns the process exit code.
|
|
421
|
+
* @param {ReturnType<typeof normalizeIntegrateResponse>} result
|
|
422
|
+
* @param {{ tenant:string, label:string }} ctx
|
|
423
|
+
* @returns {number}
|
|
424
|
+
*/
|
|
425
|
+
export function reportIntegrated(result, { tenant, label }) {
|
|
426
|
+
const state = `${result.queueState ?? "?"}/${result.runState ?? "—"}`;
|
|
427
|
+
if (result.ok) {
|
|
428
|
+
console.log(`\n ✓ queued ${label} into ${tenant}'s preview aggregate — it is GREEN.`);
|
|
429
|
+
console.log(` aggregate: ${state}${result.aggregateSha ? ` (${result.aggregateSha})` : ""}`);
|
|
430
|
+
if (result.pointerMoved) console.log(" the shared preview pointer moved to this aggregate.");
|
|
431
|
+
console.log(" → next: `tot ship` to promote this green aggregate live.");
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
// Already integrated (candidate_not_open): the PR merged before this attempt —
|
|
435
|
+
// a SUCCESS the operator is re-hearing, not a failure. Never render the
|
|
436
|
+
// fix-and-retry template for it (live confusion, 2026-08-20: it told an
|
|
437
|
+
// operator to re-submit a change that had already landed). Exit 0 — the
|
|
438
|
+
// desired end state ("this change is in the aggregate") already holds.
|
|
439
|
+
if (result.reason === "candidate_not_open") {
|
|
440
|
+
console.log(`\n ✓ ${label} was ALREADY integrated into ${tenant}'s preview aggregate — nothing left to accept.`);
|
|
441
|
+
console.log(" → next: `tot ship` to promote the aggregate live, or `tot revert` to pull the change back out.");
|
|
442
|
+
return 0;
|
|
443
|
+
}
|
|
444
|
+
// Honest non-green: the candidate did NOT land in the shippable aggregate.
|
|
445
|
+
console.log(`\n ✗ ${label} did NOT integrate into ${tenant}'s preview aggregate.`);
|
|
446
|
+
console.log(` aggregate: ${state}${result.reason ? ` (${result.reason})` : ""}`);
|
|
447
|
+
if (result.statusMessage) console.log(` why: ${result.statusMessage}`);
|
|
448
|
+
console.log(" → next: fix the candidate (re-`tot preview`), then re-run `tot accept`.");
|
|
449
|
+
return 1;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* The accept-means-integrate flow after args are parsed: state the exact plan,
|
|
454
|
+
* confirm, then POST `/api/changes/integrate` (operator-secret transport) and render
|
|
455
|
+
* the honest terminal aggregate status. `fetch`/`confirmPlan` injected so it is
|
|
456
|
+
* unit-tested with no network/TTY.
|
|
457
|
+
*
|
|
458
|
+
* @param {{ tenant:string, pr:number|null, changeId:string|null, headSha:string|null,
|
|
459
|
+
* secret:string, storefrontUrl?:string|null, yes?:boolean, refresh?:boolean,
|
|
460
|
+
* strategy?:string, mcpUrl?:string|null, identity?:string|null, env?:NodeJS.ProcessEnv }} params
|
|
461
|
+
* @param {{ fetch?:typeof fetch, confirmPlan?:typeof printPlanAndConfirm,
|
|
462
|
+
* resolveViewerTransport?:typeof resolveViewerTransport }} [deps]
|
|
463
|
+
* @returns {Promise<number>} process exit code
|
|
464
|
+
*/
|
|
465
|
+
export async function runIntegrate(
|
|
466
|
+
{
|
|
467
|
+
tenant,
|
|
468
|
+
pr,
|
|
469
|
+
changeId,
|
|
470
|
+
headSha,
|
|
471
|
+
secret,
|
|
472
|
+
storefrontUrl = null,
|
|
473
|
+
yes = false,
|
|
474
|
+
refresh = false,
|
|
475
|
+
strategy = DEFAULT_REFRESH_STRATEGY,
|
|
476
|
+
mcpUrl = null,
|
|
477
|
+
identity = null,
|
|
478
|
+
env = process.env,
|
|
479
|
+
},
|
|
480
|
+
deps = {},
|
|
481
|
+
) {
|
|
482
|
+
const fetchImpl = deps.fetch || globalThis.fetch;
|
|
483
|
+
const confirmPlan = deps.confirmPlan || printPlanAndConfirm;
|
|
484
|
+
const resolveViewer = deps.resolveViewerTransport || resolveViewerTransport;
|
|
485
|
+
let base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
|
|
486
|
+
const label = pr != null ? `PR #${pr}` : changeId;
|
|
487
|
+
|
|
488
|
+
// 1. State the EXACT plan (shared U10 affordance) — queue-integrate-into-preview,
|
|
489
|
+
// NO merge, NO go-live — and gate on an explicit confirm.
|
|
490
|
+
const planLines = planForAction({ action: "accept", tenant, pr, changeId, headSha });
|
|
491
|
+
const { confirmed, reason } = await confirmPlan(planLines, {
|
|
492
|
+
yes,
|
|
493
|
+
question: refresh
|
|
494
|
+
? `Queue ${label} for integration into ${tenant}'s preview aggregate (rebuilding it first if the preview moved)?`
|
|
495
|
+
: `Queue ${label} for integration into ${tenant}'s preview aggregate?`,
|
|
496
|
+
});
|
|
497
|
+
if (!confirmed) {
|
|
498
|
+
if (reason === "non-tty") {
|
|
499
|
+
console.error(
|
|
500
|
+
fail(
|
|
501
|
+
"refusing to integrate without confirmation on a non-TTY.",
|
|
502
|
+
"re-run with --yes (an explicit human affirmative), or from an interactive terminal.",
|
|
503
|
+
),
|
|
504
|
+
);
|
|
505
|
+
return 2;
|
|
506
|
+
}
|
|
507
|
+
console.log("Aborted — nothing was integrated.");
|
|
508
|
+
return 1;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// 2. Resolve the transport. Two routes to the SAME `/api/changes/integrate`:
|
|
512
|
+
// - OPERATOR SECRET (operators/CI): Bearer + X-Tot-Owner on the generic host.
|
|
513
|
+
// - VIEWER SESSION (an invited developer, no secret): mint a `tot_session` from
|
|
514
|
+
// their OWN `tot` login on the TENANT'S host and send it as a cookie. The
|
|
515
|
+
// server authorizes on their live ship-on-behalf grant either way.
|
|
516
|
+
// No `content-type` here: it's added per-POST; the mergeable preflight GET wants none.
|
|
517
|
+
let authHeaders;
|
|
518
|
+
if (secret) {
|
|
519
|
+
authHeaders = {
|
|
520
|
+
authorization: `Bearer ${secret}`,
|
|
521
|
+
"x-tot-owner": tenant,
|
|
522
|
+
"x-tot-capability": "ship-on-behalf",
|
|
523
|
+
};
|
|
524
|
+
} else {
|
|
525
|
+
const viewer = await resolveViewer({ tenant, env, fetchImpl });
|
|
526
|
+
if (!viewer.ok) {
|
|
527
|
+
console.error(fail(viewer.message, viewer.hint));
|
|
528
|
+
return 2;
|
|
529
|
+
}
|
|
530
|
+
base = viewer.base; // the tenant's own host — the dev-viewer admission is host-scoped
|
|
531
|
+
authHeaders = viewer.authHeaders;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 2.5. MERGEABLE PREFLIGHT — read the candidate's forge verdict from b07's queue
|
|
535
|
+
// (`GET /api/changes`, SAME transport) so a doomed accept never round-trips to
|
|
536
|
+
// the forge. Best-effort: on any listing gap it's `known:false` → proceed as
|
|
537
|
+
// normal. When it KNOWS the candidate isn't mergeable, take the automate-first
|
|
538
|
+
// fork (offer/do --refresh) instead of a raw rebase.
|
|
539
|
+
let curPr = pr;
|
|
540
|
+
let curChangeId = changeId;
|
|
541
|
+
let curHead = headSha;
|
|
542
|
+
const verdict = await preflightMergeable({ base, authHeaders, pr, changeId }, fetchImpl);
|
|
543
|
+
if (verdict.mergeable === false) {
|
|
544
|
+
const handled = await handleNotMergeable(
|
|
545
|
+
{
|
|
546
|
+
tenant,
|
|
547
|
+
pr: verdict.prNumber ?? curPr,
|
|
548
|
+
changeId: verdict.changeId ?? curChangeId,
|
|
549
|
+
strategy,
|
|
550
|
+
refresh,
|
|
551
|
+
mcpUrl,
|
|
552
|
+
identity,
|
|
553
|
+
env,
|
|
554
|
+
},
|
|
555
|
+
deps,
|
|
556
|
+
);
|
|
557
|
+
if (!handled.integrate) return /** @type {number} */ (handled.code);
|
|
558
|
+
curPr = handled.pr ?? curPr;
|
|
559
|
+
curChangeId = handled.changeId ?? curChangeId;
|
|
560
|
+
curHead = null; // the rebuilt PR has a fresh head; let the server re-resolve.
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// 3. POST the honest accept path (b07 queue enqueue) and render the terminal state.
|
|
564
|
+
let res;
|
|
565
|
+
let result;
|
|
566
|
+
try {
|
|
567
|
+
({ res, result } = await postIntegrate(
|
|
568
|
+
{ base, authHeaders, body: integrateBody({ tenant, pr: curPr, changeId: curChangeId, headSha: curHead }) },
|
|
569
|
+
fetchImpl,
|
|
570
|
+
));
|
|
571
|
+
} catch (e) {
|
|
572
|
+
console.error(
|
|
573
|
+
fail(`couldn't reach the integration queue at ${base}: ${String(e?.message || e)}`, "check --url / your network, then re-run"),
|
|
574
|
+
);
|
|
575
|
+
return 1;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// A pre-flight error (auth, unknown tenant, candidate not found) is an HTTP 4xx
|
|
579
|
+
// with `{ error }` and no queue verdict — surface it distinctly from a red run.
|
|
580
|
+
if (!res.ok && result.queueState == null) {
|
|
581
|
+
return reportIntegrateHttpError(result, res, { tenant, label });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// 4. A not_mergeable outcome the preflight MISSED (listing gap / a race between
|
|
585
|
+
// read and enqueue): take the same automate-first fork rather than the generic
|
|
586
|
+
// "fix the candidate" copy. Guarded by `!verdict.known` so a candidate the
|
|
587
|
+
// preflight already routed through --refresh can't loop here.
|
|
588
|
+
if (!result.ok && result.reason === "not_mergeable" && !verdict.known) {
|
|
589
|
+
const handled = await handleNotMergeable(
|
|
590
|
+
{ tenant, pr: curPr, changeId: result.changeId ?? curChangeId, strategy, refresh, mcpUrl, identity, env },
|
|
591
|
+
deps,
|
|
592
|
+
);
|
|
593
|
+
if (!handled.integrate) return /** @type {number} */ (handled.code);
|
|
594
|
+
try {
|
|
595
|
+
({ res, result } = await postIntegrate(
|
|
596
|
+
{
|
|
597
|
+
base,
|
|
598
|
+
authHeaders,
|
|
599
|
+
body: integrateBody({
|
|
600
|
+
tenant,
|
|
601
|
+
pr: handled.pr ?? curPr,
|
|
602
|
+
changeId: handled.changeId ?? curChangeId,
|
|
603
|
+
headSha: null,
|
|
604
|
+
}),
|
|
605
|
+
},
|
|
606
|
+
fetchImpl,
|
|
607
|
+
));
|
|
608
|
+
} catch (e) {
|
|
609
|
+
console.error(
|
|
610
|
+
fail(`couldn't reach the integration queue at ${base}: ${String(e?.message || e)}`, "check --url / your network, then re-run"),
|
|
611
|
+
);
|
|
612
|
+
return 1;
|
|
613
|
+
}
|
|
614
|
+
if (!res.ok && result.queueState == null) {
|
|
615
|
+
return reportIntegrateHttpError(result, res, { tenant, label });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// A not_mergeable that survived a refresh attempt (or arrived with --refresh unset
|
|
620
|
+
// on the preflight-known path): the honest one-click offer, never a raw rebase.
|
|
621
|
+
if (!result.ok && result.reason === "not_mergeable") {
|
|
622
|
+
console.log(
|
|
623
|
+
`\n ✗ ${label} still isn't mergeable into ${tenant}'s preview.`,
|
|
624
|
+
);
|
|
625
|
+
for (const line of notMergeableOfferLines(tenant, { pr: curPr, changeId: curChangeId })) {
|
|
626
|
+
console.log(line);
|
|
627
|
+
}
|
|
628
|
+
return 1;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return reportIntegrated(result, { tenant, label: /** @type {string} */ (label) });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** The mergeable PREFLIGHT read (best-effort). See readCandidateVerdict. */
|
|
635
|
+
async function preflightMergeable({ base, authHeaders, pr, changeId }, fetchImpl) {
|
|
636
|
+
try {
|
|
637
|
+
const res = await fetchImpl(`${base}/api/changes`, { method: "GET", headers: authHeaders });
|
|
638
|
+
if (!res || !res.ok) {
|
|
639
|
+
return { known: false, mergeable: null, changeId: changeId ?? null, prNumber: pr ?? null };
|
|
640
|
+
}
|
|
641
|
+
let data = {};
|
|
642
|
+
try {
|
|
643
|
+
data = await res.json();
|
|
644
|
+
} catch {
|
|
645
|
+
return { known: false, mergeable: null, changeId: changeId ?? null, prNumber: pr ?? null };
|
|
646
|
+
}
|
|
647
|
+
return readCandidateVerdict(data, { pr, changeId });
|
|
648
|
+
} catch {
|
|
649
|
+
return { known: false, mergeable: null, changeId: changeId ?? null, prNumber: pr ?? null };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Surface an integrate HTTP pre-flight error (4xx, no queue verdict). Returns exit 1. */
|
|
654
|
+
function reportIntegrateHttpError(result, res, { tenant, label }) {
|
|
655
|
+
const msg = result.error || `HTTP ${res.status}`;
|
|
656
|
+
console.error(
|
|
657
|
+
fail(
|
|
658
|
+
`the integration queue refused the request: ${msg}`,
|
|
659
|
+
res.status === 401 || res.status === 403
|
|
660
|
+
? "you need a live ship-on-behalf grant on this tenant (ask the store owner) — or an operator secret authorised for it"
|
|
661
|
+
: res.status === 404
|
|
662
|
+
? `check that ${label} has a built candidate in ${tenant}'s queue (\`tot pr list --tenant ${tenant}\`)`
|
|
663
|
+
: "check --tenant / --url / --pr, then re-run",
|
|
664
|
+
),
|
|
665
|
+
);
|
|
666
|
+
return 1;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* @param {string[]} argv
|
|
671
|
+
* @param {any} ctx
|
|
672
|
+
*/
|
|
673
|
+
export async function run(argv, ctx) {
|
|
674
|
+
const env = process.env;
|
|
675
|
+
const args = parseAcceptArgs(argv);
|
|
676
|
+
if (args.help) {
|
|
677
|
+
console.log(USAGE);
|
|
678
|
+
return 0;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const tenant = (args.tenant || ctx?.tenant || "").trim();
|
|
682
|
+
if (!tenant) {
|
|
683
|
+
console.error(
|
|
684
|
+
fail(
|
|
685
|
+
"no target tenant.",
|
|
686
|
+
"pass --tenant <appDomain> (e.g. --tenant tokenoftrust.com), or run inside a store checkout.",
|
|
687
|
+
),
|
|
688
|
+
);
|
|
689
|
+
return 2;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const prRaw = args.pr;
|
|
693
|
+
const pr =
|
|
694
|
+
prRaw != null && `${prRaw}`.trim() && Number.isFinite(Number(prRaw)) ? Number(prRaw) : null;
|
|
695
|
+
const changeId = (args.changeId || "").trim() || null;
|
|
696
|
+
if (pr == null && !changeId) {
|
|
697
|
+
console.error(
|
|
698
|
+
fail(
|
|
699
|
+
"no PR or change to integrate.",
|
|
700
|
+
"pass --pr <N> (the PR number to queue), or --change-id <id> to target a specific record.",
|
|
701
|
+
),
|
|
702
|
+
);
|
|
703
|
+
return 2;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const headSha = (args.headSha || "").trim() || null;
|
|
707
|
+
const storefrontUrl =
|
|
708
|
+
args.url || env.TOT_STOREFRONT_URL || env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL;
|
|
709
|
+
|
|
710
|
+
// --strategy only means something with --refresh; validate its value whenever given.
|
|
711
|
+
const strategy = (args.strategy || DEFAULT_REFRESH_STRATEGY).trim();
|
|
712
|
+
if (args.strategy != null && !REFRESH_STRATEGIES.includes(strategy)) {
|
|
713
|
+
console.error(
|
|
714
|
+
fail(
|
|
715
|
+
`unknown --strategy "${args.strategy}"`,
|
|
716
|
+
`use one of: ${REFRESH_STRATEGIES.join(", ")} (default "${DEFAULT_REFRESH_STRATEGY}")`,
|
|
717
|
+
),
|
|
718
|
+
);
|
|
719
|
+
return 2;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return await runIntegrate({
|
|
723
|
+
tenant,
|
|
724
|
+
pr,
|
|
725
|
+
changeId,
|
|
726
|
+
headSha,
|
|
727
|
+
secret: resolveOperatorSecret(args.secret, env),
|
|
728
|
+
storefrontUrl,
|
|
729
|
+
yes: args.yes,
|
|
730
|
+
refresh: args.refresh,
|
|
731
|
+
strategy,
|
|
732
|
+
mcpUrl: args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL,
|
|
733
|
+
identity: args.identity || null,
|
|
734
|
+
env,
|
|
735
|
+
});
|
|
736
|
+
}
|