@tokenoftrust/cli 1.4.1 → 1.5.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.
- package/README.md +5 -0
- package/bin/tot.mjs +58 -79
- package/package.json +6 -1
- package/src/activity.mjs +15 -14
- package/src/app-scaffold.mjs +4 -4
- package/src/auth.mjs +13 -5
- package/src/candidate-state.mjs +3 -3
- package/src/commands/accept.mjs +65 -38
- package/src/commands/app/dev.mjs +8 -4
- package/src/commands/app/index.mjs +3 -3
- package/src/commands/app/scaffold.mjs +1 -1
- package/src/commands/branches.mjs +4 -3
- package/src/commands/cleanup.mjs +7 -11
- package/src/commands/clone.mjs +23 -20
- package/src/commands/dev.mjs +42 -24
- package/src/commands/doctor.mjs +4 -4
- package/src/commands/git-credential.mjs +2 -2
- package/src/commands/go-live.mjs +9 -5
- package/src/commands/grants.mjs +7 -5
- package/src/commands/hotfix.mjs +1 -1
- package/src/commands/ideas.mjs +2 -2
- package/src/commands/link.mjs +2 -2
- package/src/commands/login.mjs +5 -6
- package/src/commands/pr.mjs +33 -19
- package/src/commands/preview-build.mjs +6 -6
- package/src/commands/preview-doctor.mjs +225 -0
- package/src/commands/preview-retry-evidence.mjs +156 -0
- package/src/commands/preview.mjs +19 -3
- package/src/commands/revert.mjs +5 -5
- package/src/commands/rollback.mjs +18 -16
- package/src/commands/ship.mjs +29 -12
- package/src/commands/start.mjs +61 -51
- package/src/commands/submit.mjs +360 -50
- package/src/commands/sync.mjs +2 -2
- package/src/commands/validate.mjs +4 -3
- package/src/commands/whoami.mjs +1 -1
- package/src/dev-heartbeat.mjs +3 -2
- package/src/dev-logs.mjs +2 -2
- package/src/errors.mjs +11 -4
- package/src/git-credential.mjs +94 -21
- package/src/last-tenant.mjs +1 -1
- package/src/mcp.mjs +6 -1
- package/src/merge-doctor-report.mjs +208 -0
- package/src/oauth.mjs +18 -14
- package/src/obstacle-beacon.cjs +2 -2
- package/src/obstacle.mjs +1 -1
- package/src/plan.mjs +16 -21
- package/src/sample.mjs +4 -4
- package/src/validate.mjs +135 -15
- package/src/vendor/private-apps-devkit.mjs +3 -3
- package/src/viewer-session.mjs +118 -0
- package/template/private-app/README.md +12 -6
- package/src/commands/retire.mjs +0 -203
package/src/commands/ship.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tot ship` — publish the tenant's CURRENT GREEN AGGREGATE to live. ONE
|
|
3
|
-
* meaning
|
|
4
|
-
*
|
|
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
|
|
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` —
|
|
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
|
|
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`
|
|
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 —
|
|
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 —
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
/**
|
package/src/commands/start.mjs
CHANGED
|
@@ -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()
|
|
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
|
-
*
|
|
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
|
|
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
|
-
* (
|
|
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
|
-
*
|
|
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
|
|
25
|
+
* open the browser
|
|
26
26
|
* ↓
|
|
27
|
-
* you're live crafted ending (elapsed time printed
|
|
28
|
-
* IDEAS[0]
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
258
|
-
// default (
|
|
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
|
-
//
|
|
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
|
|
281
|
-
// above; wait for the server, open the browser
|
|
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
|
|
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
|
|
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
|
|
318
|
-
//
|
|
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
|
|
467
|
-
* ends with the next command. Docker is informational-only here
|
|
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
|
|
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
|
|
486
|
-
* for serially afterward: the native renderer artifact by default (
|
|
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
|
|
528
|
-
* (self-healing
|
|
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
|
|
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
|
|
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: ${
|
|
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
|
|
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
|
-
|
|
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(
|
|
624
|
+
const idx = await promptChoice(many.stores.length);
|
|
620
625
|
console.log("");
|
|
621
|
-
tenant =
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
*/
|