@tokenoftrust/cli 1.4.1 → 2.0.0

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.
Files changed (53) hide show
  1. package/README.md +5 -0
  2. package/bin/tot.mjs +58 -79
  3. package/package.json +6 -1
  4. package/src/activity.mjs +15 -14
  5. package/src/app-scaffold.mjs +4 -4
  6. package/src/auth.mjs +13 -5
  7. package/src/candidate-state.mjs +3 -3
  8. package/src/commands/accept.mjs +65 -38
  9. package/src/commands/app/dev.mjs +8 -4
  10. package/src/commands/app/index.mjs +3 -3
  11. package/src/commands/app/scaffold.mjs +1 -1
  12. package/src/commands/branches.mjs +4 -3
  13. package/src/commands/cleanup.mjs +7 -11
  14. package/src/commands/clone.mjs +23 -20
  15. package/src/commands/dev.mjs +42 -24
  16. package/src/commands/doctor.mjs +4 -4
  17. package/src/commands/git-credential.mjs +2 -2
  18. package/src/commands/go-live.mjs +9 -5
  19. package/src/commands/grants.mjs +7 -5
  20. package/src/commands/hotfix.mjs +1 -1
  21. package/src/commands/ideas.mjs +2 -2
  22. package/src/commands/link.mjs +2 -2
  23. package/src/commands/login.mjs +5 -6
  24. package/src/commands/pr.mjs +33 -19
  25. package/src/commands/preview-build.mjs +6 -6
  26. package/src/commands/preview-doctor.mjs +225 -0
  27. package/src/commands/preview-retry-evidence.mjs +156 -0
  28. package/src/commands/preview.mjs +19 -3
  29. package/src/commands/revert.mjs +5 -5
  30. package/src/commands/rollback.mjs +18 -16
  31. package/src/commands/ship.mjs +29 -12
  32. package/src/commands/start.mjs +61 -51
  33. package/src/commands/submit.mjs +360 -50
  34. package/src/commands/sync.mjs +2 -2
  35. package/src/commands/validate.mjs +4 -3
  36. package/src/commands/whoami.mjs +1 -1
  37. package/src/dev-heartbeat.mjs +3 -2
  38. package/src/dev-logs.mjs +2 -2
  39. package/src/errors.mjs +11 -4
  40. package/src/git-credential.mjs +94 -21
  41. package/src/last-tenant.mjs +1 -1
  42. package/src/mcp.mjs +6 -1
  43. package/src/merge-doctor-report.mjs +208 -0
  44. package/src/oauth.mjs +18 -14
  45. package/src/obstacle-beacon.cjs +2 -2
  46. package/src/obstacle.mjs +1 -1
  47. package/src/plan.mjs +16 -21
  48. package/src/sample.mjs +4 -4
  49. package/src/validate.mjs +135 -15
  50. package/src/vendor/private-apps-devkit.mjs +3 -3
  51. package/src/viewer-session.mjs +118 -0
  52. package/template/private-app/README.md +12 -6
  53. package/src/commands/retire.mjs +0 -203
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * `tot ship` — publish the tenant's CURRENT GREEN AGGREGATE to live. ONE
3
- * meaning (unit b10 — SUPERSEDES the retired context-dependent ship, decision
4
- * `ship-context-dependent-semantics`, and its developer-accept-then-deploy /
5
- * operator-deploy-only / ship-any-built-PR branches):
3
+ * meaning — SUPERSEDES the retired context-dependent ship and its
4
+ * developer-accept-then-deploy / operator-deploy-only / ship-any-built-PR branches:
6
5
  *
7
6
  * tot dev run your store locally with save→reload
8
7
  * tot preview push it to a reviewable preview (validate → reconcile → compliance)
@@ -11,17 +10,17 @@
11
10
  * `tot ship` NEVER takes a PR/candidate target and NEVER infers context from
12
11
  * your checkout or who's running it. There is nothing to detect: it ships the
13
12
  * tenant's shared `preview` aggregate — the batch of PRs that integrated
14
- * cleanly and went green (b07's queue) — full stop. The candidate-level merge
13
+ * cleanly and went green — full stop. The candidate-level merge
15
14
  * is the separate `tot accept` verb (`accept.mjs`); ship never merges.
16
15
  *
17
16
  * THE FLOW:
18
17
  *
19
- * 1. GET the read-only PLAN from `/api/changes/ship` — b09's
18
+ * 1. GET the read-only PLAN from `/api/changes/ship` — the
20
19
  * `AggregateShipOrchestrator.plan()` exposed over HTTP: the pinned
21
20
  * aggregate sha, its content-addressed artifact digest, every included
22
21
  * PR, the rollback target (what a rollback would restore), and the
23
22
  * go-live paywall verdict. Zero side effects.
24
- * 2. Print the EXACT plan via the shared U10 affordance (`../plan.mjs`) and
23
+ * 2. Print the EXACT plan via the shared affordance (`../plan.mjs`) and
25
24
  * require ONE explicit confirm, defaulting to NO. `--yes` confirms
26
25
  * non-interactively; a non-TTY WITHOUT `--yes` REFUSES — nothing ships
27
26
  * without an explicit yes.
@@ -48,6 +47,11 @@ import { planForAction, printPlanAndConfirm } from "../plan.mjs";
48
47
  import { startProgress } from "../progress.mjs";
49
48
  import { openBrowser } from "../open.mjs";
50
49
  import { emitActivity } from "../activity.mjs";
50
+ // The doctor PUSH: on a refused/failed ship (most tellingly "not green"),
51
+ // auto-append the COMPACT summary over the SAME operator transport, so the operator
52
+ // sees WHICH changes are keeping the aggregate from going green without a separate
53
+ // `tot preview doctor` run.
54
+ import { autoSurfaceDoctor } from "../merge-doctor-report.mjs";
51
55
 
52
56
  const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
53
57
 
@@ -98,7 +102,7 @@ export function parseShipArgs(argv) {
98
102
  return a;
99
103
  }
100
104
 
101
- // ─── Operator-secret transport (shared with `tot pr list --tenant` / u16/u17) ────
105
+ // ─── Operator-secret transport (shared with `tot pr list --tenant`) ────
102
106
  //
103
107
  // `resolveOperatorSecret` / `normalizeChangesQueue` are the SAME helpers the
104
108
  // retired operator-by-PR ship path minted; `tot pr list --tenant` (`pr.mjs`)
@@ -204,7 +208,7 @@ export function liveUrlFor(tenant) {
204
208
  // ─── Response normalisation (defensive — one endpoint, but shapes may vary) ─────
205
209
 
206
210
  /**
207
- * Normalise a `GET /api/changes/ship` body — b09's `AggregateShipPlan` (a
211
+ * Normalise a `GET /api/changes/ship` body — the `AggregateShipPlan` (a
208
212
  * shippable aggregate) or `AggregateShipRefusal` (nothing shippable), read
209
213
  * defensively since it crossed the wire as JSON. Pure — unit-tested.
210
214
  * @param {any} data
@@ -245,7 +249,7 @@ export function normalizeShipPlan(data) {
245
249
  }
246
250
 
247
251
  /**
248
- * Normalise a `POST /api/changes/ship` body — b09's `AggregateShipResult`.
252
+ * Normalise a `POST /api/changes/ship` body — the `AggregateShipResult`.
249
253
  * `state` is the ONLY honest terminal-state authority: render "shipped live"
250
254
  * for `"shipped"` and nothing else. Pure — unit-tested.
251
255
  * @param {any} data
@@ -327,6 +331,14 @@ export async function runShip({ tenant, secret, storefrontUrl = null, yes = fals
327
331
  "x-tot-capability": "ship-on-behalf",
328
332
  };
329
333
 
334
+ // Auto-surface the merge-doctor on a refused/failed ship. Best-effort:
335
+ // fetches the hosted doctor over the SAME operator transport and prints the COMPACT
336
+ // summary; never throws and never touches the exit code. A clean/unreachable doctor
337
+ // prints nothing — so this only ever ADDS the "here's what's not green" context.
338
+ const surfaceDoctor = async () => {
339
+ for (const line of await autoSurfaceDoctor({ base, authHeaders, fetchImpl })) console.error(line);
340
+ };
341
+
330
342
  // 1. GET the read-only plan: the pinned sha + artifact digest + included PRs +
331
343
  // rollback target + paywall verdict. Zero side effects.
332
344
  let planRes;
@@ -356,10 +368,13 @@ export async function runShip({ tenant, secret, storefrontUrl = null, yes = fals
356
368
  const plan = normalizeShipPlan(planData);
357
369
  if (!plan.ok) {
358
370
  console.error(fail(plan.message, refusalNextStep(plan.reason)));
371
+ // A not-green / no-passed-run refusal is exactly what the doctor diagnoses —
372
+ // name the changes keeping the aggregate red right here.
373
+ await surfaceDoctor();
359
374
  return 1;
360
375
  }
361
376
 
362
- // 2. Print the EXACT plan via the shared U10 affordance, then require ONE
377
+ // 2. Print the EXACT plan via the shared affordance, then require ONE
363
378
  // explicit confirm (`--yes` non-interactive; a non-TTY without it refuses).
364
379
  const liveUrl = liveUrlFor(tenant);
365
380
  const planLines = planForAction({
@@ -416,7 +431,7 @@ export async function runShip({ tenant, secret, storefrontUrl = null, yes = fals
416
431
  progress?.stop();
417
432
 
418
433
  const shipResult = normalizeShipResult(shipData);
419
- // D3: emit the ship publish lifecycle event — `tot ship` does no LOCAL git op
434
+ // Emit the ship publish lifecycle event — `tot ship` does no LOCAL git op
420
435
  // (it's HTTP-orchestrated), so this marks the outcome of the publish step
421
436
  // itself, distinct from the outer command's invoked/result pair. Fire-and-
422
437
  // forget best-effort: a silent no-op without a hosted-bridge credential, never
@@ -433,7 +448,9 @@ export async function runShip({ tenant, secret, storefrontUrl = null, yes = fals
433
448
  payload: { args: { command: "ship", subcommand: "ship.publish" } },
434
449
  });
435
450
 
436
- return reportShipResult(shipResult, { tenant, liveUrl, noOpen, openUrl: deps.openUrl });
451
+ const code = reportShipResult(shipResult, { tenant, liveUrl, noOpen, openUrl: deps.openUrl });
452
+ if (code !== 0) await surfaceDoctor();
453
+ return code;
437
454
  }
438
455
 
439
456
  /**
@@ -4,30 +4,30 @@
4
4
  *
5
5
  * It COMPOSES the other commands' cores in-process (no shelling out to `tot …`):
6
6
  *
7
- * preflight collectChecks() (F — self-healing where it can be)
7
+ * preflight collectChecks() — self-healing where it can be
8
8
  * login establishSession() (developer OAuth; offers inline sign-in when
9
- * there's no session yet — no re-run — see tryResolveSession)
10
- * (A2 — run concurrently: neither gates the other; wait on the slower)
9
+ * there's no session yet — no re-run — see tryResolveSession),
10
+ * run concurrently: neither gates the other; wait on the slower
11
11
  * ↓
12
12
  * store normalizeStores(client_list) → auto-pick if exactly one, else
13
- * use --tenant, else the remembered last tenant, else choose (A4)
13
+ * use --tenant, else the remembered last tenant, else choose
14
14
  * ↓
15
15
  * checkout ─┬─ checkoutTenant() → ./<tenant>
16
16
  * runner ─┘ prefetch the runner: the native renderer artifact by default
17
- * (F3 — no Docker prerequisite), or the Docker registry
17
+ * (no Docker prerequisite), or the Docker registry
18
18
  * credential on --docker / as the automatic fallback if the
19
19
  * native artifact can't be fetched (same fallback `tot dev`
20
- * uses — see dev.mjs#NativeArtifactUnavailableError)
21
- * (C1 — run concurrently with checkout: both just need the
22
- * session resolved above)
20
+ * uses — see dev.mjs#NativeArtifactUnavailableError), run
21
+ * concurrently with checkout: both just need the
22
+ * session resolved above
23
23
  * ↓
24
24
  * dev spawnNativeDev() or spawnDevContainer() → wait for ready →
25
- * open the browser (C/D)
25
+ * open the browser
26
26
  * ↓
27
- * you're live crafted ending (elapsed time printed — A3), seeded with
28
- * IDEAS[0] (G3), THEN the optional "Connect Claude?" prompt: on
27
+ * you're live crafted ending (elapsed time printed), seeded with
28
+ * IDEAS[0], THEN the optional "Connect Claude?" prompt: on
29
29
  * yes, `claude mcp add` AND drop straight into that seeded
30
- * prompt (G2) — the payoff, not a pointer to go write one.
30
+ * prompt — the payoff, not a pointer to go write one.
31
31
  *
32
32
  * ZERO-LOGIN FREE TASTE (--sample / no session): when the user passes --sample,
33
33
  * or has no Token of Trust session (and doesn't decline), `tot start` SKIPS
@@ -36,11 +36,11 @@
36
36
  * ends on the SAME "connect the ToT MCP for the real power" opt-in. The MCP is
37
37
  * repositioned as the upsell, not a gate. See runSampleStart + sample.mjs.
38
38
  *
39
- * Value-first ordering (C): the running site + browser come BEFORE any mention of
39
+ * Value-first ordering: the running site + browser come BEFORE any mention of
40
40
  * connecting the MCP; Claude is the supercharge offered after the aha, not a gate.
41
- * ONE felt browser login (B2): `claude mcp add` only ever runs behind that final
41
+ * ONE felt browser login: `claude mcp add` only ever runs behind that final
42
42
  * opt-in — the core loop above needs nothing but the cached `tot login` session.
43
- * Every failure ends with the exact next command via the shared formatter (F).
43
+ * Every failure ends with the exact next command via the shared formatter.
44
44
  *
45
45
  * Dependency-free (node:readline/promises + the cores above).
46
46
  */
@@ -130,7 +130,7 @@ export function pickStore(stores) {
130
130
  /**
131
131
  * Decide which tenant to use, given the identity's available stores, an
132
132
  * explicit `--tenant` override, and a remembered "last tenant" from a prior
133
- * run (A4). Pure + exported so the precedence is unit-tested without any I/O.
133
+ * run. Pure + exported so the precedence is unit-tested without any I/O.
134
134
  * Precedence: explicit > single store (pickStore) > remembered (if still one
135
135
  * of the available stores) > ambiguous ("many" — caller must prompt).
136
136
  * @param {Array<{id:string,name:string}>} stores
@@ -191,7 +191,7 @@ export async function run(argv, ctx) {
191
191
 
192
192
  const startedAt = Date.now();
193
193
 
194
- // Explicit free taste: skip login/store/checkout entirely (A0 zero-login).
194
+ // Explicit free taste: skip login/store/checkout entirely (zero-login).
195
195
  if (decideStartMode({ sampleFlag: args.sample, hasSession: true }) === "sample") {
196
196
  return runSampleStart(args, ctx, env, startedAt);
197
197
  }
@@ -205,7 +205,7 @@ export async function run(argv, ctx) {
205
205
  const client = createMcpClient(baseUrl);
206
206
 
207
207
  // Resolve a session, tolerating a no-session / no-network condition so we can
208
- // offer the free local preview instead of dead-ending (B/A0).
208
+ // offer the free local preview instead of dead-ending.
209
209
  const session = await tryResolveSession(client, env, args);
210
210
  if (decideStartMode({ sampleFlag: false, hasSession: !!session }) === "sample") {
211
211
  if (!(await confirmSampleFallback(args))) {
@@ -216,18 +216,18 @@ export async function run(argv, ctx) {
216
216
  return await runSampleStart(args, ctx, env, startedAt);
217
217
  }
218
218
 
219
- // Authenticated path. Preflight (F) now that we know we're taking it — the
219
+ // Authenticated path. Preflight now that we know we're taking it — the
220
220
  // local machine checks (node/git/Docker) that the authed loop needs.
221
221
  await preflight(ctx, env);
222
222
  console.log(` ✓ signed in as ${describeIdentity(session)} via ${mcpOrigin(baseUrl)}`);
223
223
 
224
224
  // Resolve the activity-bridge credential + the hosted Developer Cockpit URL
225
225
  // ONCE, up front — both the pre-pick "keep the cockpit alive" heartbeat below
226
- // (b2) and the post-boot heartbeat + milestone banners (u2) use them.
226
+ // and the post-boot heartbeat + milestone banners use them.
227
227
  const bridgeEnv = activityBridgeEnv(env);
228
228
  const cockpitUrl = cockpitUrlFrom(bridgeEnv.TOT_DEV_ACTIVITY_URL);
229
229
 
230
- // b2 — a >1-store identity blocks on the interactive store picker below, and
230
+ // A >1-store identity blocks on the interactive store picker below, and
231
231
  // (before this) the hosted cockpit got NO heartbeat until the dev server
232
232
  // spawned much later, so a multi-store dev's cockpit sat on Step 1 forever
233
233
  // while the terminal quietly waited for a number. Start beating NOW (no local
@@ -236,7 +236,7 @@ export async function run(argv, ctx) {
236
236
  // the live localhost URL) takes over once we have it.
237
237
  const stopEarlyHeartbeat = startHeartbeatFromEnv(bridgeEnv, {});
238
238
 
239
- // 3. store — auto-pick, use --tenant, use the remembered one, or choose (A4).
239
+ // 3. store — auto-pick, use --tenant, use the remembered one, or choose.
240
240
  // Keep the raw client_list so a tool ERROR (unauthenticated / not-entitled) is
241
241
  // surfaced with who + where, not collapsed to an empty list (fb-...f45zg9).
242
242
  let tenant;
@@ -254,31 +254,31 @@ export async function run(argv, ctx) {
254
254
  }
255
255
 
256
256
  // 4. checkout → ./<tenant> (reuse an existing checkout on a re-run),
257
- // OVERLAPPED (C1) with prefetching the runner: the native artifact by
258
- // default (F3 — no Docker prerequisite), or the Docker registry
257
+ // OVERLAPPED with prefetching the runner: the native artifact by
258
+ // default (no Docker prerequisite), or the Docker registry
259
259
  // credential on --docker / as the automatic native-unavailable fallback.
260
260
  // Both are independent authenticated calls once login (above) has
261
261
  // resolved, so there's no reason to pay for them serially.
262
262
  const dir = resolve(process.cwd(), tenant);
263
263
  // Resolve a free port so the URL we open/poll/print matches what the runner
264
264
  // binds (Vite strictPort is off → a busy port would drift). No-op if free.
265
- const devArgs = {
265
+ const devArgs = /** @type {any} */ ({
266
266
  image: null, port: String(await firstFreePort(Number(args.port || 4321))), mcp: args.mcp,
267
267
  noLogin: false, noOpen: args.noOpen, docker: args.docker,
268
- };
269
- const runtime = { useDocker: args.docker, runnerDir: null };
268
+ });
269
+ const runtime = /** @type {{ useDocker: boolean, runnerDir: any }} */ ({ useDocker: args.docker, runnerDir: null });
270
270
  await Promise.all([
271
271
  ensureCheckout(client, tenant, dir, env),
272
272
  prefetchRuntime(client, devArgs, env, runtime, ctx),
273
273
  ]);
274
274
 
275
- // u3 — say EXACTLY where the code landed + THE one next step, so a freshly
275
+ // Say EXACTLY where the code landed + THE one next step, so a freshly
276
276
  // checked-out dev is never left wondering what to open. Absolute path (works
277
277
  // from any directory) and a single, unambiguous action.
278
278
  printCheckoutLanding(dir, cockpitUrl);
279
279
 
280
- // 5. dev — native by default (F3), Docker on --docker or the fallback
281
- // above; wait for the server, open the browser (C/D).
280
+ // 5. dev — native by default, Docker on --docker or the fallback
281
+ // above; wait for the server, open the browser.
282
282
  const ctxDev = detectContext(dir);
283
283
  let url, handle;
284
284
  if (runtime.useDocker) {
@@ -292,11 +292,11 @@ export async function run(argv, ctx) {
292
292
  handle = spawnNativeDev(runtime.runnerDir, dir, devArgs.port, { stdio: "piped", env: bridgeEnv });
293
293
  }
294
294
 
295
- // Heartbeat the hosted cockpit (G1) with the CLI version + this live localhost
295
+ // Heartbeat the hosted cockpit with the CLI version + this live localhost
296
296
  // URL for the life of the run — CLI-side, using the cached bridge credential
297
297
  // (no-op when none is cached). The runner keeps reporting file-saves itself.
298
298
  // Hand off from the pre-pick (URL-less) beat to this URL-bearing one so the
299
- // cockpit's live local link lights up exactly when the server is up (b2).
299
+ // cockpit's live local link lights up exactly when the server is up.
300
300
  stopEarlyHeartbeat();
301
301
  const stopHeartbeat = startHeartbeatFromEnv(bridgeEnv, { url, cwd: dir });
302
302
  handle.done.finally(() => stopHeartbeat());
@@ -314,8 +314,8 @@ export async function run(argv, ctx) {
314
314
  console.log(" ✓ opened your browser");
315
315
  }
316
316
 
317
- // 6. you're live (G) — land on the store ROOT (not /dev), print elapsed time
318
- // (A3) so the "instant" claim is measured. The "Connect Claude" step is
317
+ // 6. you're live — land on the store ROOT (not /dev), print elapsed time
318
+ // so the "instant" claim is measured. The "Connect Claude" step is
319
319
  // intentionally removed for now — a blocking prompt here meant Ctrl-C'ing it
320
320
  // tore down the dev server; revisit AI-connect as a non-blocking step later.
321
321
  printLiveEnding(tenant, url, formatElapsed(Date.now() - startedAt), cockpitUrl, dir);
@@ -463,10 +463,10 @@ async function confirmSampleFallback(args) {
463
463
  }
464
464
 
465
465
  /**
466
- * Self-healing preflight (F): run the readiness checks; a blocking failure
467
- * ends with the next command. Docker is informational-only here (F3: native
466
+ * Self-healing preflight: run the readiness checks; a blocking failure
467
+ * ends with the next command. Docker is informational-only here native
468
468
  * is the default runtime, so Docker not running doesn't block `tot start`
469
- * anymore) — its own readiness is handled lazily in prefetchDockerLogin(),
469
+ * anymore — its own readiness is handled lazily in prefetchDockerLogin(),
470
470
  * only when the run actually needs it (--docker or the native fallback).
471
471
  */
472
472
  async function preflight(ctx, env) {
@@ -482,8 +482,8 @@ async function preflight(ctx, env) {
482
482
  }
483
483
 
484
484
  /**
485
- * Prefetch the runner (C1) so it's overlapped with checkout instead of paid
486
- * for serially afterward: the native renderer artifact by default (F3 — no
485
+ * Prefetch the runner so it's overlapped with checkout instead of paid
486
+ * for serially afterward: the native renderer artifact by default (no
487
487
  * Docker prerequisite), or the Docker registry pull credential on --docker.
488
488
  *
489
489
  * On a native (entitled-artifact) fetch failure the runtime stays NATIVE: it
@@ -524,8 +524,8 @@ async function prefetchRuntime(client, devArgs, env, runtime, ctx) {
524
524
 
525
525
  /**
526
526
  * The Docker-path half of prefetchRuntime: registry login (reusing the
527
- * already-authenticated client, C1) plus a best-effort Docker auto-start
528
- * (self-healing — F) so buildContainerPlan doesn't have to hard-fail on a
527
+ * already-authenticated client) plus a best-effort Docker auto-start
528
+ * (self-healing) so buildContainerPlan doesn't have to hard-fail on a
529
529
  * Docker Desktop that just needs a nudge.
530
530
  */
531
531
  async function prefetchDockerLogin(client, devArgs, env) {
@@ -570,7 +570,7 @@ function noStoresError({ session, baseUrl, listErr, list = null }) {
570
570
  { next: "run `tot whoami` to check your session, or `tot login` again — then re-run `tot start`" },
571
571
  );
572
572
  }
573
- // Status-aware (card c2): an UNLINKED identity is pointed at `tot link`, not the
573
+ // Status-aware: an UNLINKED identity is pointed at `tot link`, not the
574
574
  // misleading "ask for a store invite" copy; the genuine zero-grants case keeps it.
575
575
  const g = noStoresGuidance(list);
576
576
  return new CliError(`signed in as ${who} via ${origin}, but ${g.headline}`, { next: g.next });
@@ -578,9 +578,13 @@ function noStoresError({ session, baseUrl, listErr, list = null }) {
578
578
 
579
579
  /**
580
580
  * Resolve the tenant to work on from the (normalized) store list, args, and
581
- * the remembered last tenant (A4) — then remember whatever was decided so the
581
+ * the remembered last tenant — then remember whatever was decided so the
582
582
  * next bare `tot start` doesn't have to ask again.
583
583
  */
584
+ /**
585
+ * @param {any} stores @param {any} args @param {any} env @param {string} baseUrl
586
+ * @param {{ session?: any, listErr?: string|null, list?: any }} [opts]
587
+ */
584
588
  async function resolveTenant(stores, args, env, baseUrl, { session = null, listErr = null, list = null } = {}) {
585
589
  const lastTenantPath = defaultLastTenantPath(env);
586
590
  const pick = pickTenant(stores, {
@@ -603,22 +607,23 @@ async function resolveTenant(stores, args, env, baseUrl, { session = null, listE
603
607
  } else {
604
608
  // Many stores, nothing remembered — choose. Non-interactive (no TTY /
605
609
  // --yes without a name) can't guess.
610
+ const many = /** @type {any} */ (pick);
606
611
  if (!isInteractive() || args.yes) {
607
612
  throw new CliError("you can build on several stores — pick one", {
608
- next: `tot start --tenant <tenant> (one of: ${pick.stores.map((s) => s.id).join(", ")})`,
613
+ next: `tot start --tenant <tenant> (one of: ${many.stores.map((s) => s.id).join(", ")})`,
609
614
  });
610
615
  }
611
616
  // Loud + unmissable: a >1-store dev is often looking at their browser
612
617
  // cockpit (which is waiting on this pick), so make the terminal shout for
613
- // their attention rather than sitting as a quiet one-liner (b2).
618
+ // their attention rather than sitting as a quiet one-liner.
614
619
  console.log("");
615
620
  console.log(" ⚑ ACTION NEEDED IN YOUR TERMINAL — you can build on several stores.");
616
621
  console.log(" Pick one here to light up your cockpit:\n");
617
- pick.stores.forEach((s, i) => console.log(` ${i + 1}. ${s.id}${s.name ? ` — ${s.name}` : ""}`));
622
+ many.stores.forEach((s, i) => console.log(` ${i + 1}. ${s.id}${s.name ? ` — ${s.name}` : ""}`));
618
623
  console.log("");
619
- const idx = await promptChoice(pick.stores.length);
624
+ const idx = await promptChoice(many.stores.length);
620
625
  console.log("");
621
- tenant = pick.stores[idx].id;
626
+ tenant = many.stores[idx].id;
622
627
  }
623
628
 
624
629
  writeLastTenant(lastTenantPath, { mcpUrl: baseUrl, tenant });
@@ -668,7 +673,7 @@ async function waitForBoot(url, handle) {
668
673
 
669
674
  /**
670
675
  * Format elapsed milliseconds as a short human string for the "you're live"
671
- * ending (A3 — measure, don't just claim, "instant"). Pure + exported so it's
676
+ * ending — measure, don't just claim, "instant". Pure + exported so it's
672
677
  * unit-tested without any I/O.
673
678
  * @param {number} ms
674
679
  * @returns {string|null}
@@ -679,12 +684,13 @@ export function formatElapsed(ms) {
679
684
  }
680
685
 
681
686
  /**
682
- * u3 — after checkout, tell the developer EXACTLY where their code landed and
687
+ * After checkout, tell the developer EXACTLY where their code landed and
683
688
  * THE single next step. `dir` is absolute (from resolve(cwd, tenant)) so the
684
689
  * "open this" path works no matter what directory they're in. When we know the
685
690
  * Developer Cockpit URL, the one next step is framed as "edit a line → see it in
686
691
  * your cockpit"; otherwise it's the same edit-to-see-it-reload action.
687
692
  */
693
+ /** @param {string} dir @param {string|null} [cockpitUrl] */
688
694
  function printCheckoutLanding(dir, cockpitUrl = null) {
689
695
  console.log("");
690
696
  console.log(` 📁 Your store code is at: ${dir}`);
@@ -720,6 +726,10 @@ function editorOpenLines(dir, { platform = process.platform, env = process.env }
720
726
  * an editor" suggestion, and the four-pane layout tip (this terminal, cockpit,
721
727
  * local preview, editor) so a build problem, a reload, and the code are never
722
728
  * more than a glance apart. */
729
+ /**
730
+ * @param {string} tenant @param {string} url @param {string|null} elapsed
731
+ * @param {string|null} [cockpitUrl] @param {string|null} [dir]
732
+ */
723
733
  function printLiveEnding(tenant, url, elapsed, cockpitUrl = null, dir = null) {
724
734
  const lines = [
725
735
  `✨ You're live.${elapsed ? ` (${elapsed})` : ""}`,
@@ -770,7 +780,7 @@ function printSampleLiveEnding(url, elapsed) {
770
780
  }
771
781
 
772
782
  /**
773
- * Offer + wire Claude (G2): `claude mcp add`, then — the payoff, not just a
783
+ * Offer + wire Claude: `claude mcp add`, then — the payoff, not just a
774
784
  * pointer — drop the user straight into an interactive Claude session already
775
785
  * seeded with IDEAS[0], so there's no blank slate to stare at.
776
786
  */