@mycadet/cli 0.12.10 → 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 (105) 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/ingress.ts +3 -3
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  5. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  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-login-poll.test.ts +69 -0
  14. package/src/__tests__/cloud-releases.test.ts +82 -0
  15. package/src/__tests__/commands-mock.ts +101 -0
  16. package/src/__tests__/debug.test.ts +17 -91
  17. package/src/__tests__/devices.test.ts +64 -2
  18. package/src/__tests__/ensure-binary.test.ts +52 -0
  19. package/src/__tests__/gateway-token.test.ts +267 -0
  20. package/src/__tests__/hatch-next-steps.test.ts +38 -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__/multi-local.test.ts +3 -3
  24. package/src/__tests__/nginx-ingress.test.ts +233 -36
  25. package/src/__tests__/orphan-detection.test.ts +1 -1
  26. package/src/__tests__/paired-lifecycle.test.ts +2 -2
  27. package/src/__tests__/ps-cloud-status.test.ts +8 -7
  28. package/src/__tests__/root-backup-docker.test.ts +294 -0
  29. package/src/__tests__/root-backup.test.ts +527 -0
  30. package/src/__tests__/root-restore-command.test.ts +147 -0
  31. package/src/__tests__/root-signing-key.test.ts +50 -0
  32. package/src/__tests__/setup.test.ts +17 -7
  33. package/src/__tests__/sleep.test.ts +11 -0
  34. package/src/__tests__/teleport.test.ts +68 -2996
  35. package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
  36. package/src/__tests__/tunnel-stop.test.ts +226 -0
  37. package/src/__tests__/tunnel.test.ts +13 -3
  38. package/src/__tests__/upgrade-local.test.ts +21 -11
  39. package/src/__tests__/upgrade-replay-env.test.ts +2 -2
  40. package/src/__tests__/upgrade-root-recovery.test.ts +185 -0
  41. package/src/__tests__/windows-lifecycle.test.ts +25 -0
  42. package/src/commands/backup.ts +44 -399
  43. package/src/commands/client.ts +3 -3
  44. package/src/commands/completions.ts +0 -2
  45. package/src/commands/debug.ts +75 -131
  46. package/src/commands/devices.ts +117 -47
  47. package/src/commands/gateway/token.ts +77 -23
  48. package/src/commands/hatch.ts +10 -52
  49. package/src/commands/pair.ts +9 -3
  50. package/src/commands/ps.ts +4 -2
  51. package/src/commands/restore.ts +77 -444
  52. package/src/commands/retire.ts +2 -3
  53. package/src/commands/sleep.ts +15 -2
  54. package/src/commands/teleport.ts +47 -1559
  55. package/src/commands/tunnel.ts +43 -4
  56. package/src/commands/upgrade.ts +84 -157
  57. package/src/commands/wake.ts +1 -1
  58. package/src/index.ts +0 -4
  59. package/src/lib/__tests__/http-client-version.test.ts +29 -0
  60. package/src/lib/__tests__/runtime-url.test.ts +3 -75
  61. package/src/lib/__tests__/web-dist.test.ts +27 -1
  62. package/src/lib/assistant-client.ts +1 -1
  63. package/src/lib/assistant-config.ts +20 -23
  64. package/src/lib/backup-ops.ts +37 -251
  65. package/src/lib/cloud-client.ts +10 -13
  66. package/src/lib/cloud-login-poll.ts +75 -0
  67. package/src/lib/cloud-login.ts +5 -2
  68. package/src/lib/cloud-releases.ts +54 -10
  69. package/src/lib/cloud-values.ts +1 -1
  70. package/src/lib/confirm-action.ts +2 -2
  71. package/src/lib/docker.ts +65 -34
  72. package/src/lib/ensure-binary.ts +65 -6
  73. package/src/lib/github-repository.ts +2 -0
  74. package/src/lib/guardian-token.ts +1 -23
  75. package/src/lib/hatch-local.ts +18 -52
  76. package/src/lib/hatch-next-steps.ts +15 -2
  77. package/src/lib/host-teleport.ts +506 -0
  78. package/src/lib/http-client.ts +25 -0
  79. package/src/lib/live-voice/tunnel.test.ts +1 -1
  80. package/src/lib/live-voice/tunnel.ts +3 -3
  81. package/src/lib/local.ts +10 -8
  82. package/src/lib/nginx-ingress.ts +254 -36
  83. package/src/lib/process.ts +13 -0
  84. package/src/lib/root-backup.ts +917 -0
  85. package/src/lib/root-signing-key.ts +31 -0
  86. package/src/lib/runtime-url.ts +0 -29
  87. package/src/lib/teleport-backup.ts +3 -16
  88. package/src/lib/tunnel.ts +86 -34
  89. package/src/lib/tunnels/cadet.ts +12 -1
  90. package/src/lib/tunnels/cloudflare.ts +36 -1
  91. package/src/lib/tunnels/ngrok.ts +16 -2
  92. package/src/lib/tunnels/process-lifecycle.ts +26 -0
  93. package/src/lib/tunnels/tailscale.ts +79 -1
  94. package/src/lib/upgrade-lifecycle.ts +63 -141
  95. package/src/lib/web-dist.ts +23 -1
  96. package/src/lib/xdg-log.ts +11 -0
  97. package/src/__tests__/bundle-staging.test.ts +0 -268
  98. package/src/__tests__/unpair.test.ts +0 -160
  99. package/src/commands/rollback.ts +0 -479
  100. package/src/commands/unpair.ts +0 -122
  101. package/src/lib/__tests__/job-polling.test.ts +0 -278
  102. package/src/lib/__tests__/local-runtime-client.test.ts +0 -666
  103. package/src/lib/bundle-staging.ts +0 -220
  104. package/src/lib/job-polling.ts +0 -195
  105. package/src/lib/local-runtime-client.ts +0 -374
@@ -1,122 +0,0 @@
1
- /**
2
- * `cadet unpair <name> [--yes]`
3
- *
4
- * Forget a pairing imported from another machine via `cadet connect import`:
5
- * remove its lockfile entry and stored guardian token from THIS machine. Only
6
- * paired assistants (`cloud: "paired"`) can be unpaired — `cadet retire` owns
7
- * local and cloud assistants.
8
- *
9
- * This is client-side only: it forgets the connection here but does not revoke
10
- * the device on the host. (Host-side revocation is `cadet devices`.)
11
- */
12
-
13
- import {
14
- formatAssistantLookupError,
15
- getAssistantDisplayName,
16
- lookupAssistantByIdentifier,
17
- removeAssistantEntry,
18
- } from "../lib/assistant-config";
19
- import { parseAssistantTargetArg } from "../lib/assistant-target-args.js";
20
- import {
21
- canPromptForConfirmation,
22
- confirmAction,
23
- } from "../lib/confirm-action.js";
24
- import { deleteGuardianToken } from "../lib/guardian-token";
25
-
26
- function printUsage(): void {
27
- console.log(`cadet unpair [beta] - Forget a paired assistant imported from another machine
28
-
29
- USAGE:
30
- cadet unpair <name> [--yes]
31
-
32
- ARGUMENTS:
33
- <name> Name or id of the paired assistant to forget
34
-
35
- OPTIONS:
36
- --yes Skip the interactive confirmation prompt (for automation)
37
-
38
- Removes the local connection (lockfile entry + stored token). Only paired
39
- assistants (imported via 'cadet connect import') can be unpaired; use
40
- 'cadet retire' for local or cloud assistants.
41
-
42
- EXAMPLES:
43
- cadet unpair paired-desk
44
- cadet unpair "Desk Box"
45
- cadet unpair paired-desk --yes
46
- `);
47
- }
48
-
49
- export const unpair = Object.assign(
50
- async function unpair(): Promise<void> {
51
- const args = process.argv.slice(3);
52
-
53
- if (args.includes("--help") || args.includes("-h")) {
54
- printUsage();
55
- return;
56
- }
57
-
58
- const yes = args.includes("--yes");
59
- const name = parseAssistantTargetArg(args, []);
60
- if (!name) {
61
- console.error("Error: assistant name or id is required.");
62
- printUsage();
63
- process.exit(1);
64
- }
65
-
66
- const lookup = lookupAssistantByIdentifier(name);
67
- if (lookup.status !== "found") {
68
- console.error(formatAssistantLookupError(name, lookup));
69
- process.exit(1);
70
- }
71
- const entry = lookup.entry;
72
-
73
- if (entry.cloud !== "paired") {
74
- console.error(
75
- `Error: '${name}' is not a paired assistant. Use \`cadet retire\` to remove a local or cloud assistant.`,
76
- );
77
- process.exit(1);
78
- }
79
-
80
- // Print the resolved identity before acting (cli/AGENTS.md).
81
- const displayName = getAssistantDisplayName(entry);
82
- console.log("Pairing to unpair:");
83
- if (displayName !== entry.assistantId) {
84
- console.log(` Name: ${displayName}`);
85
- }
86
- console.log(` ID: ${entry.assistantId}`);
87
- if (entry.runtimeUrl) {
88
- console.log(` Host: ${entry.runtimeUrl}`);
89
- }
90
- console.log("");
91
-
92
- if (!yes) {
93
- if (!canPromptForConfirmation()) {
94
- console.error(
95
- "Error: Refusing to unpair without confirmation in a non-interactive terminal.",
96
- );
97
- console.error("Re-run with --yes to confirm from automation.");
98
- process.exit(1);
99
- }
100
- const confirmed = await confirmAction(
101
- "Press Enter to unpair, or Esc/q to cancel: ",
102
- );
103
- if (!confirmed) {
104
- console.log("Unpair cancelled.");
105
- process.exit(1);
106
- }
107
- }
108
-
109
- removeAssistantEntry(entry.assistantId);
110
- deleteGuardianToken(entry.assistantId);
111
-
112
- console.log(
113
- `Unpaired '${name}' — removed the local connection (lockfile entry + token).`,
114
- );
115
- console.log("");
116
- console.log(
117
- "Note: this only forgets the connection on this machine. The assistant's host can fully revoke this device from its side.",
118
- );
119
- },
120
- { description: "Forget a paired assistant imported from another machine [beta]" },
121
- );
122
-
@@ -1,278 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
2
-
3
- import { pollJobUntilDone } from "../job-polling.js";
4
- import type { UnifiedJobStatus } from "../cloud-client.js";
5
-
6
- describe("pollJobUntilDone", () => {
7
- test("returns terminal 'complete' after N processing polls", async () => {
8
- const statuses: UnifiedJobStatus[] = [
9
- { jobId: "j1", type: "export", status: "processing" },
10
- { jobId: "j1", type: "export", status: "processing" },
11
- {
12
- jobId: "j1",
13
- type: "export",
14
- status: "complete",
15
- bundleKey: "bundles/j1.tar.gz",
16
- },
17
- ];
18
- let i = 0;
19
- const result = await pollJobUntilDone({
20
- poll: async () => statuses[i++]!,
21
- intervalMs: 1,
22
- timeoutMs: 1_000,
23
- label: "test export",
24
- });
25
-
26
- expect(result.status).toBe("complete");
27
- if (result.status === "complete") {
28
- expect(result.bundleKey).toBe("bundles/j1.tar.gz");
29
- }
30
- expect(i).toBe(3);
31
- });
32
-
33
- test("propagates terminal 'failed' status to caller without throwing", async () => {
34
- const result = await pollJobUntilDone({
35
- poll: async () => ({
36
- jobId: "j2",
37
- type: "import",
38
- status: "failed",
39
- error: "bad bundle",
40
- }),
41
- intervalMs: 1,
42
- timeoutMs: 1_000,
43
- label: "test import",
44
- });
45
-
46
- expect(result.status).toBe("failed");
47
- if (result.status === "failed") {
48
- expect(result.error).toBe("bad bundle");
49
- }
50
- });
51
-
52
- test("throws with label when polling exceeds timeoutMs", async () => {
53
- let calls = 0;
54
- await expect(
55
- pollJobUntilDone({
56
- poll: async () => {
57
- calls += 1;
58
- return { jobId: "j3", type: "export", status: "processing" };
59
- },
60
- intervalMs: 20,
61
- timeoutMs: 10,
62
- label: "slow export",
63
- }),
64
- ).rejects.toThrow(/slow export/);
65
-
66
- // The loop does one poll before checking the deadline, so calls ≥ 1.
67
- expect(calls).toBeGreaterThanOrEqual(1);
68
- });
69
-
70
- test("uses defaults when intervalMs/timeoutMs are omitted (fast path)", async () => {
71
- // Fast path: first poll is already terminal so neither default matters.
72
- const result = await pollJobUntilDone({
73
- poll: async () => ({ jobId: "j4", type: "export", status: "complete" }),
74
- label: "defaults test",
75
- });
76
- expect(result.status).toBe("complete");
77
- });
78
-
79
- describe("transient-error retry", () => {
80
- let warnSpy: ReturnType<typeof spyOn>;
81
-
82
- beforeEach(() => {
83
- warnSpy = spyOn(console, "warn").mockImplementation(() => {});
84
- });
85
-
86
- afterEach(() => {
87
- warnSpy.mockRestore();
88
- });
89
-
90
- test("retries N-1 transient errors then returns terminal status", async () => {
91
- const maxTransientErrors = 3;
92
- let calls = 0;
93
- const result = await pollJobUntilDone({
94
- label: "flaky export",
95
- intervalMs: 1,
96
- timeoutMs: 1_000,
97
- maxTransientErrors,
98
- poll: async () => {
99
- calls += 1;
100
- if (calls < maxTransientErrors) {
101
- throw new Error(
102
- `Local job status check failed: 503 Service Unavailable`,
103
- );
104
- }
105
- return {
106
- jobId: "j5",
107
- type: "export",
108
- status: "complete",
109
- } as UnifiedJobStatus;
110
- },
111
- });
112
- expect(result.status).toBe("complete");
113
- expect(calls).toBe(maxTransientErrors);
114
- // One warning per retried transient error (first two attempts).
115
- expect(warnSpy).toHaveBeenCalledTimes(maxTransientErrors - 1);
116
- });
117
-
118
- test("propagates the last error once maxTransientErrors is exceeded", async () => {
119
- const maxTransientErrors = 2;
120
- let calls = 0;
121
- await expect(
122
- pollJobUntilDone({
123
- label: "always broken",
124
- intervalMs: 1,
125
- timeoutMs: 1_000,
126
- maxTransientErrors,
127
- poll: async () => {
128
- calls += 1;
129
- throw new Error(`Local job status check failed: 502 Bad Gateway`);
130
- },
131
- }),
132
- ).rejects.toThrow(/502 Bad Gateway/);
133
- // Helper makes `maxTransientErrors + 1` attempts before giving up: the
134
- // first attempt plus N retries, counted against the budget.
135
- expect(calls).toBe(maxTransientErrors + 1);
136
- });
137
-
138
- test("permanent 4xx errors (except 429) propagate immediately", async () => {
139
- let calls = 0;
140
- await expect(
141
- pollJobUntilDone({
142
- label: "auth broken",
143
- intervalMs: 1,
144
- timeoutMs: 1_000,
145
- maxTransientErrors: 5,
146
- poll: async () => {
147
- calls += 1;
148
- throw new Error(`Local job status check failed: 403 Forbidden`);
149
- },
150
- }),
151
- ).rejects.toThrow(/403 Forbidden/);
152
- expect(calls).toBe(1);
153
- expect(warnSpy).not.toHaveBeenCalled();
154
- });
155
-
156
- test("429 rate-limit is retried as transient", async () => {
157
- let calls = 0;
158
- const result = await pollJobUntilDone({
159
- label: "rate limited",
160
- intervalMs: 1,
161
- timeoutMs: 1_000,
162
- maxTransientErrors: 3,
163
- poll: async () => {
164
- calls += 1;
165
- if (calls === 1) {
166
- throw new Error(`Local job status check failed: 429 Too Many`);
167
- }
168
- return {
169
- jobId: "j6",
170
- type: "export",
171
- status: "complete",
172
- } as UnifiedJobStatus;
173
- },
174
- });
175
- expect(result.status).toBe("complete");
176
- expect(calls).toBe(2);
177
- });
178
-
179
- test("refreshOn401 is invoked on 401 and polling continues after refresh", async () => {
180
- let calls = 0;
181
- let refreshes = 0;
182
- const result = await pollJobUntilDone({
183
- label: "expiring auth",
184
- intervalMs: 1,
185
- timeoutMs: 1_000,
186
- maxTransientErrors: 0,
187
- refreshOn401: async () => {
188
- refreshes += 1;
189
- },
190
- poll: async () => {
191
- calls += 1;
192
- if (calls === 1) {
193
- throw new Error("Local job status check failed: 401 Unauthorized");
194
- }
195
- return {
196
- jobId: "j401",
197
- type: "export",
198
- status: "complete",
199
- } as UnifiedJobStatus;
200
- },
201
- });
202
- expect(result.status).toBe("complete");
203
- expect(refreshes).toBe(1);
204
- expect(calls).toBe(2);
205
- // The 401 branch logs its own distinct warning (not the generic
206
- // "polling failed:" one) so operators can distinguish an
207
- // auth refresh from a transient-error retry in the output.
208
- expect(warnSpy).toHaveBeenCalledWith(
209
- expect.stringContaining("refreshing auth"),
210
- );
211
- });
212
-
213
- test("propagates 401 once maxAuthRefreshes is exceeded", async () => {
214
- const maxAuthRefreshes = 2;
215
- let calls = 0;
216
- let refreshes = 0;
217
- await expect(
218
- pollJobUntilDone({
219
- label: "persistently unauthorized",
220
- intervalMs: 1,
221
- timeoutMs: 1_000,
222
- maxAuthRefreshes,
223
- refreshOn401: async () => {
224
- refreshes += 1;
225
- },
226
- poll: async () => {
227
- calls += 1;
228
- throw new Error("Local job status check failed: 401 Unauthorized");
229
- },
230
- }),
231
- ).rejects.toThrow(/401 Unauthorized/);
232
- // Helper allows `maxAuthRefreshes` successful refresh-and-retry cycles
233
- // (each counted against the budget after the poll fails), plus one
234
- // final attempt on the refreshed token that exceeds the budget.
235
- expect(calls).toBe(maxAuthRefreshes + 1);
236
- expect(refreshes).toBe(maxAuthRefreshes);
237
- });
238
-
239
- test("without refreshOn401, 401 still propagates as a permanent 4xx", async () => {
240
- let calls = 0;
241
- await expect(
242
- pollJobUntilDone({
243
- label: "no refresh hook",
244
- intervalMs: 1,
245
- timeoutMs: 1_000,
246
- poll: async () => {
247
- calls += 1;
248
- throw new Error("Local job status check failed: 401 Unauthorized");
249
- },
250
- }),
251
- ).rejects.toThrow(/401 Unauthorized/);
252
- expect(calls).toBe(1);
253
- });
254
-
255
- test("unclassified network-style errors are treated as transient", async () => {
256
- let calls = 0;
257
- const result = await pollJobUntilDone({
258
- label: "network blip",
259
- intervalMs: 1,
260
- timeoutMs: 1_000,
261
- maxTransientErrors: 3,
262
- poll: async () => {
263
- calls += 1;
264
- if (calls === 1) {
265
- throw new Error("fetch failed");
266
- }
267
- return {
268
- jobId: "j7",
269
- type: "export",
270
- status: "complete",
271
- } as UnifiedJobStatus;
272
- },
273
- });
274
- expect(result.status).toBe("complete");
275
- expect(calls).toBe(2);
276
- });
277
- });
278
- });