@mycadet/cli 0.12.10 → 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 (111) 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 +10 -10
  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 +3 -3
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  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 +3 -3
  8. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  9. package/node_modules/@mycadet/service-contracts/src/channels.ts +0 -1
  10. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  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-login-poll.test.ts +69 -0
  17. package/src/__tests__/cloud-releases.test.ts +82 -0
  18. package/src/__tests__/commands-mock.ts +101 -0
  19. package/src/__tests__/debug.test.ts +17 -91
  20. package/src/__tests__/devices.test.ts +64 -2
  21. package/src/__tests__/ensure-binary.test.ts +52 -0
  22. package/src/__tests__/gateway-token.test.ts +267 -0
  23. package/src/__tests__/hatch-next-steps.test.ts +38 -0
  24. package/src/__tests__/host-teleport-docker.test.ts +371 -0
  25. package/src/__tests__/host-teleport.test.ts +405 -0
  26. package/src/__tests__/multi-local.test.ts +3 -3
  27. package/src/__tests__/nginx-ingress.test.ts +587 -36
  28. package/src/__tests__/orphan-detection.test.ts +1 -1
  29. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  30. package/src/__tests__/ps-cloud-status.test.ts +8 -7
  31. package/src/__tests__/root-backup-docker.test.ts +294 -0
  32. package/src/__tests__/root-backup.test.ts +527 -0
  33. package/src/__tests__/root-restore-command.test.ts +147 -0
  34. package/src/__tests__/root-signing-key.test.ts +50 -0
  35. package/src/__tests__/setup.test.ts +17 -7
  36. package/src/__tests__/shell-mode.test.ts +83 -0
  37. package/src/__tests__/sleep.test.ts +11 -0
  38. package/src/__tests__/teleport.test.ts +68 -2996
  39. package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
  40. package/src/__tests__/tunnel-stop.test.ts +226 -0
  41. package/src/__tests__/tunnel.test.ts +22 -4
  42. package/src/__tests__/upgrade-local.test.ts +21 -11
  43. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  44. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  45. package/src/__tests__/wake.test.ts +4 -1
  46. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  47. package/src/commands/backup.ts +44 -399
  48. package/src/commands/client.ts +3 -3
  49. package/src/commands/completions.ts +0 -2
  50. package/src/commands/debug.ts +75 -131
  51. package/src/commands/devices.ts +117 -47
  52. package/src/commands/gateway/token.ts +77 -23
  53. package/src/commands/hatch.ts +10 -52
  54. package/src/commands/pair.ts +9 -3
  55. package/src/commands/ps.ts +4 -2
  56. package/src/commands/restore.ts +77 -444
  57. package/src/commands/retire.ts +2 -3
  58. package/src/commands/sleep.ts +15 -2
  59. package/src/commands/teleport.ts +47 -1559
  60. package/src/commands/tunnel.ts +91 -11
  61. package/src/commands/upgrade.ts +84 -157
  62. package/src/commands/wake.ts +1 -1
  63. package/src/index.ts +0 -4
  64. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  65. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  66. package/src/lib/__tests__/web-dist.test.ts +27 -1
  67. package/src/lib/assistant-client.ts +1 -1
  68. package/src/lib/assistant-config.ts +43 -23
  69. package/src/lib/backup-ops.ts +37 -251
  70. package/src/lib/cloud-client.ts +10 -13
  71. package/src/lib/cloud-login-poll.ts +75 -0
  72. package/src/lib/cloud-login.ts +5 -2
  73. package/src/lib/cloud-releases.ts +54 -10
  74. package/src/lib/cloud-values.ts +1 -1
  75. package/src/lib/confirm-action.ts +2 -2
  76. package/src/lib/docker.ts +65 -34
  77. package/src/lib/ensure-binary.ts +65 -6
  78. package/src/lib/github-repository.ts +2 -0
  79. package/src/lib/guardian-token.ts +1 -23
  80. package/src/lib/hatch-local.ts +18 -52
  81. package/src/lib/hatch-next-steps.ts +15 -2
  82. package/src/lib/host-teleport.ts +506 -0
  83. package/src/lib/http-client.ts +25 -0
  84. package/src/lib/ingress-config.ts +31 -0
  85. package/src/lib/live-voice/tunnel.test.ts +1 -1
  86. package/src/lib/live-voice/tunnel.ts +3 -3
  87. package/src/lib/local.ts +10 -8
  88. package/src/lib/nginx-ingress.ts +572 -95
  89. package/src/lib/process.ts +13 -0
  90. package/src/lib/root-backup.ts +917 -0
  91. package/src/lib/root-signing-key.ts +31 -0
  92. package/src/lib/runtime-url.ts +0 -29
  93. package/src/lib/teleport-backup.ts +3 -16
  94. package/src/lib/tunnel.ts +87 -34
  95. package/src/lib/tunnels/cadet.ts +12 -1
  96. package/src/lib/tunnels/cloudflare.ts +36 -1
  97. package/src/lib/tunnels/ngrok.ts +16 -2
  98. package/src/lib/tunnels/process-lifecycle.ts +26 -0
  99. package/src/lib/tunnels/tailscale.ts +79 -1
  100. package/src/lib/upgrade-lifecycle.ts +63 -141
  101. package/src/lib/web-dist.ts +79 -1
  102. package/src/lib/xdg-log.ts +11 -0
  103. package/src/__tests__/bundle-staging.test.ts +0 -268
  104. package/src/__tests__/unpair.test.ts +0 -160
  105. package/src/commands/rollback.ts +0 -479
  106. package/src/commands/unpair.ts +0 -122
  107. package/src/lib/__tests__/job-polling.test.ts +0 -278
  108. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  109. package/src/lib/bundle-staging.ts +0 -220
  110. package/src/lib/job-polling.ts +0 -195
  111. package/src/lib/local-runtime-client.ts +0 -374
@@ -107,7 +107,7 @@ describe("getKnownPidsFromAssistants", () => {
107
107
  const entry: AssistantEntry = {
108
108
  assistantId: "managed-1",
109
109
  runtimeUrl: "https://cloud.mycadet.ai/foo",
110
- cloud: "cadet",
110
+ cloud: "cloud",
111
111
  };
112
112
  const pids = getKnownPidsFromAssistants([entry]);
113
113
  expect(pids.size).toBe(0);
@@ -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);
@@ -262,7 +262,8 @@ describe("cadet ps — platform status line", () => {
262
262
  // children of an assistant row…
263
263
  expect(output).toContain("ngrok");
264
264
  expect(output).not.toContain("└ ngrok");
265
- expect(output).toContain("nginx edge");
265
+ expect(output).toContain("nginx");
266
+ expect(output).toContain("port ");
266
267
  // …the type column distinguishes the three process kinds…
267
268
  expect(output).toContain("🧑‍🚀 Assistant");
268
269
  expect(output).toContain("🚇 Tunnel");
@@ -277,7 +278,7 @@ describe("cadet ps — platform status line", () => {
277
278
  assistantId: "assistant-123",
278
279
  name: "Alice",
279
280
  runtimeUrl: "https://platform.example",
280
- cloud: "cadet",
281
+ cloud: "cloud",
281
282
  },
282
283
  ]);
283
284
  getActiveAssistantMock.mockReturnValue("assistant-123");
@@ -286,7 +287,7 @@ describe("cadet ps — platform status line", () => {
286
287
 
287
288
  const output = stdout.join("\n");
288
289
  expect(output).toContain("* Alice");
289
- expect(output).toContain("remote: cadet");
290
+ expect(output).toContain("remote: cloud");
290
291
  expect(output).toContain("https://platform.example");
291
292
  expect(output).not.toContain("* assistant-123");
292
293
  });
@@ -297,13 +298,13 @@ describe("cadet ps — platform status line", () => {
297
298
  assistantId: "assistant-123",
298
299
  name: "Alice",
299
300
  runtimeUrl: "https://platform.example/a",
300
- cloud: "cadet",
301
+ cloud: "cloud",
301
302
  },
302
303
  {
303
304
  assistantId: "assistant-456",
304
305
  name: "Bob",
305
306
  runtimeUrl: "https://platform.example/b",
306
- cloud: "cadet",
307
+ cloud: "cloud",
307
308
  },
308
309
  ]);
309
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
+ });