@tokenoftrust/cli 1.4.0-rc.11 → 1.4.0-rc.12

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 CHANGED
@@ -15,6 +15,8 @@
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
17
  * tot ship promote a reconciled preview live ← built (diff-vs-live + y/N confirm → change_accept; refuses non-TTY / unreconciled)
18
+ * tot accept / tot merge merge a PR into main (operator verb) ← built (plan + y/N confirm → the SAME change_accept ship uses; no deploy; refuses non-TTY without --yes)
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)
18
20
  * tot pr list / view / close your candidate PRs ← built (candidate_status/candidate_close; gh-pr-shaped)
19
21
  * tot doctor check this machine is ready
20
22
  * tot ideas copy-paste AI prompts that reliably wow
@@ -63,6 +65,10 @@ tot — Token of Trust developer CLI
63
65
  tot dev run your store locally with save→reload
64
66
  tot preview push your store to a reviewable preview
65
67
  tot ship promote a reconciled preview live (diff → confirm → ship)
68
+ tot accept / tot merge merge a PR into main — operator verb, no deploy (plan → confirm → merge)
69
+ tot rollback [<version>] instant re-point to a prior live version (list → confirm → rollback)
70
+ tot retire evict a candidate PR's preview to reclaim space — operator verb, rebuildable (plan → confirm → evict)
71
+ tot go-live cut the apex domain over to the storefront (readiness → confirm → cutover)
66
72
  tot pr list / view / close your candidate PRs
67
73
  tot doctor check this machine is ready
68
74
  tot ideas copy-paste AI prompts that reliably wow
@@ -168,6 +174,33 @@ async function dispatch(cmd, rest, ctx) {
168
174
  return run(rest, ctx);
169
175
  }
170
176
 
177
+ // `accept` merges a PR into main (no deploy) — a DISTINCT operator verb from
178
+ // `ship`, backed by the SAME change_accept pipeline (see accept.mjs's header).
179
+ // `merge` is a first-class alias, not a teaching nudge: both dispatch straight
180
+ // to the same command.
181
+ if (cmd === "accept" || cmd === "merge") {
182
+ const { run } = await import("../src/commands/accept.mjs");
183
+ return run(rest, ctx);
184
+ }
185
+
186
+ if (cmd === "rollback") {
187
+ const { run } = await import("../src/commands/rollback.mjs");
188
+ return run(rest, ctx);
189
+ }
190
+
191
+ // `retire` evicts a candidate PR's hosted preview to reclaim space (unit U7) —
192
+ // a DISTINCT operator verb from `accept`/reject: retire touches no change
193
+ // lifecycle and is reversible-by-rebuild (`tot preview build`). See retire.mjs.
194
+ if (cmd === "retire") {
195
+ const { run } = await import("../src/commands/retire.mjs");
196
+ return run(rest, ctx);
197
+ }
198
+
199
+ if (cmd === "go-live") {
200
+ const { run } = await import("../src/commands/go-live.mjs");
201
+ return run(rest, ctx);
202
+ }
203
+
171
204
  if (cmd === "pr") {
172
205
  const { run } = await import("../src/commands/pr.mjs");
173
206
  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.11",
3
+ "version": "1.4.0-rc.12",
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",
@@ -0,0 +1,247 @@
1
+ /**
2
+ * `tot accept --tenant <t> --pr <N>` (alias: `tot merge`) — OPERATOR verb: MERGE
3
+ * a PR into main. DISTINCT from `tot ship`: per board decision
4
+ * `operator-verb-and-hosting-model`, accepting a PR merges it into main; main
5
+ * becomes default-hosted on preview + live only after the NEXT `tot ship` /
6
+ * deploy — accept itself does not deploy. (`tot ship` run by the developer in
7
+ * their own checkout stays accept-THEN-deploy in one gated step — see
8
+ * `ship-context-dependent-semantics` in `../plan.mjs`; this verb is the
9
+ * explicit, deploy-free half of that, for an operator who just wants the merge.)
10
+ *
11
+ * TRANSPORT — reuses the EXISTING accept pipeline `tot ship` already drives over
12
+ * MCP (merge → reconcile → promote → verify): the SAME `change_accept` tool,
13
+ * the SAME session/auth pattern (`createMcpClient` + `establishSession`), and
14
+ * the SAME result normaliser/poll (`normalizeChangeResult` / `pollChangeShipped`,
15
+ * imported straight from `./ship.mjs` rather than re-implemented). No new
16
+ * transport, no new wire contract.
17
+ *
18
+ * TARGET RESOLUTION — `--pr N --tenant t` names the PR, but there is no
19
+ * PR→change-record lookup on the wire, and even if there were, this CLI's own
20
+ * identity may not have cross-tenant forge read access to resolve one for a PR
21
+ * that isn't the developer's own (`resolveChangeRecordId` in ship.mjs only works
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).
29
+ *
30
+ * // u9: auto-resolve PR→changeId via forge PR-read (candidate_status), so an
31
+ * // operator who DOES have forge access can omit --change-id and pass --pr
32
+ * // alone. Not implemented here — deliberately out of scope for this unit.
33
+ *
34
+ * HUMAN GATE — merging to main is a decision a human makes at the keyboard, so
35
+ * this verb ALWAYS states the EXACT plan (shared `planForAction`, unit U10:
36
+ * which PR, which tenant, "merge into main", plus a note that live/preview
37
+ * don't move until the next ship/deploy) and requires an explicit confirm.
38
+ * `--yes` is still an explicit affirmative supplied on the command line — there
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).
41
+ *
42
+ * Dependency-free (the MCP client + the shared plan module).
43
+ */
44
+ import { createMcpClient } from "../mcp.mjs";
45
+ import { establishSession, AuthUnavailableError } from "../auth.mjs";
46
+ import { fail } from "../errors.mjs";
47
+ import { planForAction, printPlanAndConfirm } from "../plan.mjs";
48
+ import { normalizeChangeResult, pollChangeShipped } from "./ship.mjs";
49
+
50
+ const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
51
+
52
+ const USAGE = `tot accept — merge a PR into main (alias: tot merge)
53
+
54
+ tot accept --tenant <t> --pr <N> --change-id <chg_...>
55
+ tot merge --tenant <t> --pr <N> --change-id <chg_...> (same command)
56
+
57
+ Merges the given PR into main via the existing accept pipeline (merge →
58
+ reconcile → promote → verify) — the SAME gate \`tot ship\` uses. Distinct
59
+ from \`tot ship\`: accepting does NOT deploy. Main becomes default-hosted on
60
+ preview + live only after the next \`tot ship\` / deploy.
61
+
62
+ Merge-to-main is a human decision: this ALWAYS prints the exact plan and asks
63
+ for an explicit confirm. There is no default-yes; a non-TTY without --yes is
64
+ refused rather than silently proceeding.
65
+
66
+ Options:
67
+ --tenant <appDomain> Target tenant (e.g. tokenoftrust.com). Defaults to the
68
+ current checkout's tenant when run inside one.
69
+ --pr <N> PR number to merge (for the plan text / labeling).
70
+ --change-id <id> The change-record id (chg_...) to accept. REQUIRED —
71
+ there is no PR→changeId auto-resolution yet (u9); pass
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).
75
+ --head-sha <sha> Optional expected PR head sha (expectedHeadSha) — an
76
+ optimistic-concurrency guard against a PR that moved.
77
+ --mcp <url> MCP base URL (default: env MCP_BASE_URL / TOT_MCP_URL)
78
+ --identity <id> sign in as a specific identity for this accept
79
+ --yes, -y Skip the interactive confirm (still an explicit human
80
+ affirmative — there is no default-yes).
81
+ --help, -h Show this help.`;
82
+
83
+ /** Parse `tot accept` / `tot merge` argv. Pure — unit-testable. */
84
+ export function parseAcceptArgs(argv) {
85
+ const a = {
86
+ tenant: null,
87
+ pr: null,
88
+ changeId: null,
89
+ headSha: null,
90
+ mcp: null,
91
+ identity: null,
92
+ yes: false,
93
+ help: false,
94
+ };
95
+ for (let i = 0; i < argv.length; i++) {
96
+ const t = argv[i];
97
+ if (t === "--tenant") a.tenant = argv[++i];
98
+ else if (t === "--pr") a.pr = argv[++i];
99
+ else if (t === "--change-id") a.changeId = argv[++i];
100
+ else if (t === "--head-sha" || t === "--head") a.headSha = argv[++i];
101
+ else if (t === "--mcp") a.mcp = argv[++i];
102
+ else if (t === "--identity") a.identity = argv[++i];
103
+ else if (t === "--yes" || t === "-y") a.yes = true;
104
+ else if (t === "--help" || t === "-h") a.help = true;
105
+ }
106
+ return a;
107
+ }
108
+
109
+ /** Report the accept result in house style. Pure given its inputs. */
110
+ function reportAccepted(accept, status, { tenant, pr, changeId }) {
111
+ const label = pr != null ? `PR #${pr}` : changeId;
112
+ const merged = Boolean(status?.shipped || accept?.shipped);
113
+ if (merged) {
114
+ console.log(`\n ✓ merged ${label} into main for ${tenant}.`);
115
+ console.log(
116
+ " → next: `tot ship` (or an operator `tot ship`/deploy) to promote main to preview + live.",
117
+ );
118
+ return 0;
119
+ }
120
+ // The accept committed but the terminal state hasn't been observed yet (the
121
+ // pipeline may still be landing reconcile/promote/verify) — report honestly.
122
+ console.log(`\n ~ accept committed for ${label} on ${tenant}; the merge is landing now.`);
123
+ console.log(" → next: re-check shortly, then `tot ship` / deploy to go live.");
124
+ return 0;
125
+ }
126
+
127
+ /**
128
+ * @param {string[]} argv
129
+ * @param {any} ctx
130
+ */
131
+ export async function run(argv, ctx) {
132
+ const env = process.env;
133
+ const args = parseAcceptArgs(argv);
134
+ if (args.help) {
135
+ console.log(USAGE);
136
+ return 0;
137
+ }
138
+
139
+ const tenant = (args.tenant || ctx?.tenant || "").trim();
140
+ if (!tenant) {
141
+ console.error(
142
+ fail(
143
+ "no target tenant.",
144
+ "pass --tenant <appDomain> (e.g. --tenant tokenoftrust.com), or run inside a store checkout.",
145
+ ),
146
+ );
147
+ return 2;
148
+ }
149
+
150
+ const prRaw = args.pr;
151
+ const pr = prRaw != null && `${prRaw}`.trim() && Number.isFinite(Number(prRaw)) ? Number(prRaw) : null;
152
+ if (pr == null && !args.changeId) {
153
+ console.error(
154
+ fail("no PR or change to accept.", "pass --pr <N> (the PR number to merge) and --change-id <chg_...>."),
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).
165
+ console.error(
166
+ fail(
167
+ `no --change-id for PR #${pr} — can't auto-resolve it yet.`,
168
+ "pass --change-id <chg_...> (the change record a prior review step opened/readied), " +
169
+ "or run `tot ship` from the PR author's own checkout instead.",
170
+ ),
171
+ );
172
+ return 2;
173
+ }
174
+
175
+ const headSha = (args.headSha || "").trim() || null;
176
+
177
+ // Self-declaring: state the EXACT plan before acting (the shared plan module,
178
+ // unit U10) plus the accept-specific caveat that main doesn't go live/preview
179
+ // until the next ship/deploy.
180
+ const planLines = [
181
+ ...planForAction({ action: "accept", tenant, pr, changeId, headSha }),
182
+ " note: main becomes default-hosted on preview + live only after the next `tot ship` / deploy.",
183
+ ];
184
+ const { confirmed, reason } = await printPlanAndConfirm(planLines, {
185
+ yes: args.yes,
186
+ question: "Merge this into main?",
187
+ });
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
+ }