@mycadet/cli 0.12.11 → 0.13.0-dev.202610171626.d97b8b8

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 (94) hide show
  1. package/README.md +13 -0
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  3. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  4. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  6. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  7. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  9. package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  10. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  11. package/package.json +8 -3
  12. package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
  13. package/src/__tests__/backup-ops-matching.test.ts +15 -13
  14. package/src/__tests__/backup-ops.test.ts +105 -0
  15. package/src/__tests__/backup.test.ts +114 -673
  16. package/src/__tests__/cloud-releases.test.ts +82 -0
  17. package/src/__tests__/commands-mock.ts +101 -0
  18. package/src/__tests__/debug.test.ts +17 -91
  19. package/src/__tests__/devices.test.ts +2 -2
  20. package/src/__tests__/gateway-token.test.ts +267 -0
  21. package/src/__tests__/host-teleport-docker.test.ts +371 -0
  22. package/src/__tests__/host-teleport.test.ts +405 -0
  23. package/src/__tests__/nginx-ingress.test.ts +536 -23
  24. package/src/__tests__/orphan-detection.test.ts +1 -1
  25. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  26. package/src/__tests__/ps-cloud-status.test.ts +6 -6
  27. package/src/__tests__/root-backup-docker.test.ts +294 -0
  28. package/src/__tests__/root-backup.test.ts +527 -0
  29. package/src/__tests__/root-restore-command.test.ts +147 -0
  30. package/src/__tests__/root-signing-key.test.ts +50 -0
  31. package/src/__tests__/setup.test.ts +3 -3
  32. package/src/__tests__/shell-mode.test.ts +83 -0
  33. package/src/__tests__/sleep.test.ts +11 -0
  34. package/src/__tests__/teleport.test.ts +68 -2996
  35. package/src/__tests__/tunnel.test.ts +10 -2
  36. package/src/__tests__/upgrade-local.test.ts +21 -11
  37. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  38. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  39. package/src/__tests__/wake.test.ts +4 -1
  40. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  41. package/src/commands/backup.ts +44 -399
  42. package/src/commands/client.ts +3 -3
  43. package/src/commands/completions.ts +0 -2
  44. package/src/commands/debug.ts +75 -131
  45. package/src/commands/devices.ts +51 -17
  46. package/src/commands/gateway/token.ts +77 -23
  47. package/src/commands/pair.ts +2 -2
  48. package/src/commands/restore.ts +77 -444
  49. package/src/commands/retire.ts +2 -3
  50. package/src/commands/sleep.ts +15 -2
  51. package/src/commands/teleport.ts +47 -1559
  52. package/src/commands/tunnel.ts +48 -7
  53. package/src/commands/upgrade.ts +78 -156
  54. package/src/commands/wake.ts +1 -1
  55. package/src/index.ts +0 -4
  56. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  57. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  58. package/src/lib/__tests__/web-dist.test.ts +27 -1
  59. package/src/lib/assistant-client.ts +1 -1
  60. package/src/lib/assistant-config.ts +23 -18
  61. package/src/lib/backup-ops.ts +37 -251
  62. package/src/lib/cloud-client.ts +10 -13
  63. package/src/lib/cloud-login.ts +2 -1
  64. package/src/lib/cloud-releases.ts +54 -10
  65. package/src/lib/cloud-values.ts +1 -1
  66. package/src/lib/confirm-action.ts +2 -2
  67. package/src/lib/docker.ts +59 -22
  68. package/src/lib/github-repository.ts +2 -0
  69. package/src/lib/guardian-token.ts +1 -23
  70. package/src/lib/hatch-local.ts +17 -1
  71. package/src/lib/host-teleport.ts +506 -0
  72. package/src/lib/http-client.ts +25 -0
  73. package/src/lib/ingress-config.ts +31 -0
  74. package/src/lib/live-voice/tunnel.test.ts +1 -1
  75. package/src/lib/live-voice/tunnel.ts +1 -1
  76. package/src/lib/local.ts +4 -2
  77. package/src/lib/nginx-ingress.ts +454 -79
  78. package/src/lib/process.ts +13 -0
  79. package/src/lib/root-backup.ts +917 -0
  80. package/src/lib/root-signing-key.ts +31 -0
  81. package/src/lib/runtime-url.ts +0 -29
  82. package/src/lib/teleport-backup.ts +3 -16
  83. package/src/lib/tunnel.ts +1 -0
  84. package/src/lib/upgrade-lifecycle.ts +63 -141
  85. package/src/lib/web-dist.ts +79 -1
  86. package/src/__tests__/bundle-staging.test.ts +0 -268
  87. package/src/__tests__/unpair.test.ts +0 -160
  88. package/src/commands/rollback.ts +0 -479
  89. package/src/commands/unpair.ts +0 -122
  90. package/src/lib/__tests__/job-polling.test.ts +0 -278
  91. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  92. package/src/lib/bundle-staging.ts +0 -220
  93. package/src/lib/job-polling.ts +0 -195
  94. package/src/lib/local-runtime-client.ts +0 -374
@@ -91,7 +91,12 @@ mock.module("../lib/tunnels/gateway-notify.js", () => ({
91
91
  const EDGE_PORT = 18080;
92
92
 
93
93
  const ensureTunnelEdgeMock = mock<typeof nginxIngress.ensureTunnelEdge>(
94
- async () => ({ port: EDGE_PORT, started: true, includesWebApp: true }),
94
+ async () => ({
95
+ port: EDGE_PORT,
96
+ started: true,
97
+ includesWebApp: true,
98
+ shellMode: "web",
99
+ }),
95
100
  );
96
101
  const getNginxVersionMock = mock<typeof nginxIngress.getNginxVersion>(
97
102
  () => "nginx version (test)",
@@ -132,7 +137,7 @@ function makeCloudEntry(assistantId = "cloud-1"): AssistantEntry {
132
137
  return {
133
138
  assistantId,
134
139
  runtimeUrl: `https://runtime.example.com/${assistantId}`,
135
- cloud: "cadet",
140
+ cloud: "cloud",
136
141
  };
137
142
  }
138
143
 
@@ -289,6 +294,7 @@ describe("tunnel edge targeting", () => {
289
294
  port: EDGE_PORT,
290
295
  started: true,
291
296
  includesWebApp: true,
297
+ shellMode: "web",
292
298
  });
293
299
  });
294
300
 
@@ -803,6 +809,7 @@ describe("tunnel edge targeting", () => {
803
809
  port: EDGE_PORT,
804
810
  started: false,
805
811
  includesWebApp: false,
812
+ shellMode: "web",
806
813
  });
807
814
 
808
815
  const logs = await runTunnelCapturingLogs();
@@ -1744,6 +1751,7 @@ describe("tunnel background mode", () => {
1744
1751
  port: EDGE_PORT,
1745
1752
  started: true,
1746
1753
  includesWebApp: true,
1754
+ shellMode: "web",
1747
1755
  });
1748
1756
  const configHome = mkdtempSync(join(tmpdir(), "cadet-tunnel-xdg-"));
1749
1757
  tempDirs.push(configHome);
@@ -55,7 +55,7 @@ mock.module("../lib/assistant-config", () => ({
55
55
  }));
56
56
 
57
57
  const createBackupMock = mock<typeof backupOps.createBackup>(
58
- async () => "/tmp/local-pre-upgrade.vbundle",
58
+ async () => "/tmp/local-pre-upgrade.tar.gz",
59
59
  );
60
60
  const pruneOldBackupsMock = mock<typeof backupOps.pruneOldBackups>(() => {});
61
61
  const restoreBackupMock = mock<typeof backupOps.restoreBackup>(
@@ -238,7 +238,7 @@ beforeEach(() => {
238
238
  loadAllAssistantsMock.mockReturnValue([entry]);
239
239
  saveAssistantEntryMock.mockReset();
240
240
  createBackupMock.mockReset();
241
- createBackupMock.mockResolvedValue("/tmp/local-pre-upgrade.vbundle");
241
+ createBackupMock.mockResolvedValue("/tmp/local-pre-upgrade.tar.gz");
242
242
  pruneOldBackupsMock.mockReset();
243
243
  pruneOldBackupsMock.mockReturnValue(undefined);
244
244
  restoreBackupMock.mockReset();
@@ -372,7 +372,7 @@ describe("cadet upgrade local", () => {
372
372
  await upgrade();
373
373
 
374
374
  expect(loopbackSafeFetchMock).toHaveBeenCalledWith(
375
- "http://127.0.0.1:7830/healthz?include=migrations",
375
+ "http://127.0.0.1:7830/healthz",
376
376
  expect.any(Object),
377
377
  );
378
378
  expect(createBackupMock).toHaveBeenCalledWith(
@@ -382,14 +382,6 @@ describe("cadet upgrade local", () => {
382
382
  prefix: "local-assistant-pre-upgrade",
383
383
  }),
384
384
  );
385
- expect(saveAssistantEntryMock).toHaveBeenCalledWith(
386
- expect.objectContaining({
387
- previousVersion: "v0.8.11",
388
- previousDbMigrationVersion: 12,
389
- previousWorkspaceMigrationId: "011-test",
390
- preUpgradeBackupPath: "/tmp/local-pre-upgrade.vbundle",
391
- }),
392
- );
393
385
  expect(stopLocalProcessesMock).toHaveBeenCalledWith(
394
386
  expect.objectContaining({
395
387
  instanceDir: tempDir,
@@ -446,6 +438,24 @@ describe("cadet upgrade local", () => {
446
438
  expect(consoleLogSpy.mock.calls.flat().join("\n")).toContain("upgraded to");
447
439
  });
448
440
 
441
+ test("failed safety backup aborts before runtime installation or service replacement", async () => {
442
+ createBackupMock.mockResolvedValue(null);
443
+ await expect(upgrade()).rejects.toThrow("process.exit(1)");
444
+ expect(ensureLocalRuntimeMock).not.toHaveBeenCalled();
445
+ expect(stopLocalProcessesMock).not.toHaveBeenCalled();
446
+ expect(startLocalDaemonMock).not.toHaveBeenCalled();
447
+ });
448
+
449
+ test("failed target startup restores prior runtime selection and all roots before restarting", async () => {
450
+ startLocalDaemonMock.mockRejectedValueOnce(new Error("target startup failed"));
451
+ await expect(upgrade()).rejects.toThrow("process.exit(1)");
452
+ expect(restoreBackupMock).toHaveBeenCalledWith("http://127.0.0.1:7830", "local-assistant", "/tmp/local-pre-upgrade.tar.gz", { resume: false });
453
+ expect(startLocalDaemonMock).toHaveBeenCalledTimes(2);
454
+ expect(startCesMock).toHaveBeenCalledTimes(2);
455
+ expect(stopLocalProcessesMock).toHaveBeenCalledTimes(2);
456
+ expect(waitForReadyMock).toHaveBeenCalled();
457
+ });
458
+
449
459
  test("skips restart when the local assistant is already on the target version", async () => {
450
460
  loopbackSafeFetchMock.mockResolvedValue({
451
461
  ok: true,
@@ -156,10 +156,10 @@ describe("buildReplayState", () => {
156
156
  expect(state.signingKey).toBe("sign-key");
157
157
  });
158
158
 
159
- test("generates fresh secrets when missing from captured env", () => {
159
+ test("generates a service token but preserves file-backed signing identity when env is absent", () => {
160
160
  const state = buildReplayState({}, {});
161
161
  expect(state.bootstrapSecret).toBeUndefined();
162
162
  expect(state.cesServiceToken).toMatch(/^[0-9a-f]{64}$/);
163
- expect(state.signingKey).toMatch(/^[0-9a-f]{64}$/);
163
+ expect(state.signingKey).toBeUndefined();
164
164
  });
165
165
  });
@@ -0,0 +1,185 @@
1
+ import { afterAll, beforeEach, expect, spyOn, test } from "bun:test";
2
+ import * as backups from "../lib/backup-ops.js";
3
+ import * as docker from "../lib/docker.js";
4
+ import * as lifecycle from "../lib/upgrade-lifecycle.js";
5
+ import * as config from "../lib/assistant-config.js";
6
+ import { mockCommands } from "./commands-mock.js";
7
+ import * as loopback from "../lib/loopback-fetch.js";
8
+
9
+ const capture = spyOn(backups, "createBackup");
10
+ const images = spyOn(docker, "captureImageRefs");
11
+ const pull = spyOn(docker, "pullImage");
12
+ const wake = spyOn(docker, "wakeContainers");
13
+ const save = spyOn(config, "saveAssistantEntry");
14
+ const restore = spyOn(backups, "restoreBackup");
15
+ const sleep = spyOn(docker, "sleepContainers");
16
+ const stop = spyOn(docker, "stopContainers");
17
+ const start = spyOn(docker, "startContainers");
18
+ const fetch = spyOn(loopback, "loopbackSafeFetch");
19
+ const res = docker.dockerResourceNames("recovery");
20
+ const containerOptions = {
21
+ gatewayPort: 7830,
22
+ assistantPort: 3001,
23
+ imageTags: {
24
+ assistant: "assistant:old",
25
+ gateway: "gateway:old",
26
+ "credential-executor": "ces:old",
27
+ },
28
+ instanceName: "recovery",
29
+ res,
30
+ };
31
+ const events: string[] = [];
32
+ beforeEach(() => {
33
+ events.length = 0;
34
+ capture.mockReset();
35
+ images.mockReset();
36
+ pull.mockReset();
37
+ wake.mockReset();
38
+ save.mockReset();
39
+ save.mockImplementation(() => {});
40
+ restore.mockReset();
41
+ sleep.mockReset();
42
+ stop.mockReset();
43
+ start.mockReset();
44
+ fetch.mockReset();
45
+ restore.mockImplementation(async () => {
46
+ events.push("restore-roots");
47
+ return true;
48
+ });
49
+ sleep.mockImplementation(async () => {
50
+ events.push("sleep");
51
+ });
52
+ stop.mockImplementation(async () => {
53
+ events.push("remove");
54
+ });
55
+ start.mockImplementation(async () => {
56
+ events.push("start-old-version");
57
+ });
58
+ fetch.mockImplementation(async () => {
59
+ events.push("check-ready");
60
+ return Response.json({ ready: true });
61
+ });
62
+ });
63
+ afterAll(() => {
64
+ for (const spy of [
65
+ restore,
66
+ sleep,
67
+ stop,
68
+ start,
69
+ fetch,
70
+ capture,
71
+ images,
72
+ pull,
73
+ wake,
74
+ save,
75
+ ])
76
+ spy.mockRestore();
77
+ });
78
+
79
+ test("failed-state recovery restores tar roots while stopped before restarting old services", async () => {
80
+ expect(
81
+ await lifecycle.attemptFailedStateRestore({
82
+ runtimeUrl: "http://127.0.0.1:1",
83
+ assistantId: "recovery",
84
+ backupPath: "pre-upgrade.tar.gz",
85
+ backupLabel: "pre-upgrade",
86
+ res,
87
+ containerOptions,
88
+ }),
89
+ ).toEqual({ restored: true, ready: true });
90
+ expect(restore).toHaveBeenCalledWith(
91
+ "http://127.0.0.1:1",
92
+ "recovery",
93
+ "pre-upgrade.tar.gz",
94
+ { resume: false },
95
+ );
96
+ expect(events).toEqual([
97
+ "restore-roots",
98
+ "sleep",
99
+ "remove",
100
+ "start-old-version",
101
+ "check-ready",
102
+ ]);
103
+ });
104
+
105
+ test("failed root restore does not restart a partial installation", async () => {
106
+ restore.mockResolvedValue(false);
107
+ expect(
108
+ await lifecycle.attemptFailedStateRestore({
109
+ runtimeUrl: "http://127.0.0.1:1",
110
+ assistantId: "recovery",
111
+ backupPath: "pre-upgrade.tar.gz",
112
+ backupLabel: "pre-upgrade",
113
+ res,
114
+ containerOptions,
115
+ }),
116
+ ).toEqual({ restored: false, ready: false });
117
+ expect(start).not.toHaveBeenCalled();
118
+ expect(stop).not.toHaveBeenCalled();
119
+ expect(fetch).not.toHaveBeenCalled();
120
+ });
121
+
122
+ test("versioned restore installs roots into stopped target containers before first startup", async () => {
123
+ const commands = mockCommands();
124
+ commands.on(
125
+ "docker",
126
+ (args) => args[0] === "inspect",
127
+ () => ({
128
+ stdout: JSON.stringify(["ACTOR_TOKEN_SIGNING_KEY=" + "01".repeat(32)]),
129
+ }),
130
+ );
131
+ const entry: config.AssistantEntry = {
132
+ assistantId: "recovery",
133
+ cloud: "docker",
134
+ runtimeUrl: "http://127.0.0.1:1",
135
+ };
136
+ capture.mockImplementation(async () => {
137
+ events.push("capture-safety");
138
+ return "/tmp/pre-rollback.tar.gz";
139
+ });
140
+ images.mockResolvedValue({
141
+ assistant: "assistant:current",
142
+ gateway: "gateway:current",
143
+ "credential-executor": "ces:current",
144
+ });
145
+ pull.mockResolvedValue(undefined);
146
+ start.mockImplementation(async (options) => {
147
+ expect(options.createOnly).toBe(true);
148
+ events.push("create-stopped-target");
149
+ });
150
+ wake.mockImplementation(async () => {
151
+ events.push("wake-restored-target");
152
+ });
153
+ fetch.mockImplementation(async (url) => {
154
+ if (String(url).includes("/healthz"))
155
+ return Response.json({ version: "0.12.11" });
156
+ if (String(url).includes("/readyz")) {
157
+ events.push("check-ready");
158
+ return Response.json({ ready: true });
159
+ }
160
+ return Response.json({});
161
+ });
162
+ try {
163
+ await lifecycle.performDockerRollback(entry, {
164
+ targetVersion: "0.12.10",
165
+ backupPath: "/tmp/source.tar.gz",
166
+ });
167
+ expect(restore).toHaveBeenCalledWith(
168
+ entry.runtimeUrl,
169
+ entry.assistantId,
170
+ "/tmp/source.tar.gz",
171
+ { resume: false },
172
+ );
173
+ expect(events).toEqual([
174
+ "capture-safety",
175
+ "sleep",
176
+ "remove",
177
+ "create-stopped-target",
178
+ "restore-roots",
179
+ "wake-restored-target",
180
+ "check-ready",
181
+ ]);
182
+ } finally {
183
+ commands.restore();
184
+ }
185
+ });
@@ -173,7 +173,7 @@ mock.module("../lib/ingress-config.js", () => ({
173
173
  }));
174
174
 
175
175
  const ensureTunnelEdgeMock = mock<typeof nginxIngress.ensureTunnelEdge>(
176
- async () => ({ port: 7840, started: true, includesWebApp: true }),
176
+ async () => ({ port: 7840, started: true, includesWebApp: true, shellMode: "web" }),
177
177
  );
178
178
  const isIngressRunningMock = mock<typeof nginxIngress.isIngressRunning>(
179
179
  () => false,
@@ -305,6 +305,7 @@ beforeEach(() => {
305
305
  port: 7840,
306
306
  started: true,
307
307
  includesWebApp: true,
308
+ shellMode: "web",
308
309
  });
309
310
  isIngressRunningMock.mockReset();
310
311
  isIngressRunningMock.mockReturnValue(false);
@@ -1073,6 +1074,7 @@ describe("cadet wake — tunnel edge restore", () => {
1073
1074
  port: 7840,
1074
1075
  started: false,
1075
1076
  includesWebApp: true,
1077
+ shellMode: "web",
1076
1078
  });
1077
1079
  process.argv = ["bun", "cadet", "wake", "docker-1"];
1078
1080
 
@@ -1579,6 +1581,7 @@ describe("cadet wake — tunnel edge restore", () => {
1579
1581
  port: 7841,
1580
1582
  started: false,
1581
1583
  includesWebApp: true,
1584
+ shellMode: "web",
1582
1585
  });
1583
1586
 
1584
1587
  await wake();
@@ -227,3 +227,28 @@ test("preserves PID tracking when process termination fails", async () => {
227
227
  expect(isProcessAlive(pidFile)).toEqual({ alive: true, pid: process.pid });
228
228
  rmSync(dir, { recursive: true, force: true });
229
229
  });
230
+
231
+ test("graceful-only stop times out without SIGKILL or losing PID identity", async () => {
232
+ const originalKill = process.kill;
233
+ const calls: Array<NodeJS.Signals | number | undefined> = [];
234
+ process.kill = ((_pid: number, signal?: NodeJS.Signals | number) => {
235
+ calls.push(signal);
236
+ return true;
237
+ }) as typeof process.kill;
238
+ try {
239
+ expect(
240
+ await stopProcess(
241
+ 4812,
242
+ "backup service",
243
+ 0,
244
+ "linux",
245
+ undefined,
246
+ () => "cadet-daemon",
247
+ true,
248
+ ),
249
+ ).toBe(false);
250
+ expect(calls).toEqual([0, "SIGTERM", 0]);
251
+ } finally {
252
+ process.kill = originalKill;
253
+ }
254
+ });