@mycadet/cli 0.12.11 → 0.13.0-dev.202610171617.9ea363e

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
@@ -102,13 +102,13 @@ describe("paired lifecycle guards", () => {
102
102
  expect(errors).not.toContain("only works with local and docker");
103
103
  });
104
104
 
105
- test("retire refuses a paired entry and points to unpair", async () => {
105
+ test("retire refuses a paired entry and points to the host", async () => {
106
106
  process.argv = ["bun", "cadet", "retire", "px", "--yes"];
107
107
  const { exited, errors } = await runGuarded(retire);
108
108
 
109
109
  expect(exited).toBe(true);
110
110
  expect(errors).toContain("paired from another machine");
111
- expect(errors).toContain("cadet unpair");
111
+ expect(errors).toContain("cadet devices");
112
112
  // It must NOT fall through to the generic "Unknown cloud type" path.
113
113
  expect(errors).not.toContain("Unknown cloud type");
114
114
  });
@@ -230,13 +230,13 @@ describe("cadet ps — platform status line", () => {
230
230
  assistantId: "assistant-123",
231
231
  name: "Alice",
232
232
  runtimeUrl: "https://platform.example",
233
- cloud: "cadet",
233
+ cloud: "cloud",
234
234
  },
235
235
  {
236
236
  assistantId: "assistant-456",
237
237
  name: "Bob",
238
238
  runtimeUrl: "https://platform.example/b",
239
- cloud: "cadet",
239
+ cloud: "cloud",
240
240
  },
241
241
  ]);
242
242
  getActiveAssistantMock.mockReturnValue(null);
@@ -278,7 +278,7 @@ describe("cadet ps — platform status line", () => {
278
278
  assistantId: "assistant-123",
279
279
  name: "Alice",
280
280
  runtimeUrl: "https://platform.example",
281
- cloud: "cadet",
281
+ cloud: "cloud",
282
282
  },
283
283
  ]);
284
284
  getActiveAssistantMock.mockReturnValue("assistant-123");
@@ -287,7 +287,7 @@ describe("cadet ps — platform status line", () => {
287
287
 
288
288
  const output = stdout.join("\n");
289
289
  expect(output).toContain("* Alice");
290
- expect(output).toContain("remote: cadet");
290
+ expect(output).toContain("remote: cloud");
291
291
  expect(output).toContain("https://platform.example");
292
292
  expect(output).not.toContain("* assistant-123");
293
293
  });
@@ -298,13 +298,13 @@ describe("cadet ps — platform status line", () => {
298
298
  assistantId: "assistant-123",
299
299
  name: "Alice",
300
300
  runtimeUrl: "https://platform.example/a",
301
- cloud: "cadet",
301
+ cloud: "cloud",
302
302
  },
303
303
  {
304
304
  assistantId: "assistant-456",
305
305
  name: "Bob",
306
306
  runtimeUrl: "https://platform.example/b",
307
- cloud: "cadet",
307
+ cloud: "cloud",
308
308
  },
309
309
  ]);
310
310
  getActiveAssistantMock.mockReturnValue("assistant-123");
@@ -0,0 +1,294 @@
1
+ import { afterEach, beforeEach, expect, test } from "bun:test";
2
+ import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { pipeline } from "node:stream/promises";
7
+ import * as tar from "tar";
8
+ import {
9
+ DockerRootStorage,
10
+ packRoot,
11
+ restoreRootBackup,
12
+ stageRootArchive,
13
+ STATE_ROOTS,
14
+ } from "../lib/root-backup.js";
15
+ import {
16
+ dockerResourceNames,
17
+ sleepContainers,
18
+ startContainers,
19
+ } from "../lib/docker.js";
20
+ import type { AssistantEntry } from "../lib/assistant-config.js";
21
+ import { mockCommands } from "./commands-mock.js";
22
+
23
+ const entry: AssistantEntry = {
24
+ assistantId: "docker-backup",
25
+ cloud: "docker",
26
+ runtimeUrl: "http://127.0.0.1:1",
27
+ };
28
+ const names = dockerResourceNames(entry.assistantId);
29
+ const containers = [
30
+ names.assistantContainer,
31
+ names.gatewayContainer,
32
+ names.cesContainer,
33
+ ];
34
+ const rootContainers = {
35
+ workspace: names.assistantContainer,
36
+ "gateway-security": names.gatewayContainer,
37
+ "ces-security": names.cesContainer,
38
+ "ces-data": names.cesContainer,
39
+ };
40
+ let directory: string;
41
+ let commands: ReturnType<typeof mockCommands>;
42
+ const running = new Map<string, boolean>();
43
+ let writes = 0;
44
+ let failWrite = 0;
45
+
46
+ beforeEach(async () => {
47
+ directory = await mkdtemp(join(tmpdir(), "docker-root-mock-"));
48
+ commands = mockCommands();
49
+ writes = 0;
50
+ failWrite = 0;
51
+ for (const container of containers) running.set(container, true);
52
+ for (const root of STATE_ROOTS) {
53
+ await mkdir(join(directory, root), { mode: 0o700 });
54
+ await writeFile(join(directory, root, "value"), "original", {
55
+ mode: 0o600,
56
+ });
57
+ }
58
+ await writeFile(
59
+ join(directory, "gateway-security", "actor-token-signing-key"),
60
+ Buffer.alloc(32, 1),
61
+ { mode: 0o600 },
62
+ );
63
+ commands.on("docker", ["info"], {});
64
+ commands.on(
65
+ "docker",
66
+ (args) => args[0] === "inspect",
67
+ ({ args }) => {
68
+ const container = args.at(-1)!;
69
+ if (args.includes("--format"))
70
+ return {
71
+ stdout:
72
+ args[2] === "{{.State.Running}}"
73
+ ? String(running.get(container))
74
+ : running.get(container)
75
+ ? "running"
76
+ : "exited",
77
+ };
78
+ return {
79
+ stdout: JSON.stringify([
80
+ {
81
+ Image: "sha256:service-image",
82
+ State: {
83
+ Running: running.get(container),
84
+ Status: running.get(container) ? "running" : "exited",
85
+ },
86
+ Config: {
87
+ Env: ["RUNTIME_HTTP_PORT=3001", "GATEWAY_PORT=7830"],
88
+ User: "1001:1001",
89
+ },
90
+ Mounts: STATE_ROOTS.filter(
91
+ (root) => rootContainers[root] === container,
92
+ ).map((root) => ({
93
+ Type: "volume",
94
+ Name: `remote-${root}`,
95
+ Source: `/remote/${root}`,
96
+ Destination: `/${root}`,
97
+ RW: true,
98
+ })),
99
+ },
100
+ ]),
101
+ };
102
+ },
103
+ );
104
+ commands.on(
105
+ "docker",
106
+ (args) => args[0] === "kill",
107
+ ({ args }) => {
108
+ expect(args[1]).toBe("--signal=SIGTERM");
109
+ running.set(args.at(-1)!, false);
110
+ return {};
111
+ },
112
+ );
113
+ commands.on(
114
+ "docker",
115
+ (args) => args[0] === "start",
116
+ ({ args }) => {
117
+ running.set(args[1]!, true);
118
+ return {};
119
+ },
120
+ );
121
+ commands.on(
122
+ "docker",
123
+ (args) => args[0] === "cp",
124
+ async ({ args }) => {
125
+ const root = args[1]!.split(":/")[1]!.split("/")[0]!;
126
+ await cp(join(directory, root), args[2]!, { recursive: true });
127
+ return {};
128
+ },
129
+ );
130
+ commands.on(
131
+ "docker",
132
+ (args) => args[0] === "run" && args.includes("-i"),
133
+ async ({ args, input }) => {
134
+ writes++;
135
+ const mount = args[args.indexOf("--mount") + 1]!;
136
+ const root = mount.split("src=remote-")[1]!.split(",")[0]!;
137
+ expect(args).toContain("--network=none");
138
+ expect(args).toContain("--user=0");
139
+ expect(args).toContain("1001:1001");
140
+ expect(mount).not.toContain(directory);
141
+ expect(args[args.indexOf("-ec") + 1]).toContain('chown -R "$1" /restore');
142
+ await rm(join(directory, root), { recursive: true });
143
+ await mkdir(join(directory, root));
144
+ if (writes === failWrite)
145
+ return { code: 1, stderr: "injected volume replacement failure" };
146
+ await pipeline(
147
+ Readable.from(input),
148
+ tar.x({ cwd: join(directory, root), preserveOwner: false }),
149
+ );
150
+ return {};
151
+ },
152
+ );
153
+ });
154
+ afterEach(async () => {
155
+ commands.restore();
156
+ await rm(directory, { recursive: true, force: true });
157
+ });
158
+
159
+ async function archive() {
160
+ const capture = join(directory, "capture");
161
+ await mkdir(capture);
162
+ const storage = new DockerRootStorage(entry);
163
+ await sleepContainers(names, { timeoutMs: 100 });
164
+ await storage.capture(capture);
165
+ const file = join(directory, "backup.tar.gz");
166
+ await packRoot(capture, file);
167
+ return { storage, file };
168
+ }
169
+
170
+ test("Docker subprocess round-trip preserves every root, private files and clears stale entries", async () => {
171
+ const { storage, file } = await archive();
172
+ const stage = await stageRootArchive(file, directory);
173
+ for (const root of STATE_ROOTS)
174
+ await writeFile(join(directory, root, "stale"), "delete");
175
+ await storage.replace(join(stage, "state"));
176
+ for (const root of STATE_ROOTS) {
177
+ expect(await readFile(join(directory, root, "value"), "utf8")).toBe(
178
+ "original",
179
+ );
180
+ await expect(readFile(join(directory, root, "stale"))).rejects.toThrow();
181
+ }
182
+ expect(writes).toBe(4);
183
+ expect(commands.calls.filter((call) => call.args[0] === "kill")).toHaveLength(
184
+ 3,
185
+ );
186
+ });
187
+
188
+ test("failure during Docker replacement recovers prior roots before restarting", async () => {
189
+ const { storage, file } = await archive();
190
+ for (const container of containers) running.set(container, true);
191
+ for (const root of STATE_ROOTS)
192
+ await writeFile(join(directory, root, "value"), "destination");
193
+ failWrite = 2;
194
+ const recovery = join(directory, "recovery.tar.gz");
195
+ await expect(
196
+ restoreRootBackup(
197
+ entry,
198
+ file,
199
+ { recoveryPath: recovery },
200
+ {
201
+ storage,
202
+ sleep: () => sleepContainers(names, { timeoutMs: 100 }),
203
+ wake: async () => {
204
+ for (const container of containers) running.set(container, true);
205
+ },
206
+ save: () => {},
207
+ },
208
+ ),
209
+ ).rejects.toThrow("injected volume replacement failure");
210
+ expect(await storage.running()).toBe(true);
211
+ const previous = await stageRootArchive(recovery, directory);
212
+ for (const root of STATE_ROOTS) {
213
+ expect(await readFile(join(directory, root, "value"), "utf8")).toBe(
214
+ "destination",
215
+ );
216
+ expect(await readFile(join(previous, "state", root, "value"), "utf8")).toBe(
217
+ "destination",
218
+ );
219
+ }
220
+ });
221
+
222
+ test("restart probes gateway and assistant on their own service ports", async () => {
223
+ const storage = new DockerRootStorage(entry);
224
+ expect(await storage.running()).toBe(true);
225
+ for (const container of containers) running.set(container, false);
226
+ commands.on(
227
+ "docker",
228
+ (args) => args[0] === "exec",
229
+ () => ({ stdout: "ready" }),
230
+ );
231
+ await storage.resume();
232
+ const probes = commands.calls.filter((call) => call.args[0] === "exec");
233
+ expect(
234
+ probes.find((call) => call.args[1] === names.gatewayContainer)?.args.at(-1),
235
+ ).toBe("http://127.0.0.1:7830/healthz");
236
+ expect(
237
+ probes
238
+ .find((call) => call.args[1] === names.assistantContainer)
239
+ ?.args.at(-1),
240
+ ).toBe("http://127.0.0.1:3001/readyz");
241
+ });
242
+
243
+ test("default Docker sleep aborts a graceful timeout without forcing termination", async () => {
244
+ commands.on(
245
+ "docker",
246
+ (args) => args[0] === "kill",
247
+ () => ({}),
248
+ );
249
+ await expect(sleepContainers(names, { timeoutMs: 0 })).rejects.toThrow(
250
+ "did not stop gracefully",
251
+ );
252
+ expect(running.get(names.assistantContainer)).toBe(true);
253
+ expect(commands.calls.some((call) => call.args[0] === "stop")).toBe(false);
254
+ expect(
255
+ commands.calls
256
+ .filter((call) => call.args[0] === "kill")
257
+ .every((call) => call.args.includes("--signal=SIGTERM")),
258
+ ).toBe(true);
259
+ });
260
+
261
+ test("create-only provisioning creates every target container without starting services", async () => {
262
+ commands.on(
263
+ "docker",
264
+ (args) => args[0] === "create",
265
+ ({ args }) => {
266
+ running.set(args[args.indexOf("--name") + 1]!, false);
267
+ return { stdout: "created" };
268
+ },
269
+ );
270
+ await startContainers(
271
+ {
272
+ createOnly: true,
273
+ gatewayPort: 7830,
274
+ assistantPort: 3001,
275
+ imageTags: {
276
+ assistant: "assistant:target",
277
+ gateway: "gateway:target",
278
+ "credential-executor": "ces:target",
279
+ },
280
+ instanceName: entry.assistantId,
281
+ res: names,
282
+ },
283
+ () => {},
284
+ );
285
+ const creates = commands.calls.filter((call) => call.args[0] === "create");
286
+ expect(creates).toHaveLength(3);
287
+ expect(creates.every((call) => !call.args.includes("-d"))).toBe(true);
288
+ expect(
289
+ commands.calls.some(
290
+ (call) => call.args[0] === "start" || call.args[0] === "run",
291
+ ),
292
+ ).toBe(false);
293
+ expect([...running.values()].every((value) => !value)).toBe(true);
294
+ });