@tokenoftrust/cli 1.4.0-rc.11 → 1.4.0-rc.13
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 +33 -0
- package/package.json +1 -1
- package/src/commands/accept.mjs +247 -0
- package/src/commands/go-live.mjs +482 -0
- package/src/commands/preview-build.mjs +225 -0
- package/src/commands/preview.mjs +9 -0
- package/src/commands/retire.mjs +203 -0
- package/src/commands/rollback.mjs +401 -0
- package/src/commands/ship.mjs +753 -43
- package/src/commands/submit.mjs +146 -46
- package/src/plan.mjs +160 -0
- package/src/sample.mjs +27 -1
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.
|
|
3
|
+
"version": "1.4.0-rc.13",
|
|
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
|
+
}
|