@mycadet/cli 0.12.6-dev.202610082233.54244ee → 0.12.7

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 (89) hide show
  1. package/eslint.config.mjs +7 -0
  2. package/node_modules/@mycadet/local-mode/src/__tests__/environment.test.ts +1 -4
  3. package/node_modules/@mycadet/local-mode/src/config.ts +14 -19
  4. package/node_modules/@mycadet/local-mode/src/index.ts +5 -0
  5. package/node_modules/@mycadet/local-mode/src/local-signing-key.ts +73 -0
  6. package/node_modules/@mycadet/local-mode/src/lockfile.ts +18 -3
  7. package/package.json +4 -3
  8. package/src/__tests__/assistant-config.test.ts +30 -28
  9. package/src/__tests__/debug.test.ts +8 -8
  10. package/src/__tests__/default-main-screen-formatting.test.ts +5 -4
  11. package/src/__tests__/flags.test.ts +2 -2
  12. package/src/__tests__/hatch-provider-secrets.test.ts +3 -3
  13. package/src/__tests__/ingress-config.test.ts +2 -2
  14. package/src/__tests__/login-loopback.test.ts +1 -1
  15. package/src/__tests__/nginx-ingress.test.ts +68 -76
  16. package/src/__tests__/pair.test.ts +135 -121
  17. package/src/__tests__/platform-client.test.ts +2 -2
  18. package/src/__tests__/recover.test.ts +96 -3
  19. package/src/__tests__/retire.test.ts +5 -3
  20. package/src/__tests__/signing-key-continuity.test.ts +261 -0
  21. package/src/__tests__/sleep.test.ts +189 -6
  22. package/src/__tests__/tailscale-tunnel.test.ts +1 -1
  23. package/src/__tests__/uninstall.test.ts +171 -0
  24. package/src/__tests__/upgrade-local.test.ts +16 -3
  25. package/src/__tests__/use.test.ts +2 -2
  26. package/src/__tests__/wake-error.test.ts +76 -0
  27. package/src/__tests__/wake.test.ts +309 -36
  28. package/src/commands/backup.ts +6 -2
  29. package/src/commands/clean.ts +6 -2
  30. package/src/commands/client.ts +91 -41
  31. package/src/commands/confirm.ts +6 -2
  32. package/src/commands/connect.ts +6 -2
  33. package/src/commands/debug.ts +6 -2
  34. package/src/commands/devices.ts +6 -2
  35. package/src/commands/events.ts +6 -2
  36. package/src/commands/exec.ts +50 -12
  37. package/src/commands/flags.ts +6 -2
  38. package/src/commands/gateway.ts +6 -2
  39. package/src/commands/hatch.ts +6 -2
  40. package/src/commands/login.ts +26 -356
  41. package/src/commands/logs.ts +6 -2
  42. package/src/commands/message.ts +6 -2
  43. package/src/commands/pair.ts +6 -2
  44. package/src/commands/ps.ts +7 -2
  45. package/src/commands/recover.ts +21 -16
  46. package/src/commands/restore.ts +6 -2
  47. package/src/commands/retire.ts +6 -2
  48. package/src/commands/roadmap.ts +6 -2
  49. package/src/commands/rollback.ts +6 -2
  50. package/src/commands/setup.ts +76 -9
  51. package/src/commands/sleep.ts +131 -31
  52. package/src/commands/ssh.ts +6 -2
  53. package/src/commands/teleport.ts +6 -2
  54. package/src/commands/terminal.ts +6 -2
  55. package/src/commands/tunnel.ts +6 -2
  56. package/src/commands/uninstall.ts +228 -0
  57. package/src/commands/unpair.ts +6 -2
  58. package/src/commands/upgrade.ts +13 -8
  59. package/src/commands/use.ts +6 -2
  60. package/src/commands/voice.ts +6 -2
  61. package/src/commands/wake.ts +69 -37
  62. package/src/commands/workflows.ts +6 -2
  63. package/src/components/DefaultMainScreen.tsx +250 -36
  64. package/src/index.ts +23 -55
  65. package/src/lib/assistant-config.ts +27 -13
  66. package/src/lib/backup-ops.ts +3 -4
  67. package/src/lib/cli-error.ts +5 -0
  68. package/src/lib/client-identity.ts +8 -10
  69. package/src/lib/cloud-login.ts +422 -0
  70. package/src/lib/color.ts +16 -0
  71. package/src/lib/default-main-screen-formatting.ts +32 -8
  72. package/src/lib/environments/__tests__/paths.test.ts +9 -11
  73. package/src/lib/environments/paths.ts +2 -20
  74. package/src/lib/guardian-token.ts +72 -18
  75. package/src/lib/hatch-local.ts +52 -1
  76. package/src/lib/health-check.ts +24 -15
  77. package/src/lib/lifecycle-reporter.ts +9 -1
  78. package/src/lib/local-hatch-lock.ts +19 -27
  79. package/src/lib/local-runtime-client.ts +25 -14
  80. package/src/lib/local.ts +17 -43
  81. package/src/lib/ngrok.ts +48 -16
  82. package/src/lib/platform-client.ts +141 -72
  83. package/src/lib/retire-archive.ts +4 -4
  84. package/src/lib/retire-local.ts +2 -25
  85. package/src/lib/teleport-backup.ts +23 -9
  86. package/src/lib/terminal-client.ts +21 -9
  87. package/src/lib/tunnel-edge.ts +11 -13
  88. package/src/lib/upgrade-lifecycle.ts +16 -4
  89. package/src/lib/workos-pkce.ts +52 -21
@@ -8,7 +8,8 @@ import {
8
8
  spyOn,
9
9
  test,
10
10
  } from "bun:test";
11
- import { mkdtempSync, rmSync } from "node:fs";
11
+ import { resolveExistingLocalSigningKey } from "@mycadet/local-mode";
12
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
12
13
  import { tmpdir } from "node:os";
13
14
  import { join } from "node:path";
14
15
 
@@ -21,6 +22,7 @@ import * as local from "../lib/local.js";
21
22
  import * as nginxIngress from "../lib/nginx-ingress.js";
22
23
  import * as ngrok from "../lib/ngrok.js";
23
24
  import * as processLib from "../lib/process.js";
25
+ import * as portAllocator from "../lib/port-allocator.js";
24
26
  import type { AssistantEntry } from "../lib/assistant-config.js";
25
27
 
26
28
  const realAssistantConfig = { ...assistantConfig };
@@ -29,6 +31,7 @@ const realGuardianToken = { ...guardianToken };
29
31
  const realLocal = { ...local };
30
32
  const realNgrok = { ...ngrok };
31
33
  const realProcessLib = { ...processLib };
34
+ const realPortAllocator = { ...portAllocator };
32
35
 
33
36
  const resolveTargetAssistantMock =
34
37
  mock<typeof assistantConfig.resolveTargetAssistant>();
@@ -113,6 +116,15 @@ mock.module("../lib/process", () => ({
113
116
  isProcessAlive: isProcessAliveMock,
114
117
  }));
115
118
 
119
+ const findOpenPortMock = mock<typeof portAllocator.findOpenPort>(
120
+ async (port) => port,
121
+ );
122
+
123
+ mock.module("../lib/port-allocator.js", () => ({
124
+ ...realPortAllocator,
125
+ findOpenPort: findOpenPortMock,
126
+ }));
127
+
116
128
  const generateLocalSigningKeyMock = mock<typeof local.generateLocalSigningKey>(
117
129
  () => "generated-bootstrap-secret",
118
130
  );
@@ -216,7 +228,7 @@ function makeLocalEntry(): AssistantEntry {
216
228
  gatewayPort: 7830,
217
229
  qdrantPort: 6333,
218
230
  cesPort: 7822,
219
- signingKey: "existing-signing-key",
231
+ signingKey: "ab".repeat(32),
220
232
  },
221
233
  };
222
234
  }
@@ -250,6 +262,8 @@ beforeEach(() => {
250
262
  );
251
263
  stopProcessByPidFileMock.mockReset();
252
264
  stopProcessByPidFileMock.mockResolvedValue(true);
265
+ findOpenPortMock.mockReset();
266
+ findOpenPortMock.mockImplementation(async (port) => port);
253
267
  generateLocalSigningKeyMock.mockReset();
254
268
  generateLocalSigningKeyMock.mockReturnValue("generated-bootstrap-secret");
255
269
  isAssistantWatchModeAvailableMock.mockReset();
@@ -314,6 +328,7 @@ afterAll(() => {
314
328
  mock.module("../lib/docker.js", () => realDocker);
315
329
  mock.module("../lib/guardian-token.js", () => realGuardianToken);
316
330
  mock.module("../lib/process", () => realProcessLib);
331
+ mock.module("../lib/port-allocator.js", () => realPortAllocator);
317
332
  mock.module("../lib/local", () => realLocal);
318
333
  mock.module("../lib/ngrok", () => realNgrok);
319
334
  mock.module("../lib/ingress-config.js", () => realIngressConfig);
@@ -322,6 +337,286 @@ afterAll(() => {
322
337
  });
323
338
 
324
339
  describe("cadet wake", () => {
340
+ test("missing identity refuses before probing, stopping, starting, or repairing", async () => {
341
+ delete localEntry.resources!.signingKey;
342
+ await expect(wake()).rejects.toThrow(
343
+ /signing key missing.*cadet wake.*--repair-guardian/,
344
+ );
345
+ expect(resolveProcessStateMock).not.toHaveBeenCalled();
346
+ expect(stopProcessByPidFileMock).not.toHaveBeenCalled();
347
+ expect(startLocalDaemonMock).not.toHaveBeenCalled();
348
+ expect(startGatewayMock).not.toHaveBeenCalled();
349
+ expect(generateLocalSigningKeyMock).not.toHaveBeenCalled();
350
+ expect(resetGuardianBootstrapMock).not.toHaveBeenCalled();
351
+ expect(saveAssistantEntryMock).not.toHaveBeenCalled();
352
+ });
353
+
354
+ test("recovers the existing deprecated key before process startup", async () => {
355
+ delete localEntry.resources!.signingKey;
356
+ localEntry.guardianBootstrapSecret = "existing-bootstrap";
357
+ const dir = join(tempDir, "workspace", "deprecated");
358
+ mkdirSync(dir, { recursive: true });
359
+ writeFileSync(
360
+ join(dir, "actor-token-signing-key"),
361
+ Buffer.from("cd".repeat(32), "hex"),
362
+ );
363
+ process.argv = ["bun", "cadet", "wake", "local-assistant"];
364
+ resolveProcessStateMock.mockImplementation(async () => {
365
+ expect(localEntry.resources!.signingKey).toBe("cd".repeat(32));
366
+ expect(saveAssistantEntryMock).toHaveBeenCalled();
367
+ return { status: "needs_start", pid: null };
368
+ });
369
+ isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
370
+ await wake();
371
+ expect(startLocalDaemonMock).toHaveBeenCalledWith(
372
+ false,
373
+ localEntry.resources,
374
+ { foreground: false, signingKey: "cd".repeat(32) },
375
+ );
376
+ expect(startGatewayMock).toHaveBeenCalledWith(false, localEntry.resources, {
377
+ signingKey: "cd".repeat(32),
378
+ bootstrapSecret: "existing-bootstrap",
379
+ });
380
+ expect(generateLocalSigningKeyMock).not.toHaveBeenCalled();
381
+ expect(resetGuardianBootstrapMock).not.toHaveBeenCalled();
382
+ expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
383
+ });
384
+
385
+ test("a conflicting key refuses before any process is stopped", async () => {
386
+ delete localEntry.resources!.signingKey;
387
+ const legacy = join(tempDir, "workspace", "deprecated");
388
+ mkdirSync(legacy, { recursive: true });
389
+ writeFileSync(
390
+ join(legacy, "actor-token-signing-key"),
391
+ Buffer.from("ab".repeat(32), "hex"),
392
+ );
393
+ const dir = join(tempDir, "gateway-security");
394
+ mkdirSync(dir, { recursive: true });
395
+ writeFileSync(
396
+ join(dir, "actor-token-signing-key"),
397
+ Buffer.from("cd".repeat(32), "hex"),
398
+ );
399
+ await expect(wake()).rejects.toThrow("signing key conflict");
400
+ expect(resolveProcessStateMock).not.toHaveBeenCalled();
401
+ expect(stopProcessByPidFileMock).not.toHaveBeenCalled();
402
+ expect(saveAssistantEntryMock).not.toHaveBeenCalled();
403
+ });
404
+
405
+ for (const failure of [
406
+ "missing",
407
+ "invalid",
408
+ "conflict",
409
+ "unreadable",
410
+ ] as const) {
411
+ test(`explicit repair recovers ${failure} identity and the next ordinary wake reuses it`, async () => {
412
+ delete localEntry.resources!.signingKey;
413
+ localEntry.guardianBootstrapSecret = "existing-bootstrap";
414
+ const protectedDir = join(tempDir, "gateway-security");
415
+ const legacyDir = join(tempDir, "workspace", "deprecated");
416
+ if (failure === "invalid") localEntry.resources!.signingKey = "bad-key";
417
+ if (failure === "unreadable")
418
+ mkdirSync(join(protectedDir, "actor-token-signing-key"), {
419
+ recursive: true,
420
+ });
421
+ if (failure === "conflict") {
422
+ mkdirSync(protectedDir, { recursive: true });
423
+ mkdirSync(legacyDir, { recursive: true });
424
+ writeFileSync(
425
+ join(protectedDir, "actor-token-signing-key"),
426
+ Buffer.from("ab".repeat(32), "hex"),
427
+ );
428
+ writeFileSync(
429
+ join(legacyDir, "actor-token-signing-key"),
430
+ Buffer.from("cd".repeat(32), "hex"),
431
+ );
432
+ }
433
+ process.argv = [
434
+ "bun",
435
+ "cadet",
436
+ "wake",
437
+ "local-assistant",
438
+ "--repair-guardian",
439
+ ];
440
+ const replacement = "ef".repeat(32);
441
+ generateLocalSigningKeyMock.mockReturnValue(replacement);
442
+ resolveProcessStateMock.mockImplementation(async () => {
443
+ expect(saveAssistantEntryMock).toHaveBeenCalled();
444
+ expect(stopProcessByPidFileMock).toHaveBeenCalledTimes(2);
445
+ return { status: "needs_start", pid: null };
446
+ });
447
+ isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
448
+ await wake();
449
+ expect(startLocalDaemonMock).toHaveBeenCalledWith(
450
+ false,
451
+ localEntry.resources,
452
+ { foreground: false, signingKey: replacement },
453
+ );
454
+ expect(startGatewayMock).toHaveBeenCalledWith(
455
+ false,
456
+ localEntry.resources,
457
+ { signingKey: replacement, bootstrapSecret: "existing-bootstrap" },
458
+ );
459
+ expect(resetGuardianBootstrapMock).toHaveBeenCalledTimes(1);
460
+ expect(leaseGuardianTokenMock).toHaveBeenCalledTimes(1);
461
+ expect(warnSpy.mock.calls.flat().join(" ")).toContain(
462
+ "all clients must authenticate again",
463
+ );
464
+ expect(resolveExistingLocalSigningKey(localEntry.resources)).toBe(
465
+ replacement,
466
+ );
467
+ process.argv = ["bun", "cadet", "wake", "local-assistant"];
468
+ generateLocalSigningKeyMock.mockClear();
469
+ resetGuardianBootstrapMock.mockClear();
470
+ leaseGuardianTokenMock.mockClear();
471
+ await wake();
472
+ expect(generateLocalSigningKeyMock).not.toHaveBeenCalled();
473
+ expect(resetGuardianBootstrapMock).not.toHaveBeenCalled();
474
+ expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
475
+ });
476
+ }
477
+
478
+ test("explicit repair preserves a recoverable key without restarting healthy services", async () => {
479
+ localEntry.guardianBootstrapSecret = "existing-bootstrap";
480
+ process.argv = [
481
+ "bun",
482
+ "cadet",
483
+ "wake",
484
+ "local-assistant",
485
+ "--repair-guardian",
486
+ ];
487
+ isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
488
+ await wake();
489
+ expect(findOpenPortMock).not.toHaveBeenCalled();
490
+ expect(generateLocalSigningKeyMock).not.toHaveBeenCalled();
491
+ expect(stopProcessByPidFileMock).not.toHaveBeenCalled();
492
+ expect(startLocalDaemonMock).not.toHaveBeenCalled();
493
+ expect(startGatewayMock).not.toHaveBeenCalled();
494
+ expect(leaseGuardianTokenMock).toHaveBeenCalledTimes(1);
495
+ expect(localEntry.resources!.signingKey).toBe("ab".repeat(32));
496
+ });
497
+
498
+ test("repair cannot replace the persisted key while a service refuses to stop", async () => {
499
+ delete localEntry.resources!.signingKey;
500
+ process.argv = [
501
+ "bun",
502
+ "cadet",
503
+ "wake",
504
+ "local-assistant",
505
+ "--repair-guardian",
506
+ ];
507
+ stopProcessByPidFileMock.mockResolvedValue(false);
508
+ isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
509
+ await expect(wake()).rejects.toThrow("could not be stopped");
510
+ expect(saveAssistantEntryMock).not.toHaveBeenCalled();
511
+ expect(startLocalDaemonMock).not.toHaveBeenCalled();
512
+ expect(startGatewayMock).not.toHaveBeenCalled();
513
+ expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
514
+ });
515
+
516
+ for (const [portField, label] of [
517
+ ["daemonPort", "assistant"],
518
+ ["gatewayPort", "gateway"],
519
+ ] as const) {
520
+ test(`repair refuses an untracked ${label} listener before saving a key and can retry after it stops`, async () => {
521
+ delete localEntry.resources!.signingKey;
522
+ localEntry.guardianBootstrapSecret = "existing-bootstrap";
523
+ process.argv = [
524
+ "bun",
525
+ "cadet",
526
+ "wake",
527
+ "local-assistant",
528
+ "--repair-guardian",
529
+ ];
530
+ const orphan = Bun.serve({
531
+ hostname: "127.0.0.1",
532
+ port: 0,
533
+ fetch: () => new Response("not ready", { status: 503 }),
534
+ });
535
+ const port = orphan.port!;
536
+ localEntry.resources![portField] = port;
537
+ stopProcessByPidFileMock.mockImplementation(
538
+ realProcessLib.stopProcessByPidFile,
539
+ );
540
+ findOpenPortMock.mockImplementation((candidate, options) =>
541
+ candidate === port
542
+ ? realPortAllocator.findOpenPort(candidate, options)
543
+ : Promise.resolve(candidate),
544
+ );
545
+ generateLocalSigningKeyMock.mockReturnValue("ef".repeat(32));
546
+ resolveProcessStateMock.mockResolvedValue({
547
+ status: "needs_start",
548
+ pid: null,
549
+ });
550
+ startLocalDaemonMock.mockImplementation(async () => {
551
+ expect(saveAssistantEntryMock).toHaveBeenCalled();
552
+ isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
553
+ });
554
+ try {
555
+ await expect(wake()).rejects.toThrow(
556
+ `could not confirm that ${label} port ${port} is free`,
557
+ );
558
+ expect(localEntry.resources!.signingKey).toBeUndefined();
559
+ expect(saveAssistantEntryMock).not.toHaveBeenCalled();
560
+ expect(startLocalDaemonMock).not.toHaveBeenCalled();
561
+ expect(startGatewayMock).not.toHaveBeenCalled();
562
+ expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
563
+ expect(findOpenPortMock).toHaveBeenCalledWith(port, {
564
+ maxAttempts: 1,
565
+ host: "127.0.0.1",
566
+ });
567
+ expect((await fetch(`http://127.0.0.1:${port}/healthz`)).status).toBe(
568
+ 503,
569
+ );
570
+ await orphan.stop(true);
571
+ await wake();
572
+ expect(localEntry.resources).toMatchObject({
573
+ signingKey: "ef".repeat(32),
574
+ });
575
+ expect(startLocalDaemonMock).toHaveBeenCalledTimes(1);
576
+ expect(startGatewayMock).toHaveBeenCalledTimes(1);
577
+ expect(leaseGuardianTokenMock).toHaveBeenCalledTimes(1);
578
+ } finally {
579
+ await orphan.stop(true);
580
+ }
581
+ });
582
+ }
583
+
584
+ test("repair refuses to save a replacement key when a port cannot be checked", async () => {
585
+ delete localEntry.resources!.signingKey;
586
+ process.argv = [
587
+ "bun",
588
+ "cadet",
589
+ "wake",
590
+ "local-assistant",
591
+ "--repair-guardian",
592
+ ];
593
+ findOpenPortMock.mockRejectedValue(new Error("EPERM"));
594
+ await expect(wake()).rejects.toThrow("no replacement key was saved");
595
+ expect(localEntry.resources!.signingKey).toBeUndefined();
596
+ expect(saveAssistantEntryMock).not.toHaveBeenCalled();
597
+ expect(startLocalDaemonMock).not.toHaveBeenCalled();
598
+ expect(startGatewayMock).not.toHaveBeenCalled();
599
+ expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
600
+ });
601
+
602
+ test("repair persistence failure prevents launching with an ephemeral replacement key", async () => {
603
+ delete localEntry.resources!.signingKey;
604
+ process.argv = [
605
+ "bun",
606
+ "cadet",
607
+ "wake",
608
+ "local-assistant",
609
+ "--repair-guardian",
610
+ ];
611
+ saveAssistantEntryMock.mockImplementation(() => {
612
+ throw new Error("registry write failed");
613
+ });
614
+ await expect(wake()).rejects.toThrow("registry write failed");
615
+ expect(startLocalDaemonMock).not.toHaveBeenCalled();
616
+ expect(startGatewayMock).not.toHaveBeenCalled();
617
+ expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
618
+ });
619
+
325
620
  test("restarts a running gateway without watch mode when backfilling the bootstrap secret", async () => {
326
621
  await wake();
327
622
 
@@ -338,7 +633,7 @@ describe("cadet wake", () => {
338
633
  false,
339
634
  expect.objectContaining({ instanceDir: tempDir }),
340
635
  {
341
- signingKey: "existing-signing-key",
636
+ signingKey: "ab".repeat(32),
342
637
  bootstrapSecret: "generated-bootstrap-secret",
343
638
  },
344
639
  );
@@ -642,9 +937,7 @@ describe("cadet wake — tunnel edge restore", () => {
642
937
  });
643
938
 
644
939
  test("a docker wake restores the shared default-workspace edge", async () => {
645
- const defaultWorkspace = mkdtempSync(
646
- join(tmpdir(), "cadet-wake-default-"),
647
- );
940
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
648
941
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
649
942
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
650
943
  resolveTargetAssistantMock.mockReturnValue({
@@ -679,9 +972,7 @@ describe("cadet wake — tunnel edge restore", () => {
679
972
  });
680
973
 
681
974
  test("a docker wake leaves an edge fronting another container alone", async () => {
682
- const defaultWorkspace = mkdtempSync(
683
- join(tmpdir(), "cadet-wake-default-"),
684
- );
975
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
685
976
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
686
977
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
687
978
  resolveTargetAssistantMock.mockReturnValue({
@@ -715,9 +1006,7 @@ describe("cadet wake — tunnel edge restore", () => {
715
1006
  });
716
1007
 
717
1008
  test("a docker wake adopts an edge that already fronts it", async () => {
718
- const defaultWorkspace = mkdtempSync(
719
- join(tmpdir(), "cadet-wake-default-"),
720
- );
1009
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
721
1010
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
722
1011
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
723
1012
  resolveTargetAssistantMock.mockReturnValue({
@@ -764,9 +1053,7 @@ describe("cadet wake — tunnel edge restore", () => {
764
1053
  });
765
1054
 
766
1055
  test("a failed docker edge restore does not claim webhooks still work", async () => {
767
- const defaultWorkspace = mkdtempSync(
768
- join(tmpdir(), "cadet-wake-default-"),
769
- );
1056
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
770
1057
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
771
1058
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
772
1059
  resolveTargetAssistantMock.mockReturnValue({
@@ -801,9 +1088,7 @@ describe("cadet wake — tunnel edge restore", () => {
801
1088
  });
802
1089
 
803
1090
  test("a docker wake leaves a saved ingress owned by another container alone", async () => {
804
- const defaultWorkspace = mkdtempSync(
805
- join(tmpdir(), "cadet-wake-default-"),
806
- );
1091
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
807
1092
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
808
1093
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
809
1094
  // The edge is stopped, so its gatewayPort record is gone; only the saved
@@ -842,9 +1127,7 @@ describe("cadet wake — tunnel edge restore", () => {
842
1127
  });
843
1128
 
844
1129
  test("a docker wake waits for the gateway before restoring the edge", async () => {
845
- const defaultWorkspace = mkdtempSync(
846
- join(tmpdir(), "cadet-wake-default-"),
847
- );
1130
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
848
1131
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
849
1132
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
850
1133
  resolveTargetAssistantMock.mockReturnValue({
@@ -872,9 +1155,7 @@ describe("cadet wake — tunnel edge restore", () => {
872
1155
  });
873
1156
 
874
1157
  test("a docker wake skips the edge when the gateway never comes up", async () => {
875
- const defaultWorkspace = mkdtempSync(
876
- join(tmpdir(), "cadet-wake-default-"),
877
- );
1158
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
878
1159
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
879
1160
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
880
1161
  resolveTargetAssistantMock.mockReturnValue({
@@ -906,9 +1187,7 @@ describe("cadet wake — tunnel edge restore", () => {
906
1187
  });
907
1188
 
908
1189
  test("a docker wake with no ingress config never waits for the gateway", async () => {
909
- const defaultWorkspace = mkdtempSync(
910
- join(tmpdir(), "cadet-wake-default-"),
911
- );
1190
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
912
1191
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
913
1192
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
914
1193
  resolveTargetAssistantMock.mockReturnValue({
@@ -934,9 +1213,7 @@ describe("cadet wake — tunnel edge restore", () => {
934
1213
  });
935
1214
 
936
1215
  test("a docker wake restores a saved ingress that no entry claims", async () => {
937
- const defaultWorkspace = mkdtempSync(
938
- join(tmpdir(), "cadet-wake-default-"),
939
- );
1216
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
940
1217
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
941
1218
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
942
1219
  // Tunneled before the lockfile mirror existed: publicBaseUrl is saved but
@@ -976,9 +1253,7 @@ describe("cadet wake — tunnel edge restore", () => {
976
1253
  });
977
1254
 
978
1255
  test("a docker wake respects an ingress claim from another environment", async () => {
979
- const defaultWorkspace = mkdtempSync(
980
- join(tmpdir(), "cadet-wake-default-"),
981
- );
1256
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
982
1257
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
983
1258
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
984
1259
  resolveTargetAssistantMock.mockReturnValue({
@@ -1013,9 +1288,7 @@ describe("cadet wake — tunnel edge restore", () => {
1013
1288
  });
1014
1289
 
1015
1290
  test("a docker wake without an ingress config leaves the edge alone", async () => {
1016
- const defaultWorkspace = mkdtempSync(
1017
- join(tmpdir(), "cadet-wake-default-"),
1018
- );
1291
+ const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
1019
1292
  const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
1020
1293
  process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
1021
1294
  resolveTargetAssistantMock.mockReturnValue({
@@ -23,7 +23,8 @@ import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
23
23
  // --export-timeout.
24
24
  const DEFAULT_EXPORT_TIMEOUT_MS = 300_000;
25
25
 
26
- export async function backup(): Promise<void> {
26
+ export const backup = Object.assign(
27
+ async function backup(): Promise<void> {
27
28
  const args = process.argv.slice(3);
28
29
 
29
30
  if (args.includes("--help") || args.includes("-h")) {
@@ -224,7 +225,10 @@ export async function backup(): Promise<void> {
224
225
  if (manifestSha) {
225
226
  console.log(`Manifest SHA-256: ${manifestSha}`);
226
227
  }
227
- }
228
+ },
229
+ { description: "Export a backup of a running assistant" },
230
+ );
231
+
228
232
 
229
233
  // ---------------------------------------------------------------------------
230
234
  // Platform-managed (cloud="cadet") backup over GCS.
@@ -1,7 +1,8 @@
1
1
  import { detectOrphanedProcesses } from "../lib/orphan-detection";
2
2
  import { stopProcess } from "../lib/process";
3
3
 
4
- export async function clean(): Promise<void> {
4
+ export const clean = Object.assign(
5
+ async function clean(): Promise<void> {
5
6
  const args = process.argv.slice(3);
6
7
  if (args.includes("--help") || args.includes("-h")) {
7
8
  console.log("Usage: cadet clean");
@@ -36,4 +37,7 @@ export async function clean(): Promise<void> {
36
37
  }
37
38
 
38
39
  console.log(`\nCleaned up ${killed} process${killed === 1 ? "" : "es"}.`);
39
- }
40
+ },
41
+ { description: "Kill orphaned cadet processes" },
42
+ );
43
+