@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
@@ -1,268 +0,0 @@
1
- import {
2
- afterEach,
3
- beforeEach,
4
- describe,
5
- expect,
6
- mock,
7
- spyOn,
8
- test,
9
- } from "bun:test";
10
- import {
11
- existsSync,
12
- mkdirSync,
13
- mkdtempSync,
14
- readFileSync,
15
- rmSync,
16
- writeFileSync,
17
- } from "node:fs";
18
- import { tmpdir } from "node:os";
19
- import { join } from "node:path";
20
-
21
- import type { AssistantEntry } from "../lib/assistant-config.js";
22
- import { restoreBackup } from "../lib/backup-ops.js";
23
- import {
24
- bundleFileSizeBytes,
25
- formatBundleSizeMb,
26
- importStagedBundle,
27
- RESTORE_STAGING_DIRNAME,
28
- stageBundleForRestore,
29
- stagingTargetFromEntry,
30
- } from "../lib/bundle-staging.js";
31
- import * as guardianToken from "../lib/guardian-token.js";
32
- import * as loopbackFetch from "../lib/loopback-fetch.js";
33
- import * as stepRunner from "../lib/step-runner.js";
34
-
35
- function makeEntry(overrides: Partial<AssistantEntry> = {}): AssistantEntry {
36
- return {
37
- assistantId: "assistant-123",
38
- runtimeUrl: "http://127.0.0.1:7831",
39
- cloud: "local",
40
- hatchedAt: new Date().toISOString(),
41
- ...overrides,
42
- } as AssistantEntry;
43
- }
44
-
45
- let tmpRoot: string;
46
- let fetchSpy: ReturnType<
47
- typeof spyOn<typeof loopbackFetch, "loopbackSafeFetch">
48
- >;
49
- let execSpy: ReturnType<typeof spyOn<typeof stepRunner, "exec">>;
50
-
51
- beforeEach(() => {
52
- tmpRoot = mkdtempSync(join(tmpdir(), "cli-bundle-staging-"));
53
- fetchSpy = spyOn(loopbackFetch, "loopbackSafeFetch");
54
- execSpy = spyOn(stepRunner, "exec").mockResolvedValue(undefined);
55
- });
56
-
57
- afterEach(() => {
58
- fetchSpy.mockRestore();
59
- execSpy.mockRestore();
60
- rmSync(tmpRoot, { recursive: true, force: true });
61
- });
62
-
63
- describe("stagingTargetFromEntry", () => {
64
- test("returns docker staging for docker topology", () => {
65
- expect(stagingTargetFromEntry(makeEntry({ cloud: "docker" }))).toEqual({
66
- kind: "docker",
67
- assistantId: "assistant-123",
68
- });
69
- });
70
-
71
- test("returns local staging when instanceDir is present", () => {
72
- expect(
73
- stagingTargetFromEntry(
74
- makeEntry({
75
- cloud: "local",
76
- resources: {
77
- instanceDir: "/tmp/instance",
78
- } as AssistantEntry["resources"],
79
- }),
80
- ),
81
- ).toEqual({ kind: "local", instanceDir: "/tmp/instance" });
82
- });
83
-
84
- test("returns null for unknown topologies", () => {
85
- expect(
86
- stagingTargetFromEntry(makeEntry({ cloud: "custom", sshUser: "user" })),
87
- ).toBeNull();
88
- });
89
- });
90
-
91
- describe("stageBundleForRestore", () => {
92
- test("copies a host bundle into the local workspace staging directory", async () => {
93
- const instanceDir = join(tmpRoot, "instance");
94
- const hostBundle = join(tmpRoot, "backup.vbundle");
95
- writeFileSync(hostBundle, "bundle-bytes");
96
-
97
- const staged = await stageBundleForRestore(
98
- { kind: "local", instanceDir },
99
- hostBundle,
100
- );
101
-
102
- const dest = join(instanceDir, "workspace", staged.relativePath);
103
- expect(staged.relativePath.startsWith(`${RESTORE_STAGING_DIRNAME}/`)).toBe(
104
- true,
105
- );
106
- expect(staged.relativePath.endsWith(".vbundle")).toBe(true);
107
- expect(readFileSync(dest, "utf8")).toBe("bundle-bytes");
108
- expect(execSpy).not.toHaveBeenCalled();
109
-
110
- await staged.cleanup();
111
- expect(existsSync(dest)).toBe(false);
112
- });
113
-
114
- test("copies a host bundle into the docker workspace via docker cp", async () => {
115
- const hostBundle = join(tmpRoot, "backup.vbundle");
116
- writeFileSync(hostBundle, "bundle-bytes");
117
-
118
- const staged = await stageBundleForRestore(
119
- { kind: "docker", assistantId: "assistant-123" },
120
- hostBundle,
121
- );
122
-
123
- expect(execSpy).toHaveBeenCalledWith("docker", [
124
- "exec",
125
- "assistant-123-assistant",
126
- "mkdir",
127
- "-p",
128
- `/workspace/${RESTORE_STAGING_DIRNAME}`,
129
- ]);
130
- expect(execSpy).toHaveBeenCalledWith("docker", [
131
- "cp",
132
- hostBundle,
133
- `assistant-123-assistant:/workspace/${staged.relativePath}`,
134
- ]);
135
-
136
- await staged.cleanup();
137
- expect(execSpy).toHaveBeenCalledWith("docker", [
138
- "exec",
139
- "assistant-123-assistant",
140
- "rm",
141
- "-f",
142
- `/workspace/${staged.relativePath}`,
143
- ]);
144
- });
145
- });
146
-
147
- describe("importStagedBundle", () => {
148
- test("returns a direct 200 assistant response", async () => {
149
- const payload = { success: true, summary: { total_files: 1 } };
150
- fetchSpy.mockResolvedValue(
151
- new Response(JSON.stringify(payload), { status: 200 }),
152
- );
153
-
154
- const response = await importStagedBundle(
155
- "http://127.0.0.1:7831",
156
- "token",
157
- `${RESTORE_STAGING_DIRNAME}/backup.vbundle`,
158
- );
159
-
160
- expect(response.status).toBe(200);
161
- expect(await response.json()).toEqual(payload);
162
- expect(fetchSpy).toHaveBeenCalledTimes(1);
163
- const [url, init] = fetchSpy.mock.calls[0];
164
- expect(url).toBe("http://127.0.0.1:7831/v1/migrations/import");
165
- expect(init?.headers).toMatchObject({
166
- Authorization: "Bearer token",
167
- "Content-Type": "application/json",
168
- });
169
- expect(init?.body).toBe(
170
- JSON.stringify({
171
- path: `${RESTORE_STAGING_DIRNAME}/backup.vbundle`,
172
- }),
173
- );
174
- });
175
-
176
- test("polls a 202 gateway job until complete", async () => {
177
- const realSetTimeout = globalThis.setTimeout;
178
- const timeoutSpy = mock((fn: (...args: unknown[]) => void) =>
179
- realSetTimeout(fn, 0),
180
- );
181
- globalThis.setTimeout = timeoutSpy as unknown as typeof setTimeout;
182
-
183
- try {
184
- fetchSpy
185
- .mockResolvedValueOnce(
186
- new Response(JSON.stringify({ job_id: "job-1", status: "pending" }), {
187
- status: 202,
188
- }),
189
- )
190
- .mockResolvedValueOnce(
191
- new Response(JSON.stringify({ status: "processing" }), {
192
- status: 200,
193
- }),
194
- )
195
- .mockResolvedValueOnce(
196
- new Response(
197
- JSON.stringify({
198
- status: "complete",
199
- result: { success: true, summary: { total_files: 2 } },
200
- }),
201
- { status: 200 },
202
- ),
203
- );
204
-
205
- const response = await importStagedBundle(
206
- "http://127.0.0.1:7831",
207
- "token",
208
- `${RESTORE_STAGING_DIRNAME}/backup.vbundle`,
209
- );
210
-
211
- expect(response.status).toBe(200);
212
- expect(await response.json()).toEqual({
213
- success: true,
214
- summary: { total_files: 2 },
215
- });
216
- expect(fetchSpy).toHaveBeenCalledTimes(3);
217
- expect(String(fetchSpy.mock.calls[1][0])).toBe(
218
- "http://127.0.0.1:7831/v1/migrations/import/job-1/status",
219
- );
220
- } finally {
221
- globalThis.setTimeout = realSetTimeout;
222
- }
223
- });
224
- });
225
-
226
- describe("restoreBackup staged path", () => {
227
- test("stages a local bundle and posts JSON { path }", async () => {
228
- const instanceDir = join(tmpRoot, "instance");
229
- mkdirSync(join(instanceDir, "workspace"), { recursive: true });
230
- const backupPath = join(tmpRoot, "backup.vbundle");
231
- writeFileSync(backupPath, "bundle-bytes");
232
-
233
- const loadSpy = spyOn(guardianToken, "loadGuardianToken").mockReturnValue({
234
- accessToken: "local-token",
235
- accessTokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
236
- } as ReturnType<typeof guardianToken.loadGuardianToken>);
237
-
238
- fetchSpy.mockResolvedValue(
239
- new Response(JSON.stringify({ success: true }), { status: 200 }),
240
- );
241
-
242
- try {
243
- const ok = await restoreBackup(
244
- "http://127.0.0.1:7831",
245
- "assistant-123",
246
- backupPath,
247
- { kind: "local", instanceDir },
248
- );
249
- expect(ok).toBe(true);
250
- expect(fetchSpy).toHaveBeenCalledTimes(1);
251
- const init = fetchSpy.mock.calls[0][1];
252
- expect(init?.headers).toMatchObject({
253
- "Content-Type": "application/json",
254
- });
255
- const posted = JSON.parse(String(init?.body)) as { path: string };
256
- expect(posted.path.startsWith(`${RESTORE_STAGING_DIRNAME}/`)).toBe(true);
257
- } finally {
258
- loadSpy.mockRestore();
259
- }
260
- });
261
- });
262
-
263
- describe("bundle size helpers", () => {
264
- test("formatBundleSizeMb reports two decimal megabytes", () => {
265
- expect(formatBundleSizeMb(1024 * 1024)).toBe("1.00");
266
- expect(bundleFileSizeBytes(join(tmpRoot, "missing.vbundle"))).toBe(0);
267
- });
268
- });
@@ -1,160 +0,0 @@
1
- /**
2
- * Tests for `cadet unpair <name>`: forget a paired (cloud:"paired") assistant
3
- * by removing its lockfile entry + guardian token. Refuses non-paired entries.
4
- */
5
- import {
6
- afterAll,
7
- afterEach,
8
- beforeEach,
9
- describe,
10
- expect,
11
- spyOn,
12
- test,
13
- } from "bun:test";
14
- import { mkdtempSync, rmSync } from "node:fs";
15
- import { tmpdir } from "node:os";
16
- import { join } from "node:path";
17
-
18
- const testDir = mkdtempSync(join(tmpdir(), "unpair-test-"));
19
- const ORIGINAL_LOCKFILE_DIR = process.env.CADET_LOCKFILE_DIR;
20
- const ORIGINAL_CONFIG_HOME = process.env.XDG_CONFIG_HOME;
21
- const ORIGINAL_ARGV = [...process.argv];
22
-
23
- import { unpair } from "../commands/unpair.js";
24
- import {
25
- findAssistantByName,
26
- saveAssistantEntry,
27
- } from "../lib/assistant-config.js";
28
- import {
29
- deleteGuardianToken,
30
- loadGuardianToken,
31
- saveGuardianToken,
32
- } from "../lib/guardian-token.js";
33
-
34
- function seedToken(assistantId: string): void {
35
- saveGuardianToken(assistantId, {
36
- guardianPrincipalId: "imported",
37
- accessToken: "acc",
38
- accessTokenExpiresAt: Date.now() + 3_600_000,
39
- refreshToken: "ref",
40
- refreshTokenExpiresAt: Date.now() + 3_600_000,
41
- refreshAfter: "",
42
- isNew: false,
43
- deviceId: "dev",
44
- leasedAt: new Date().toISOString(),
45
- });
46
- }
47
-
48
- /** Run unpair with console.error + process.exit spied. */
49
- async function runUnpair(): Promise<{ exited: boolean; errors: string }> {
50
- const errors: string[] = [];
51
- const logSpy = spyOn(console, "log").mockImplementation(() => {});
52
- const errSpy = spyOn(console, "error").mockImplementation(
53
- (...a: unknown[]) => {
54
- errors.push(a.join(" "));
55
- },
56
- );
57
- const exitSpy = spyOn(process, "exit").mockImplementation(((c?: number) => {
58
- throw new Error(`exit:${c}`);
59
- }) as never);
60
- let exited = false;
61
- try {
62
- await unpair();
63
- } catch (e) {
64
- exited = (e as Error).message === "exit:1";
65
- } finally {
66
- logSpy.mockRestore();
67
- errSpy.mockRestore();
68
- exitSpy.mockRestore();
69
- }
70
- return { exited, errors: errors.join("\n") };
71
- }
72
-
73
- describe("cadet unpair", () => {
74
- beforeEach(() => {
75
- process.env.CADET_LOCKFILE_DIR = testDir;
76
- process.env.XDG_CONFIG_HOME = testDir;
77
- });
78
-
79
- afterEach(() => {
80
- process.argv = [...ORIGINAL_ARGV];
81
- if (ORIGINAL_LOCKFILE_DIR === undefined)
82
- delete process.env.CADET_LOCKFILE_DIR;
83
- else process.env.CADET_LOCKFILE_DIR = ORIGINAL_LOCKFILE_DIR;
84
- if (ORIGINAL_CONFIG_HOME === undefined) delete process.env.XDG_CONFIG_HOME;
85
- else process.env.XDG_CONFIG_HOME = ORIGINAL_CONFIG_HOME;
86
- });
87
-
88
- afterAll(() => {
89
- rmSync(testDir, { recursive: true, force: true });
90
- });
91
-
92
- test("removes a paired entry's lockfile entry and guardian token", async () => {
93
- saveAssistantEntry({
94
- assistantId: "px",
95
- name: "Paired Box",
96
- runtimeUrl: "http://10.0.0.9:7830",
97
- cloud: "paired",
98
- paired: true,
99
- });
100
- seedToken("px");
101
- expect(findAssistantByName("px")).not.toBeNull();
102
- expect(loadGuardianToken("px")).not.toBeNull();
103
-
104
- // Non-interactive test env → use --yes to bypass the confirmation prompt.
105
- process.argv = ["bun", "cadet", "unpair", "px", "--yes"];
106
- const { exited } = await runUnpair();
107
-
108
- expect(exited).toBe(false);
109
- expect(findAssistantByName("px")).toBeNull();
110
- expect(loadGuardianToken("px")).toBeNull();
111
- });
112
-
113
- test("refuses to unpair without --yes in a non-interactive terminal", async () => {
114
- saveAssistantEntry({
115
- assistantId: "py",
116
- name: "Paired Two",
117
- runtimeUrl: "http://10.0.0.9:7830",
118
- cloud: "paired",
119
- paired: true,
120
- });
121
- seedToken("py");
122
-
123
- process.argv = ["bun", "cadet", "unpair", "py"]; // no --yes
124
- const { exited, errors } = await runUnpair();
125
-
126
- expect(exited).toBe(true);
127
- expect(errors).toContain("--yes");
128
- // Not removed — confirmation was required.
129
- expect(findAssistantByName("py")).not.toBeNull();
130
- expect(loadGuardianToken("py")).not.toBeNull();
131
- });
132
-
133
- test("refuses a non-paired (local) assistant and leaves it intact", async () => {
134
- saveAssistantEntry({
135
- assistantId: "desk",
136
- name: "Desk",
137
- runtimeUrl: "http://127.0.0.1:7830",
138
- cloud: "local",
139
- });
140
-
141
- process.argv = ["bun", "cadet", "unpair", "desk"];
142
- const { exited, errors } = await runUnpair();
143
-
144
- expect(exited).toBe(true);
145
- expect(errors).toContain("cadet retire");
146
- expect(findAssistantByName("desk")).not.toBeNull(); // untouched
147
- });
148
-
149
- test("errors on an unknown name", async () => {
150
- process.argv = ["bun", "cadet", "unpair", "nope"];
151
- const { exited } = await runUnpair();
152
- expect(exited).toBe(true);
153
- });
154
-
155
- test("deleteGuardianToken is a no-op when the token is absent", () => {
156
- // No token seeded for this id; calling (twice) must not throw.
157
- expect(() => deleteGuardianToken("ghost")).not.toThrow();
158
- expect(() => deleteGuardianToken("ghost")).not.toThrow();
159
- });
160
- });