@tokenoftrust/cli 1.4.0-rc.9 → 1.4.1

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 CHANGED
@@ -9,7 +9,8 @@ tot clone # list the stores you can build on
9
9
  tot clone <tenant> my-store # mirrors `git clone`; dir defaults to <tenant>
10
10
  cd my-store
11
11
  tot dev # run it locally with save→reload — no Docker needed
12
- tot submit # (coming) submit it for preview
12
+ tot preview # bundle your edits into a compliance-reviewed preview
13
+ tot ship # promote a reconciled preview live (diff + one confirm)
13
14
  ```
14
15
 
15
16
  **Prerequisites: Node.js and an invite. Nothing else.** `tot dev` downloads the
@@ -23,9 +24,11 @@ be fetched.)
23
24
  | Command | Status | What it does |
24
25
  | --- | --- | --- |
25
26
  | `tot clone [<tenant>] [<dir>]` | **built** | Clone a store you're entitled to build on (mirrors `git clone`), with an authenticated remote configured. Dir defaults to `<tenant>`. No arg → list your stores. |
26
- | `tot validate` | next | Lint your store before you submit. |
27
+ | `tot validate` | next | Lint your store before you preview. |
27
28
  | `tot dev` | **built** | Run your store locally with save→reload — NATIVELY (no Docker; falls back to it with `--docker` or automatically if the native artifact isn't available). |
28
- | `tot submit` | next | Submit your store for preview (pushes the preview ref, reports the reconcile/compliance verdict + preview URL). |
29
+ | `tot preview` | **built** | Bundle your edits into a compliance-reviewed preview (validates, auto-commits the known content trees, pushes the preview ref, opens/updates a candidate PR, reports the reconcile/compliance verdict + preview URL). `tot submit` / `tot deploy` still work as teaching aliases for this same flow. |
30
+ | `tot ship` | **built** | Promote a reconciled preview live: always shows a diff-vs-live and asks for one `[y/N]` confirm (no `--yes`, refuses outside a terminal); records an approval request if you're not authorised to ship yourself. |
31
+ | `tot pr [list\|view\|close]` | **built** | See and manage the candidate PRs `tot preview` opens (`gh pr`-shaped). |
29
32
  | `tot doctor` | built | Check this machine is ready and show which context `tot` detected. |
30
33
 
31
34
  ## Context-aware
@@ -42,7 +45,7 @@ The same `tot` does the right thing wherever you run it (walks up like `git`):
42
45
 
43
46
  `tot` talks to the Token of Trust MCP (default `https://mcp.tokenoftrust.com`, override with `--mcp` or `MCP_BASE_URL`). It is **single-plane**: the only identity is **you**, signed in against the MCP over OAuth.
44
47
 
45
- - Run `tot login` once — it opens your browser (or falls back to a device code on a headless box), you sign in as yourself, and the session is cached at `~/.tot/credentials.json` and refreshed silently. Every later command (`tot clone`, `tot start`, `tot submit`, …) runs as you, with no re-auth. Entitlement is derived server-side from your ToT memberships.
48
+ - Run `tot login` once — it opens your browser (or falls back to a device code on a headless box), you sign in as yourself, and the session is cached at `~/.tot/credentials.json` and refreshed silently. Every later command (`tot clone`, `tot start`, `tot preview`, `tot ship`, …) runs as you, with no re-auth. Entitlement is derived server-side from your ToT memberships.
46
49
  - Not signed in? On a terminal, `tot start` / `tot clone` **offer to sign you in right there** and continue in-flow — no "run `tot login`, then re-run".
47
50
  - The old operator env-triple (`TOT_API_KEY` / `TOT_SECRET_KEY` / `TOT_APP_DOMAIN`) **no longer signs the CLI in** — tot-mcp went OAuth-first on 2026-07-23. If those vars are set, `tot` prints a one-line advisory and uses your `tot login` session anyway; it never reads them for auth.
48
51
 
package/bin/tot.mjs CHANGED
@@ -13,8 +13,14 @@
13
13
  * tot clone [<tenant>] clone a store you can build on ← built
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
- * tot submit submit your store for preview ← built (validate + push preview ref; MCP preview_status read-back)
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 publish the current green aggregate live ← built (GET/POST /api/changes/ship → b09 orchestrator; exact plan + y/N confirm; waits for VERIFIED live truth)
18
+ * tot accept / tot merge queue a PR into the preview aggregate ← built (b08: plan + y/N confirm → POST /api/changes/integrate = b07 queue enqueue; NO merge-to-main, NO go-live; refuses non-TTY without --yes)
19
+ * tot sync fetch `preview` + merge it into your branch ← built (b16: the common accept-conflict recovery path; local-only, stops safely on conflict)
20
+ * tot rollback [<version>] instant re-point to a prior live version ← built (u3 promotion_status/promotion_rollback seam; diff + y/N confirm; refuses non-TTY / ineligible)
17
21
  * tot pr list / view / close your candidate PRs ← built (candidate_status/candidate_close; gh-pr-shaped)
22
+ * tot branches full branch-cleanup report (every branch, not just open PRs) ← built (b14: candidate_list)
23
+ * tot cleanup owner-confirmed branch GC (deletes terminal branches only) ← built (b14: candidate_list + candidate_delete; --dry-run classifies only)
18
24
  * tot doctor check this machine is ready
19
25
  * tot ideas copy-paste AI prompts that reliably wow
20
26
  * tot feedback send a note to ToT + your recent CLI activity ← built (activity-log.mjs → feedback_submit MCP tool)
@@ -34,9 +40,10 @@ import { readFileSync } from "node:fs";
34
40
  import { detectContext } from "../src/context.mjs";
35
41
  import { printError } from "../src/errors.mjs";
36
42
  import { recordActivity, redactArgs } from "../src/activity-log.mjs";
43
+ import { emitActivity, capRendered } from "../src/activity.mjs";
37
44
  import { maybeNotifyUpdate } from "../src/update-check.mjs";
38
45
 
39
- const BUILD_ORDER = ["clone", "validate", "dev", "submit"];
46
+ const BUILD_ORDER = ["clone", "validate", "dev", "preview"];
40
47
 
41
48
  // CLI version (stamped into the activity log). Read from our own package.json; best-effort.
42
49
  const VERSION = (() => {
@@ -60,8 +67,18 @@ tot — Token of Trust developer CLI
60
67
  tot clone [<tenant>] clone a store you can build on
61
68
  tot validate lint your store before you submit
62
69
  tot dev run your store locally with save→reload
63
- tot submit submit your store for preview
70
+ tot preview push your store to a reviewable preview
71
+ tot ship publish the tenant's current green aggregate live (plan → confirm → ship)
72
+ tot accept / tot merge queue a PR into the preview aggregate — operator verb, no go-live (plan → confirm → integrate)
73
+ tot sync fetch \`preview\` and merge it into your local branch (accept-conflict recovery)
74
+ tot rollback [<version>] instant re-point to a prior live version (list → confirm → rollback)
75
+ tot revert --preview <PR|sha> remove already-integrated content from the preview aggregate via a new revert commit — no force-reset (plan → confirm → revert)
76
+ tot hotfix --pr <N> OWNER-ONLY exception: release an urgent fix from main to live, bypassing unshipped preview work (plan → confirm → release → auto forward-integrate)
77
+ tot retire evict a candidate PR's preview to reclaim space — operator verb, rebuildable (plan → confirm → evict)
78
+ tot go-live cut the apex domain over to the storefront (readiness → confirm → cutover)
64
79
  tot pr list / view / close your candidate PRs
80
+ tot branches full branch-cleanup report — every branch, any PR state, cleanup eligibility
81
+ tot cleanup owner-confirmed branch GC (--dry-run to classify only; deletes terminal branches only)
65
82
  tot doctor check this machine is ready
66
83
  tot ideas copy-paste AI prompts that reliably wow
67
84
  tot feedback "<msg>" send feedback to Token of Trust (attaches recent activity)
@@ -138,6 +155,15 @@ async function dispatch(cmd, rest, ctx) {
138
155
  return run(rest, ctx);
139
156
  }
140
157
 
158
+ // Plumbing, not a developer-facing verb (like git's own credential helpers,
159
+ // never listed in `git help`) — `tot clone` configures it as this checkout's
160
+ // `credential.helper`, and git alone invokes it (get/store/erase) on every
161
+ // fetch/push. See src/commands/git-credential.mjs's header (unit u10).
162
+ if (cmd === "git-credential") {
163
+ const { run } = await import("../src/commands/git-credential.mjs");
164
+ return run(rest, ctx);
165
+ }
166
+
141
167
  if (cmd === "validate") {
142
168
  const { run } = await import("../src/commands/validate.mjs");
143
169
  return run(rest, ctx);
@@ -148,8 +174,80 @@ async function dispatch(cmd, rest, ctx) {
148
174
  return run(rest, ctx);
149
175
  }
150
176
 
151
- if (cmd === "submit") {
152
- const { run } = await import("../src/commands/submit.mjs");
177
+ // dev preview → ship. `preview` is the first-class verb; `submit`/`deploy`
178
+ // reach the SAME preview flow as teaching aliases (preview.mjs prints a one-line
179
+ // hint nudging the developer onto `tot preview` / `tot ship`).
180
+ if (cmd === "preview") {
181
+ const { run } = await import("../src/commands/preview.mjs");
182
+ return run(rest, ctx);
183
+ }
184
+
185
+ if (cmd === "submit" || cmd === "deploy") {
186
+ const { run } = await import("../src/commands/preview.mjs");
187
+ return run(rest, ctx, { alias: cmd });
188
+ }
189
+
190
+ if (cmd === "ship") {
191
+ const { run } = await import("../src/commands/ship.mjs");
192
+ return run(rest, ctx);
193
+ }
194
+
195
+ // `accept` queue-integrates a PR into the protected `preview` aggregate (b08) —
196
+ // NO merge-to-main, NO go-live; going live is `tot ship`, which publishes the
197
+ // whole current GREEN AGGREGATE (b10). Backed by POST /api/changes/integrate
198
+ // (b07's TenantIntegrationQueue.enqueue); see accept.mjs's header. `merge` is a
199
+ // first-class alias, not a teaching nudge: both dispatch straight to the same command.
200
+ if (cmd === "accept" || cmd === "merge") {
201
+ const { run } = await import("../src/commands/accept.mjs");
202
+ return run(rest, ctx);
203
+ }
204
+
205
+ // `sync` is the common accept-conflict recovery path (P1 item 12): fetches the
206
+ // protected `preview` branch and merges it into the developer's local branch so
207
+ // they can resolve locally, then re-`tot preview`. Purely local — no push, no
208
+ // MCP call, never touches the shared `preview`/`main` refs themselves. See
209
+ // sync.mjs's header for the full contract.
210
+ if (cmd === "sync") {
211
+ const { run } = await import("../src/commands/sync.mjs");
212
+ return run(rest, ctx);
213
+ }
214
+
215
+ if (cmd === "rollback") {
216
+ const { run } = await import("../src/commands/rollback.mjs");
217
+ return run(rest, ctx);
218
+ }
219
+
220
+ // `revert --preview <PR|integration-sha>` removes already-integrated content from
221
+ // the protected `preview` aggregate via a NEW auditable revert commit (b21) — NO
222
+ // force-reset, NO touch to main/live. Backed by POST /api/changes/revert (b07's
223
+ // TenantIntegrationQueue.enqueueRevert). To undo something already LIVE, that's
224
+ // `tot rollback`, not this. See revert.mjs's header.
225
+ if (cmd === "revert") {
226
+ const { run } = await import("../src/commands/revert.mjs");
227
+ return run(rest, ctx);
228
+ }
229
+
230
+ // `hotfix --pr <N>` is the OWNER-ONLY EXCEPTION lane (b22): release an urgent fix
231
+ // from `main` to live while `preview` still holds other unshipped work, EXCLUDING
232
+ // that unshipped preview head, then automatically forward-integrate main → preview
233
+ // and re-validate. It is DELIBERATELY a distinct verb — never a `--base main` flag
234
+ // on `tot ship` (which publishes the whole green preview aggregate). Backed by
235
+ // GET/POST /api/changes/hotfix → b22's HotfixOrchestrator. See hotfix.mjs's header.
236
+ if (cmd === "hotfix") {
237
+ const { run } = await import("../src/commands/hotfix.mjs");
238
+ return run(rest, ctx);
239
+ }
240
+
241
+ // `retire` evicts a candidate PR's hosted preview to reclaim space (unit U7) —
242
+ // a DISTINCT operator verb from `accept`/reject: retire touches no change
243
+ // lifecycle and is reversible-by-rebuild (`tot preview build`). See retire.mjs.
244
+ if (cmd === "retire") {
245
+ const { run } = await import("../src/commands/retire.mjs");
246
+ return run(rest, ctx);
247
+ }
248
+
249
+ if (cmd === "go-live") {
250
+ const { run } = await import("../src/commands/go-live.mjs");
153
251
  return run(rest, ctx);
154
252
  }
155
253
 
@@ -158,6 +256,24 @@ async function dispatch(cmd, rest, ctx) {
158
256
  return run(rest, ctx);
159
257
  }
160
258
 
259
+ // `branches` is the FULL branch-cleanup report (every branch, any PR state) —
260
+ // distinct from `tot pr list`, which only shows OPEN PRs (P1 item 9). Read-only;
261
+ // always safe to run, and the report `tot cleanup` reuses before deleting anything.
262
+ if (cmd === "branches") {
263
+ const { run } = await import("../src/commands/branches.mjs");
264
+ return run(rest, ctx);
265
+ }
266
+
267
+ // `cleanup` is owner-confirmed BRANCH GC (P1 items 9/10) — an irreversible git-ref
268
+ // delete, distinct from `tot retire`'s hosted-preview eviction (rebuildable, no
269
+ // branch touched). `--dry-run` classifies and prints; it never deletes. Age alone
270
+ // never makes a branch eligible; main/preview are always kept; orphans are
271
+ // quarantined, never auto-deleted. See cleanup.mjs's header.
272
+ if (cmd === "cleanup") {
273
+ const { run } = await import("../src/commands/cleanup.mjs");
274
+ return run(rest, ctx);
275
+ }
276
+
161
277
  if (cmd === "app") {
162
278
  const { run } = await import("../src/commands/app/index.mjs");
163
279
  return run(rest, ctx);
@@ -189,12 +305,39 @@ async function dispatch(cmd, rest, ctx) {
189
305
  return 2;
190
306
  }
191
307
 
308
+ /**
309
+ * The safe `subcommand` value for a command's activity event. Only the fixed
310
+ * sub-dispatcher verbs (e.g. `app scaffold` / `app dev`) are surfaced — an
311
+ * arbitrary positional (a tenant name, a path, a code) is NEVER used, so the
312
+ * activity `subcommand` field can't carry PII even before redaction. Returns
313
+ * undefined for every command without a fixed subcommand vocabulary.
314
+ */
315
+ function safeSubcommand(cmd, rest) {
316
+ if (cmd === "app") {
317
+ const sub = rest.find((t) => t && !t.startsWith("-"));
318
+ if (sub === "scaffold" || sub === "dev") return sub;
319
+ }
320
+ return undefined;
321
+ }
322
+
192
323
  async function main() {
193
324
  const [cmd, ...rest] = process.argv.slice(2);
194
325
  const ctx = detectContext();
195
326
  const startedAt = Date.now();
327
+ const command = cmd || "(none)";
328
+ const subcommand = safeSubcommand(cmd, rest);
196
329
  let code = 0;
197
330
  let errMsg = null;
331
+
332
+ // D3: emit `cli.command.invoked` on start — best-effort, a SILENT no-op without a
333
+ // hosted-bridge credential (never a network call / never blocks). Fired without
334
+ // await so it adds no latency to the command; settled alongside the result below.
335
+ const invokedEmit = emitActivity({
336
+ action: "cli.command.invoked",
337
+ outcome: { status: "invoked" },
338
+ payload: { args: { command, ...(subcommand ? { subcommand } : {}), cliVersion: VERSION, node: process.version } },
339
+ });
340
+
198
341
  try {
199
342
  code = await dispatch(cmd, rest, ctx);
200
343
  return code;
@@ -202,17 +345,35 @@ async function main() {
202
345
  errMsg = e?.message || String(e);
203
346
  throw e;
204
347
  } finally {
348
+ const exitCode = errMsg ? 1 : (code ?? 0);
349
+ const durationMs = Date.now() - startedAt;
205
350
  // Best-effort activity breadcrumb (never throws, never blocks). `feedback`'s own
206
351
  // free-text message is omitted — it's user-typed and belongs only in the report.
207
352
  recordActivity({
208
353
  ts: new Date().toISOString(),
209
354
  v: VERSION,
210
- cmd: cmd || "(none)",
355
+ cmd: command,
211
356
  args: cmd === "feedback" ? ["«omitted»"] : redactArgs(rest),
212
- code: errMsg ? 1 : (code ?? 0),
213
- ms: Date.now() - startedAt,
357
+ code: exitCode,
358
+ ms: durationMs,
214
359
  ...(errMsg ? { err: String(errMsg).slice(0, 200) } : {}),
215
360
  });
361
+ // D3: emit `cli.command.result` (exit code + duration + a bounded/redacted
362
+ // rendered field). The house-style error text is capped + run through the JS
363
+ // redaction mirror; per the D0 catalog the `cli.*` rendered lane is empty-
364
+ // allowlisted, so it's DROPPED by design (the safe default for a high-volume
365
+ // action) — the emit path still exercises the mirror. Bounded-await here (with
366
+ // the invoked emit) so a live bridge flushes before the process exits; a no-op
367
+ // when there's no credential.
368
+ const resultEmit = emitActivity({
369
+ action: "cli.command.result",
370
+ outcome: { status: errMsg ? "failed" : "succeeded", durationMs },
371
+ payload: {
372
+ args: { command, ...(subcommand ? { subcommand } : {}), cliVersion: VERSION, exitCode, durationMs },
373
+ ...(errMsg ? { rendered: { output: capRendered(errMsg) } } : {}),
374
+ },
375
+ });
376
+ await Promise.allSettled([invokedEmit, resultEmit]);
216
377
  // Nudge if a newer/unsupported version exists (drawn from cache — instant),
217
378
  // and kick a detached registry refresh if stale. Never throws, never blocks.
218
379
  maybeNotifyUpdate(VERSION);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "1.4.0-rc.9",
3
+ "version": "1.4.1",
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",