@mycadet/cli 0.12.7 → 0.12.9-dev.202610131452.9acab64

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 (185) hide show
  1. package/README.md +2 -2
  2. package/knip.json +1 -1
  3. package/node_modules/@mycadet/assistant-api/events/cloud-disconnected.ts +21 -0
  4. package/node_modules/@mycadet/assistant-api/events/host-bash.ts +1 -1
  5. package/node_modules/@mycadet/assistant-api/events/host-cu.ts +1 -1
  6. package/node_modules/@mycadet/assistant-api/events/host-file.ts +1 -1
  7. package/node_modules/@mycadet/assistant-api/events/notification-intent.ts +1 -1
  8. package/node_modules/@mycadet/assistant-api/events/show-cloud-login.ts +18 -0
  9. package/node_modules/@mycadet/assistant-api/index.ts +11 -11
  10. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/package.json +2 -2
  11. package/node_modules/@mycadet/{service-contracts/src/platform-credential.ts → assistant-api/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
  12. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  13. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  14. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  15. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  16. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  17. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  18. package/node_modules/@mycadet/assistant-api/responses/home.ts +1 -1
  19. package/node_modules/@mycadet/assistant-api/surfaces.ts +4 -4
  20. package/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
  21. package/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
  22. package/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  23. package/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  24. package/node_modules/@mycadet/environments/src/types.ts +6 -6
  25. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/__tests__/notification-avatar.test.ts +1 -1
  26. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/avatar-manifest/src/notification-avatar.ts +2 -2
  27. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/seeds.ts +6 -6
  28. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/shell.test.ts +1 -1
  29. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/environments/src/types.ts +6 -6
  30. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/package.json +2 -2
  31. package/node_modules/@mycadet/{assistant-api/node_modules/@mycadet/service-contracts/src/platform-credential.ts → local-mode/node_modules/@mycadet/service-contracts/src/cloud-credential.ts} +9 -9
  32. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  33. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  34. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  35. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  36. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  37. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  38. package/node_modules/@mycadet/local-mode/src/__tests__/pair.test.ts +5 -5
  39. package/node_modules/@mycadet/local-mode/src/__tests__/retire.test.ts +1 -1
  40. package/node_modules/@mycadet/local-mode/src/config.ts +2 -2
  41. package/node_modules/@mycadet/local-mode/src/index.ts +1 -1
  42. package/node_modules/@mycadet/local-mode/src/lockfile-contract.test.ts +3 -3
  43. package/node_modules/@mycadet/local-mode/src/lockfile-contract.ts +10 -5
  44. package/node_modules/@mycadet/local-mode/src/lockfile.test.ts +15 -15
  45. package/node_modules/@mycadet/local-mode/src/lockfile.ts +9 -9
  46. package/node_modules/@mycadet/local-mode/src/pair.ts +2 -2
  47. package/node_modules/@mycadet/local-mode/src/retire.ts +3 -3
  48. package/node_modules/@mycadet/local-mode/src/teleport-backup-policy.ts +2 -2
  49. package/node_modules/@mycadet/local-mode/src/unpair.ts +2 -2
  50. package/node_modules/@mycadet/service-contracts/package.json +2 -2
  51. package/node_modules/@mycadet/{local-mode/node_modules/@mycadet/service-contracts/src/platform-credential.ts → service-contracts/src/cloud-credential.ts} +9 -9
  52. package/node_modules/@mycadet/service-contracts/src/handles.ts +3 -3
  53. package/node_modules/@mycadet/service-contracts/src/index.ts +1 -1
  54. package/node_modules/@mycadet/service-contracts/src/ingress.ts +11 -5
  55. package/node_modules/@mycadet/service-contracts/src/remote-web-pairing.ts +1 -1
  56. package/node_modules/@mycadet/service-contracts/src/rpc.ts +14 -14
  57. package/node_modules/@mycadet/service-contracts/src/trust-rules.ts +5 -5
  58. package/package.json +6 -4
  59. package/src/__tests__/assistant-api-key-resolution.test.ts +6 -6
  60. package/src/__tests__/{assistant-client-platform-auth.test.ts → assistant-client-cloud-auth.test.ts} +10 -10
  61. package/src/__tests__/assistant-client-refresh.test.ts +2 -2
  62. package/src/__tests__/backup.test.ts +86 -91
  63. package/src/__tests__/client-token.test.ts +1 -1
  64. package/src/__tests__/client-tui-refresh.test.ts +1 -1
  65. package/src/__tests__/client-web-config.test.ts +1 -2
  66. package/src/__tests__/{platform-client.test.ts → cloud-client.test.ts} +59 -59
  67. package/src/__tests__/{platform-releases.test.ts → cloud-releases.test.ts} +28 -18
  68. package/src/__tests__/{cloudflare-tunnel.test.ts → cloudflare.test.ts} +1 -1
  69. package/src/__tests__/connect-import.test.ts +55 -39
  70. package/src/__tests__/debug.test.ts +38 -38
  71. package/src/__tests__/ensure-binary.test.ts +182 -0
  72. package/src/__tests__/guardian-token.test.ts +149 -56
  73. package/src/__tests__/hatch-provider-secrets.test.ts +2 -2
  74. package/src/__tests__/helpers/guardian-token.ts +22 -0
  75. package/src/__tests__/helpers/http.ts +10 -0
  76. package/src/__tests__/nginx-ingress.test.ts +15 -20
  77. package/src/__tests__/ngrok-spawn.test.ts +49 -0
  78. package/src/__tests__/orphan-detection.test.ts +1 -1
  79. package/src/__tests__/pair.test.ts +149 -85
  80. package/src/__tests__/{ps-platform-status.test.ts → ps-cloud-status.test.ts} +80 -26
  81. package/src/__tests__/ps-table.test.ts +79 -0
  82. package/src/__tests__/{recover.test.ts → restore.test.ts} +29 -28
  83. package/src/__tests__/retire.test.ts +58 -199
  84. package/src/__tests__/root-process-lockfile.test.ts +99 -0
  85. package/src/__tests__/sleep.test.ts +19 -19
  86. package/src/__tests__/statefulset.test.ts +8 -8
  87. package/src/__tests__/{tailscale-tunnel.test.ts → tailscale.test.ts} +2 -2
  88. package/src/__tests__/teleport-backup.test.ts +3 -3
  89. package/src/__tests__/teleport.test.ts +180 -211
  90. package/src/__tests__/tunnel.test.ts +490 -270
  91. package/src/__tests__/uninstall.test.ts +150 -9
  92. package/src/__tests__/upgrade-local.test.ts +3 -3
  93. package/src/__tests__/upgrade-replay-env.test.ts +12 -12
  94. package/src/__tests__/wake.test.ts +260 -43
  95. package/src/__tests__/workos-pkce.test.ts +2 -2
  96. package/src/commands/backup.ts +24 -24
  97. package/src/commands/client.ts +76 -78
  98. package/src/commands/completions.test.ts +104 -0
  99. package/src/commands/completions.ts +87 -0
  100. package/src/commands/debug.ts +20 -20
  101. package/src/commands/exec.ts +12 -12
  102. package/src/commands/flags.ts +2 -2
  103. package/src/commands/hatch.ts +27 -24
  104. package/src/commands/login.ts +22 -22
  105. package/src/commands/logs.ts +8 -8
  106. package/src/commands/pair.ts +140 -37
  107. package/src/commands/ps.ts +232 -86
  108. package/src/commands/restart.ts +30 -0
  109. package/src/commands/restore.ts +140 -22
  110. package/src/commands/retire.ts +48 -150
  111. package/src/commands/rollback.ts +20 -26
  112. package/src/commands/setup.ts +2 -2
  113. package/src/commands/sleep.ts +2 -2
  114. package/src/commands/teleport.ts +110 -129
  115. package/src/commands/terminal.ts +158 -18
  116. package/src/commands/tunnel.ts +156 -175
  117. package/src/commands/uninstall.ts +137 -54
  118. package/src/commands/unpair.ts +3 -3
  119. package/src/commands/upgrade.ts +30 -54
  120. package/src/commands/use.ts +1 -1
  121. package/src/commands/wake.ts +166 -50
  122. package/src/index.ts +18 -9
  123. package/src/lib/__tests__/{platform-client-signed-url.test.ts → cloud-client-signed-url.test.ts} +16 -16
  124. package/src/lib/__tests__/docker.test.ts +168 -20
  125. package/src/lib/__tests__/job-polling.test.ts +2 -2
  126. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  127. package/src/lib/__tests__/local-runtime-client.test.ts +4 -4
  128. package/src/lib/__tests__/runtime-url.test.ts +1 -1
  129. package/src/lib/assistant-api-key-resolution.ts +5 -5
  130. package/src/lib/assistant-client.ts +8 -8
  131. package/src/lib/assistant-config.ts +58 -12
  132. package/src/lib/{platform-client.ts → cloud-client.ts} +105 -99
  133. package/src/lib/cloud-login.ts +23 -24
  134. package/src/lib/{platform-releases.ts → cloud-releases.ts} +11 -9
  135. package/src/lib/cloud-values.ts +3 -0
  136. package/src/lib/color.ts +4 -0
  137. package/src/lib/config-utils.ts +1 -1
  138. package/src/lib/detached-process.ts +1 -1
  139. package/src/lib/docker.ts +300 -111
  140. package/src/lib/ensure-binary.ts +171 -0
  141. package/src/lib/environments/__tests__/paths.test.ts +29 -3
  142. package/src/lib/environments/__tests__/resolve.test.ts +26 -26
  143. package/src/lib/environments/paths.ts +16 -8
  144. package/src/lib/environments/resolve.ts +10 -12
  145. package/src/lib/flag-args.test.ts +1 -1
  146. package/src/lib/flag-args.ts +1 -1
  147. package/src/lib/guardian-token.ts +37 -5
  148. package/src/lib/hatch-local.ts +32 -29
  149. package/src/lib/hatch-next-steps.ts +5 -9
  150. package/src/lib/health-check.ts +6 -6
  151. package/src/lib/ingress-config.ts +8 -35
  152. package/src/lib/job-polling.ts +2 -2
  153. package/src/lib/live-voice/connection.ts +12 -12
  154. package/src/lib/live-voice/tunnel.test.ts +11 -11
  155. package/src/lib/live-voice/tunnel.ts +15 -15
  156. package/src/lib/local-runtime-client.ts +20 -20
  157. package/src/lib/local.ts +46 -34
  158. package/src/lib/nginx-ingress.ts +40 -15
  159. package/src/lib/pair.ts +1 -1
  160. package/src/lib/process.ts +1 -1
  161. package/src/lib/provider-secrets.ts +3 -3
  162. package/src/lib/retire-apple-container.ts +1 -1
  163. package/src/lib/runtime-url.ts +8 -8
  164. package/src/lib/statefulset.ts +2 -2
  165. package/src/lib/step-runner.ts +6 -1
  166. package/src/lib/sync-cloud-assistants.ts +19 -19
  167. package/src/lib/teleport-backup.ts +4 -4
  168. package/src/lib/terminal-client.ts +17 -17
  169. package/src/lib/terminal-session.ts +24 -24
  170. package/src/lib/{tunnel-edge.ts → tunnel.ts} +199 -11
  171. package/src/lib/tunnels/cadet.ts +29 -0
  172. package/src/lib/{cloudflare-tunnel.ts → tunnels/cloudflare.ts} +36 -15
  173. package/src/lib/tunnels/gateway-notify.ts +61 -0
  174. package/src/lib/{ngrok.ts → tunnels/ngrok.ts} +350 -89
  175. package/src/lib/{tailscale-tunnel.ts → tunnels/tailscale.ts} +53 -9
  176. package/src/lib/upgrade-lifecycle.ts +8 -21
  177. package/src/lib/upgrade-preflight.ts +2 -2
  178. package/src/lib/workos-pkce.ts +5 -5
  179. package/node_modules/@mycadet/assistant-api/events/platform-disconnected.ts +0 -21
  180. package/node_modules/@mycadet/assistant-api/events/show-platform-login.ts +0 -20
  181. package/src/commands/recover.ts +0 -131
  182. package/src/commands/roadmap.ts +0 -459
  183. package/src/commands/ssh.ts +0 -144
  184. package/src/commands/workflows.ts +0 -305
  185. package/src/lib/managed-cloud.ts +0 -3
package/src/lib/docker.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  ASSISTANT_INTERNAL_PORT,
28
28
  getDefaultPorts,
29
29
  } from "./environments/paths.js";
30
+ import { green, yellow } from "./color.js";
30
31
  import { getCurrentEnvironment } from "./environments/resolve.js";
31
32
  import { leaseGuardianToken } from "./guardian-token";
32
33
  import { logHatchNextSteps } from "./hatch-next-steps.js";
@@ -37,11 +38,11 @@ import {
37
38
  isLocalBuildRef,
38
39
  loadImageViaHost,
39
40
  } from "./host-image-loader.js";
40
- import { resolveOwnImageRefs } from "./platform-releases.js";
41
+ import { resolveOwnImageRefs } from "./cloud-releases.js";
41
42
  import { isDevVersion } from "./version-compat.js";
42
43
  import {
44
+ collectProviderApiKey,
43
45
  configureHatchProviderApiKey,
44
- formatProviderName,
45
46
  promptProviderChoice,
46
47
  resolveHatchProvider,
47
48
  shouldPromptForHatchProvider,
@@ -340,14 +341,96 @@ export function resolveDockerProviderCredentialSetupAction(options: {
340
341
  provider: string | null | undefined;
341
342
  guardianAccessToken?: string;
342
343
  detached: boolean;
344
+ /**
345
+ * Key — or an explicit skip ("") — collected from the user right after
346
+ * they picked the provider. When present (even empty), the user already
347
+ * answered at the prompt, so detached hatches configure (or report the
348
+ * missing key) instead of deferring to a second `cadet setup` run.
349
+ */
350
+ precollectedKey?: string;
343
351
  }): DockerProviderCredentialSetupAction {
344
352
  if (options.provider === undefined) return "skip";
345
353
  if (options.provider === null) return options.detached ? "skip" : "configure";
346
- if (options.detached) return "defer";
354
+ if (options.detached) {
355
+ if (options.precollectedKey !== undefined) {
356
+ return options.guardianAccessToken ? "configure" : "missing-token";
357
+ }
358
+ return "defer";
359
+ }
347
360
  if (!options.guardianAccessToken) return "missing-token";
348
361
  return "configure";
349
362
  }
350
363
 
364
+ /** Time to wait for the Docker daemon to answer `docker info` after a Colima start. */
365
+ const COLIMA_SETTLE_TIMEOUT_MS = 15_000;
366
+
367
+ /** Poll `docker info` until the daemon answers or the timeout elapses. */
368
+ async function waitForDockerDaemon(
369
+ execFn: typeof exec,
370
+ timeoutMs: number,
371
+ ): Promise<boolean> {
372
+ const deadline = Date.now() + timeoutMs;
373
+ for (;;) {
374
+ try {
375
+ await execFn("docker", ["info"]);
376
+ return true;
377
+ } catch {
378
+ if (Date.now() >= deadline) return false;
379
+ await new Promise((resolve) => setTimeout(resolve, 1000));
380
+ }
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Start Colima and wait for the Docker daemon to actually answer.
386
+ *
387
+ * `colima start` exiting 0 only means Colima's own state file says the VM is
388
+ * up — that state can be stale (zombie VM after a host sleep or crash), in
389
+ * which case start is a no-op ("already running, ignoring") while the daemon
390
+ * inside stays dead. So success is decided by re-probing `docker info`, and a
391
+ * failed probe triggers the manual-fix cycle: force-stop and start fresh.
392
+ * Never `colima delete` — the VM disk holds every container image and volume.
393
+ * `startArgs` carries extra flags (e.g. `--memory`) for a first start; the
394
+ * wake path starts an existing instance bare.
395
+ */
396
+ async function startColimaAndAwaitDaemon(
397
+ execFn: typeof exec,
398
+ log: (msg: string) => void,
399
+ opts: { startArgs?: string[]; settleTimeoutMs?: number } = {},
400
+ ): Promise<void> {
401
+ const startArgs = opts.startArgs ?? [];
402
+ const settleTimeoutMs = opts.settleTimeoutMs ?? COLIMA_SETTLE_TIMEOUT_MS;
403
+
404
+ try {
405
+ await execFn("colima", ["start", ...startArgs]);
406
+ } catch (err) {
407
+ // exec captures the child's output and only surfaces it in the error —
408
+ // print it so a failed start is never silent.
409
+ log(`⚠️ colima start failed: ${err instanceof Error ? err.message : err}`);
410
+ }
411
+
412
+ if (await waitForDockerDaemon(execFn, settleTimeoutMs)) return;
413
+
414
+ log(
415
+ "🔄 Colima state looks stale — restarting the VM (colima stop --force, then start)...",
416
+ );
417
+ await execFn("colima", ["stop", "--force"]).catch(() => {});
418
+ try {
419
+ await execFn("colima", ["start", ...startArgs]);
420
+ } catch (err) {
421
+ const message = err instanceof Error ? err.message : String(err);
422
+ throw new Error(
423
+ `Failed to restart Colima. Please run 'colima stop --force && colima start' manually.\n${message}`,
424
+ );
425
+ }
426
+ if (!(await waitForDockerDaemon(execFn, settleTimeoutMs))) {
427
+ throw new Error(
428
+ "Docker daemon is still not reachable after restarting Colima. " +
429
+ "Check `colima status` and `docker info`, then try again.",
430
+ );
431
+ }
432
+ }
433
+
351
434
  /**
352
435
  * Checks whether the `docker` CLI and daemon are available on the system.
353
436
  * Installs Colima and Docker via direct binary download if missing (no sudo
@@ -418,41 +501,9 @@ async function ensureDockerInstalled(): Promise<void> {
418
501
  }
419
502
 
420
503
  console.log("🚀 Docker daemon not running. Starting Colima...");
421
- try {
422
- await exec("colima", [
423
- "start",
424
- "--memory",
425
- String(COLIMA_DEFAULT_MEMORY_GIB),
426
- ]);
427
- } catch {
428
- // Colima may fail if a previous VM instance is in a corrupt state.
429
- // Attempt to delete the stale instance and retry once.
430
- console.log(
431
- "⚠️ Colima start failed — attempting to reset stale VM state...",
432
- );
433
- try {
434
- await exec("colima", ["stop", "--force"]).catch(() => {});
435
- await exec("colima", ["delete", "--force"]);
436
- } catch {
437
- // If delete also fails, fall through to the retry which will
438
- // produce a clear error message.
439
- }
440
-
441
- try {
442
- console.log("🔄 Retrying colima start...");
443
- await exec("colima", [
444
- "start",
445
- "--memory",
446
- String(COLIMA_DEFAULT_MEMORY_GIB),
447
- ]);
448
- } catch (retryErr) {
449
- const message =
450
- retryErr instanceof Error ? retryErr.message : String(retryErr);
451
- throw new Error(
452
- `Failed to start Colima after resetting stale VM state. Please run 'colima start' manually.\n${message}`,
453
- );
454
- }
455
- }
504
+ await startColimaAndAwaitDaemon(exec, console.log, {
505
+ startArgs: ["--memory", String(COLIMA_DEFAULT_MEMORY_GIB)],
506
+ });
456
507
  }
457
508
  }
458
509
 
@@ -486,6 +537,56 @@ export async function removeContainer(containerName: string): Promise<void> {
486
537
  }
487
538
  }
488
539
 
540
+ /**
541
+ * Error signatures Docker Hub returns when the host has STALE Docker Hub
542
+ * credentials: an expired personal access token saved by a past
543
+ * `docker login` gets attached to the pull, and Hub rejects the request
544
+ * instead of falling back to anonymous access for a public image.
545
+ */
546
+ export function isExpiredDockerHubCredentialError(err: unknown): boolean {
547
+ const message = err instanceof Error ? err.message : String(err);
548
+ return /personal access token is expired|authentication required|unauthorized/i.test(
549
+ message,
550
+ );
551
+ }
552
+
553
+ /**
554
+ * Pull an image, retrying once without stale credentials when Docker Hub
555
+ * rejects the pull with an expired-token/auth error — public pulls need no
556
+ * login, so `docker logout` (clears the stored Docker Hub credentials the
557
+ * docker CLI attaches to pulls) costs nothing. Other failures propagate raw;
558
+ * a still-failing auth retry is rethrown with actionable copy.
559
+ */
560
+ export async function pullImage(
561
+ ref: string,
562
+ log: (msg: string) => void = console.log,
563
+ execFn: typeof exec = exec,
564
+ ): Promise<void> {
565
+ try {
566
+ await execFn("docker", ["pull", ref]);
567
+ return;
568
+ } catch (err) {
569
+ if (!isExpiredDockerHubCredentialError(err)) throw err;
570
+ log(
571
+ `⚠️ Docker Hub rejected the pull of ${ref} with an expired-credential ` +
572
+ `error — clearing stale Docker Hub login and retrying (public images ` +
573
+ `need no credentials)...`,
574
+ );
575
+ await execFn("docker", ["logout"]).catch(() => {});
576
+ }
577
+ try {
578
+ await execFn("docker", ["pull", ref]);
579
+ } catch (err) {
580
+ const message = err instanceof Error ? err.message : String(err);
581
+ throw new Error(
582
+ `Failed to pull ${ref}.\n${message}\n` +
583
+ `If the Docker daemon is not running, start it, e.g. 'colima start' on macOS. ` +
584
+ `If the error mentions authentication, run 'docker logout' and check your ` +
585
+ `Docker Hub credentials — public image pulls need none.`,
586
+ );
587
+ }
588
+ }
589
+
489
590
  export async function retireDocker(name: string): Promise<void> {
490
591
  console.log(`\u{1F5D1}\ufe0f Stopping Docker containers for '${name}'...\n`);
491
592
 
@@ -785,6 +886,113 @@ export async function wakeContainers(
785
886
  }
786
887
  }
787
888
 
889
+ /** The `docker start` failure for a container that was never created. */
890
+ export function isNoSuchContainerError(err: unknown): boolean {
891
+ const msg = err instanceof Error ? err.message : String(err);
892
+ return msg.includes("No such container");
893
+ }
894
+
895
+ /**
896
+ * Recreate a docker instance's service group from its lockfile entry — the
897
+ * wake path for containers that don't exist yet. A migrated install has its
898
+ * data in the volumes but no containers: nothing ever ran `docker run` for
899
+ * it (v0.0.10 run feedback: `wake --all` died on "No such container:
900
+ * <name>-assistant").
901
+ *
902
+ * Network and volume creates are idempotent; the three service containers
903
+ * come up from the entry's recorded images (the migrator points those at
904
+ * the installed cadet's release). The guardian bootstrap secret rides
905
+ * through from the entry; the signing key and CES token are deliberately
906
+ * NOT invented — with no env value the gateway and assistant load the
907
+ * persisted trust material from their security volumes (a migrated install
908
+ * keeps its own), matching the gateway's resolution order (env → file →
909
+ * generate). Empty volumes just get fresh state on first boot.
910
+ */
911
+ export async function recreateDockerContainers(entry: AssistantEntry): Promise<void> {
912
+ const info = entry.containerInfo;
913
+ if (!info?.assistantImage || !info.gatewayImage || !info.cesImage) {
914
+ throw new Error(
915
+ `no container images recorded for '${entry.assistantId}' — re-hatch or run \`cadet upgrade\` to set them`,
916
+ );
917
+ }
918
+ const res = dockerResourceNames(entry.assistantId);
919
+ const log = (msg: string): void => console.log(msg);
920
+
921
+ // Clean slate for the create: an existing container (even a stopped one)
922
+ // would collide with `docker run --name` (the upgrade path does the same).
923
+ await stopContainers(res);
924
+
925
+ try {
926
+ await exec("docker", ["network", "inspect", res.network]);
927
+ } catch {
928
+ await exec("docker", ["network", "create", res.network]);
929
+ }
930
+ for (const vol of [
931
+ res.socketVolume,
932
+ res.assistantIpcVolume,
933
+ res.gatewayIpcVolume,
934
+ res.workspaceVolume,
935
+ res.gatewaySecurityVolume,
936
+ res.cesSecurityVolume,
937
+ ]) {
938
+ try {
939
+ await exec("docker", ["volume", "inspect", vol]);
940
+ } catch {
941
+ await exec("docker", ["volume", "create", vol]);
942
+ }
943
+ }
944
+
945
+ // The recorded images may not be on the machine yet (a migrated install
946
+ // gets its refs written before anything pulled them) — a pull of an
947
+ // already-present image is a cheap no-op.
948
+ for (const image of [info.assistantImage, info.gatewayImage, info.cesImage]) {
949
+ log(`📦 Pulling ${image}...`);
950
+ await pullImage(image, log);
951
+ }
952
+
953
+ const gatewayPort = parseGatewayPort(entry.runtimeUrl) ??
954
+ getDefaultPorts(getCurrentEnvironment()).gateway;
955
+ const assistantPort = info.assistantPort ?? ASSISTANT_INTERNAL_PORT;
956
+ const device = getOrCreateHostDeviceId();
957
+ await startContainers(
958
+ {
959
+ bootstrapSecret: entry.guardianBootstrapSecret,
960
+ extraAssistantEnv: { CADET_DEVICE_ID: device },
961
+ extraGatewayEnv: { CADET_DEVICE_ID: device },
962
+ gatewayPort,
963
+ assistantPort,
964
+ imageTags: {
965
+ assistant: info.assistantImage,
966
+ gateway: info.gatewayImage,
967
+ "credential-executor": info.cesImage,
968
+ },
969
+ instanceName: entry.assistantId,
970
+ res,
971
+ },
972
+ log,
973
+ );
974
+
975
+ // Persist what the recreation decided (the gateway port in the runtime
976
+ // url, the assistant port when it defaulted) so plain `docker start`
977
+ // wakes work and clients reach the right endpoints.
978
+ saveAssistantEntry({
979
+ ...entry,
980
+ runtimeUrl: `http://localhost:${gatewayPort}`,
981
+ containerInfo: { ...info, assistantPort },
982
+ });
983
+ }
984
+
985
+ /** The port of the CLI's own runtime url form (`http://localhost:7830`). */
986
+ function parseGatewayPort(runtimeUrl: string): number | null {
987
+ try {
988
+ const port = new URL(runtimeUrl).port;
989
+ const parsed = parseInt(port, 10);
990
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
991
+ } catch {
992
+ return null;
993
+ }
994
+ }
995
+
788
996
  /**
789
997
  * Ensure a Docker daemon is reachable before waking an instance's containers
790
998
  * (macOS only — on Linux the daemon runs natively).
@@ -805,6 +1013,7 @@ export async function wakeContainers(
805
1013
  */
806
1014
  export async function ensureDockerDaemonRunning(
807
1015
  execFn: typeof exec = exec,
1016
+ opts: { settleTimeoutMs?: number } = {},
808
1017
  ): Promise<void> {
809
1018
  ensureLocalBinOnPath();
810
1019
 
@@ -817,12 +1026,10 @@ export async function ensureDockerDaemonRunning(
817
1026
  // No daemon reachable — fall through to the Colima fallback below.
818
1027
  }
819
1028
 
820
- try {
821
- await execFn("colima", ["status"]);
822
- } catch {
823
- console.log("🚀 Docker daemon not running. Starting Colima...");
824
- await execFn("colima", ["start"]);
825
- }
1029
+ // Deliberately no `colima status` probe: it trusts Colima's own state file,
1030
+ // which can be stale (zombie VM) — daemon reachability is the oracle.
1031
+ console.log("🚀 Docker daemon not running. Starting Colima...");
1032
+ await startColimaAndAwaitDaemon(execFn, console.log, opts);
826
1033
  }
827
1034
 
828
1035
  /**
@@ -1127,6 +1334,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1127
1334
  let provider = setupProviderCredentials
1128
1335
  ? resolveHatchProvider(configValues)
1129
1336
  : undefined;
1337
+ let precollectedApiKey: string | undefined;
1130
1338
 
1131
1339
  if (
1132
1340
  shouldPromptForHatchProvider({
@@ -1139,6 +1347,9 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1139
1347
  const picked = await promptProviderChoice();
1140
1348
  if (picked) {
1141
1349
  provider = picked;
1350
+ // Collect the key immediately, while the choice is fresh — the
1351
+ // startup logs are quiet now, so nothing separates the two prompts.
1352
+ precollectedApiKey = await collectProviderApiKey(picked);
1142
1353
  }
1143
1354
  }
1144
1355
 
@@ -1320,7 +1531,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1320
1531
  const MAX_PULL_RETRIES = 3;
1321
1532
  for (let attempt = 1; attempt <= MAX_PULL_RETRIES; attempt++) {
1322
1533
  try {
1323
- await exec("docker", ["pull", ref]);
1534
+ await pullImage(ref, log);
1324
1535
  break;
1325
1536
  } catch (err) {
1326
1537
  if (attempt === MAX_PULL_RETRIES) throw err;
@@ -1333,7 +1544,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1333
1544
  }
1334
1545
  }
1335
1546
  }
1336
- log("✅ Docker images acquired");
1547
+ log(green("✅ Docker images acquired", process.stdout));
1337
1548
  }
1338
1549
 
1339
1550
  const res = dockerResourceNames(instanceName);
@@ -1434,9 +1645,9 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1434
1645
  : ownSecret;
1435
1646
 
1436
1647
  emitProgress(4, 6, "Starting containers...");
1437
- if (flagEnvVars.CADET_DISABLE_PLATFORM) {
1438
- extraAssistantEnv.CADET_DISABLE_PLATFORM =
1439
- flagEnvVars.CADET_DISABLE_PLATFORM;
1648
+ if (flagEnvVars.CADET_DISABLE_CLOUD) {
1649
+ extraAssistantEnv.CADET_DISABLE_CLOUD =
1650
+ flagEnvVars.CADET_DISABLE_CLOUD;
1440
1651
  }
1441
1652
  const hostDeviceId = getOrCreateHostDeviceId();
1442
1653
  extraAssistantEnv.CADET_DEVICE_ID = hostDeviceId;
@@ -1478,8 +1689,6 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1478
1689
  );
1479
1690
  const containersUpAt = Date.now();
1480
1691
 
1481
- const imageDigests = await captureImageRefs(res);
1482
-
1483
1692
  const runtimeUrl = `http://localhost:${gatewayPort}`;
1484
1693
  const dockerEntry: AssistantEntry = {
1485
1694
  assistantId: instanceName,
@@ -1491,10 +1700,6 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1491
1700
  assistantImage: imageTags.assistant,
1492
1701
  gatewayImage: imageTags.gateway,
1493
1702
  cesImage: imageTags["credential-executor"],
1494
- assistantDigest: imageDigests?.assistant,
1495
- gatewayDigest: imageDigests?.gateway,
1496
- cesDigest: imageDigests?.["credential-executor"],
1497
- networkName: res.network,
1498
1703
  assistantPort,
1499
1704
  },
1500
1705
  };
@@ -1530,6 +1735,7 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1530
1735
  provider,
1531
1736
  guardianAccessToken,
1532
1737
  detached: waitDetached,
1738
+ precollectedKey: precollectedApiKey,
1533
1739
  });
1534
1740
 
1535
1741
  if (providerSetupAction === "defer" && provider !== null) {
@@ -1539,23 +1745,22 @@ export async function hatchDocker(params: HatchDockerParams): Promise<void> {
1539
1745
  );
1540
1746
  } else if (providerSetupAction === "missing-token" && provider !== null) {
1541
1747
  log(
1542
- `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
1543
- ` The assistant is still hatched. Run \`cadet setup --provider ${provider}\` after fixing the connection.`,
1748
+ yellow(
1749
+ `⚠️ Provider credential setup skipped because the guardian token was not leased.\n` +
1750
+ ` The assistant is still hatched. Run \`cadet setup --provider ${provider}\` after fixing the connection.`,
1751
+ process.stdout,
1752
+ ),
1544
1753
  );
1545
1754
  } else if (
1546
1755
  providerSetupAction === "configure" &&
1547
1756
  provider !== undefined
1548
1757
  ) {
1549
- log(
1550
- provider === null
1551
- ? "Checking provider credentials..."
1552
- : `Checking ${formatProviderName(provider)} credentials...`,
1553
- );
1554
1758
  await configureHatchProviderApiKey({
1555
1759
  gatewayUrl: runtimeUrl,
1556
1760
  provider,
1557
1761
  bearerToken: guardianAccessToken,
1558
1762
  env: process.env,
1763
+ precollectedKey: precollectedApiKey,
1559
1764
  log,
1560
1765
  });
1561
1766
  }
@@ -1648,25 +1853,12 @@ async function waitForGatewayAndLease(opts: {
1648
1853
  };
1649
1854
 
1650
1855
  if (detached) {
1651
- log("\n✅ Docker assistant hatched!\n");
1652
- log("Instance details:");
1653
- printInstanceDetails(
1654
- {
1655
- name: instanceName,
1656
- runtime: runtimeUrl,
1657
- container: containerName,
1658
- },
1659
- log,
1660
- );
1661
- log("");
1662
- log(`Stop with: cadet retire ${instanceName}`);
1663
-
1664
- // Lease a guardian token even in detached mode so that `cadet ps`,
1665
- // `cadet exec`, and other CLI commands can authenticate to the
1666
- // gateway. Skip the /readyz readiness poll (the caller asked to detach
1667
- // and not block) but retry the lease itself since the gateway may need
1668
- // a moment to accept connections after the container starts.
1669
- const leaseStart = Date.now();
1856
+ // Lease a guardian token BEFORE declaring the assistant hatched so the
1857
+ // banner means `cadet ps`, `cadet exec`, and other CLI commands can
1858
+ // already authenticate. Skip the /readyz readiness poll (the caller
1859
+ // asked to detach and not block) but retry the lease silently since
1860
+ // the gateway may need a moment to accept connections after the
1861
+ // container starts; only a final failure surfaces to the user.
1670
1862
  const leaseDeadline = containersUpAt + DOCKER_READY_TIMEOUT_MS;
1671
1863
  let guardianAccessToken: string | undefined;
1672
1864
  while (Date.now() < leaseDeadline) {
@@ -1677,27 +1869,33 @@ async function waitForGatewayAndLease(opts: {
1677
1869
  bootstrapSecret,
1678
1870
  );
1679
1871
  guardianAccessToken = tokenData.accessToken;
1680
- const leaseElapsed = ((Date.now() - leaseStart) / 1000).toFixed(1);
1681
- log(
1682
- `Guardian token lease: success after ${leaseElapsed}s (principalId=${tokenData.guardianPrincipalId})`,
1683
- );
1684
1872
  break;
1685
- } catch (err) {
1686
- const elapsed = ((Date.now() - leaseStart) / 1000).toFixed(0);
1687
- const msg = err instanceof Error ? err.message : String(err);
1688
- log(
1689
- `Guardian token lease: attempt failed after ${elapsed}s (${msg.split("\n")[0]}), retrying...`,
1690
- );
1873
+ } catch {
1691
1874
  await new Promise((r) => setTimeout(r, 2000));
1692
1875
  }
1693
1876
  }
1694
1877
  if (!guardianAccessToken) {
1695
1878
  log(
1696
- `⚠️ Guardian token lease failed after ${DOCKER_READY_TIMEOUT_MS / 1000}s.\n` +
1697
- ` The assistant is running but CLI commands (cadet ps, cadet exec) will not authenticate.\n` +
1698
- ` Re-hatch or run \`cadet setup\` to recover.`,
1879
+ yellow(
1880
+ `⚠️ Guardian token lease failed after ${DOCKER_READY_TIMEOUT_MS / 1000}s.\n` +
1881
+ ` The assistant is running but CLI commands (cadet ps, cadet exec) will not authenticate.\n` +
1882
+ ` Re-hatch or run \`cadet setup\` to recover.`,
1883
+ process.stdout,
1884
+ ),
1699
1885
  );
1700
1886
  }
1887
+
1888
+ log(`\n${green("✅ Docker assistant hatched!", process.stdout)}\n`);
1889
+ log("Instance details:");
1890
+ printInstanceDetails(
1891
+ {
1892
+ name: instanceName,
1893
+ runtime: runtimeUrl,
1894
+ container: containerName,
1895
+ },
1896
+ log,
1897
+ );
1898
+ log("");
1701
1899
  return { ready: true, guardianAccessToken };
1702
1900
  }
1703
1901
 
@@ -1778,7 +1976,6 @@ async function waitForGatewayAndLease(opts: {
1778
1976
  // Lease guardian token. The /readyz check confirms both gateway and
1779
1977
  // assistant are reachable. Retry with backoff in case there is a brief
1780
1978
  // window where readiness passes but the guardian endpoint is not yet ready.
1781
- log(`Guardian token lease: starting for ${instanceName} at ${runtimeUrl}`);
1782
1979
  const leaseStart = Date.now();
1783
1980
  const leaseDeadline = start + DOCKER_READY_TIMEOUT_MS;
1784
1981
  let leaseSuccess = false;
@@ -1792,38 +1989,30 @@ async function waitForGatewayAndLease(opts: {
1792
1989
  instanceName,
1793
1990
  bootstrapSecret,
1794
1991
  );
1795
- const leaseElapsed = ((Date.now() - leaseStart) / 1000).toFixed(1);
1796
- log(
1797
- `Guardian token lease: success after ${leaseElapsed}s (principalId=${tokenData.guardianPrincipalId}, expiresAt=${tokenData.accessTokenExpiresAt})`,
1798
- );
1799
1992
  guardianAccessToken = tokenData.accessToken;
1800
1993
  leaseSuccess = true;
1801
1994
  break;
1802
1995
  } catch (err) {
1803
1996
  lastLeaseError =
1804
1997
  err instanceof Error ? (err.stack ?? err.message) : String(err);
1805
- // Log periodically so the user knows we're still trying
1806
- const elapsed = ((Date.now() - leaseStart) / 1000).toFixed(0);
1807
- log(
1808
- `Guardian token lease: attempt failed after ${elapsed}s (${
1809
- lastLeaseError.split("\n")[0]
1810
- }), retrying...`,
1811
- );
1812
1998
  }
1813
1999
  await new Promise((r) => setTimeout(r, 2000));
1814
2000
  }
1815
2001
 
1816
2002
  if (!leaseSuccess) {
1817
2003
  log(
1818
- `\u26a0\ufe0f Guardian token lease: FAILED after ${(
1819
- (Date.now() - leaseStart) /
1820
- 1000
1821
- ).toFixed(1)}s — ${lastLeaseError ?? "unknown error"}`,
2004
+ yellow(
2005
+ `\u26a0\ufe0f Guardian token lease: FAILED after ${(
2006
+ (Date.now() - leaseStart) /
2007
+ 1000
2008
+ ).toFixed(1)}s — ${lastLeaseError ?? "unknown error"}`,
2009
+ process.stdout,
2010
+ ),
1822
2011
  );
1823
2012
  }
1824
2013
 
1825
2014
  log("");
1826
- log(`\u2705 Docker containers are up and running!`);
2015
+ log(green(`\u2705 Docker containers are up and running!`, process.stdout));
1827
2016
  log(` Name: ${instanceName}`);
1828
2017
  log(` Runtime: ${runtimeUrl}`);
1829
2018
  log("");