@mycadet/cli 0.12.11 → 0.13.0

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 (86) 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/ingress.ts +2 -2
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/package.json +3 -2
  9. package/src/__tests__/assistant-client-cloud-auth.test.ts +2 -2
  10. package/src/__tests__/backup-ops-matching.test.ts +15 -13
  11. package/src/__tests__/backup-ops.test.ts +105 -0
  12. package/src/__tests__/backup.test.ts +114 -673
  13. package/src/__tests__/cloud-releases.test.ts +82 -0
  14. package/src/__tests__/commands-mock.ts +101 -0
  15. package/src/__tests__/debug.test.ts +17 -91
  16. package/src/__tests__/devices.test.ts +2 -2
  17. package/src/__tests__/gateway-token.test.ts +267 -0
  18. package/src/__tests__/host-teleport-docker.test.ts +371 -0
  19. package/src/__tests__/host-teleport.test.ts +405 -0
  20. package/src/__tests__/nginx-ingress.test.ts +182 -23
  21. package/src/__tests__/orphan-detection.test.ts +1 -1
  22. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  23. package/src/__tests__/ps-cloud-status.test.ts +6 -6
  24. package/src/__tests__/root-backup-docker.test.ts +294 -0
  25. package/src/__tests__/root-backup.test.ts +527 -0
  26. package/src/__tests__/root-restore-command.test.ts +147 -0
  27. package/src/__tests__/root-signing-key.test.ts +50 -0
  28. package/src/__tests__/setup.test.ts +3 -3
  29. package/src/__tests__/sleep.test.ts +11 -0
  30. package/src/__tests__/teleport.test.ts +68 -2996
  31. package/src/__tests__/tunnel.test.ts +1 -1
  32. package/src/__tests__/upgrade-local.test.ts +21 -11
  33. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  34. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  35. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  36. package/src/commands/backup.ts +44 -399
  37. package/src/commands/client.ts +3 -3
  38. package/src/commands/completions.ts +0 -2
  39. package/src/commands/debug.ts +75 -131
  40. package/src/commands/devices.ts +51 -17
  41. package/src/commands/gateway/token.ts +77 -23
  42. package/src/commands/pair.ts +2 -2
  43. package/src/commands/restore.ts +77 -444
  44. package/src/commands/retire.ts +2 -3
  45. package/src/commands/sleep.ts +15 -2
  46. package/src/commands/teleport.ts +47 -1559
  47. package/src/commands/upgrade.ts +78 -156
  48. package/src/commands/wake.ts +1 -1
  49. package/src/index.ts +0 -4
  50. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  51. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  52. package/src/lib/__tests__/web-dist.test.ts +27 -1
  53. package/src/lib/assistant-client.ts +1 -1
  54. package/src/lib/assistant-config.ts +0 -18
  55. package/src/lib/backup-ops.ts +37 -251
  56. package/src/lib/cloud-client.ts +10 -13
  57. package/src/lib/cloud-login.ts +2 -1
  58. package/src/lib/cloud-releases.ts +54 -10
  59. package/src/lib/cloud-values.ts +1 -1
  60. package/src/lib/confirm-action.ts +2 -2
  61. package/src/lib/docker.ts +59 -22
  62. package/src/lib/github-repository.ts +2 -0
  63. package/src/lib/guardian-token.ts +1 -23
  64. package/src/lib/hatch-local.ts +17 -1
  65. package/src/lib/host-teleport.ts +506 -0
  66. package/src/lib/http-client.ts +25 -0
  67. package/src/lib/live-voice/tunnel.test.ts +1 -1
  68. package/src/lib/live-voice/tunnel.ts +1 -1
  69. package/src/lib/local.ts +4 -2
  70. package/src/lib/nginx-ingress.ts +134 -18
  71. package/src/lib/process.ts +13 -0
  72. package/src/lib/root-backup.ts +917 -0
  73. package/src/lib/root-signing-key.ts +31 -0
  74. package/src/lib/runtime-url.ts +0 -29
  75. package/src/lib/teleport-backup.ts +3 -16
  76. package/src/lib/upgrade-lifecycle.ts +63 -141
  77. package/src/lib/web-dist.ts +23 -1
  78. package/src/__tests__/bundle-staging.test.ts +0 -268
  79. package/src/__tests__/unpair.test.ts +0 -160
  80. package/src/commands/rollback.ts +0 -479
  81. package/src/commands/unpair.ts +0 -122
  82. package/src/lib/__tests__/job-polling.test.ts +0 -278
  83. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  84. package/src/lib/bundle-staging.ts +0 -220
  85. package/src/lib/job-polling.ts +0 -195
  86. 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
- });