@mycadet/cli 0.12.6-dev.202610082233.54244ee → 0.12.7
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.
- package/eslint.config.mjs +7 -0
- package/node_modules/@mycadet/local-mode/src/__tests__/environment.test.ts +1 -4
- package/node_modules/@mycadet/local-mode/src/config.ts +14 -19
- package/node_modules/@mycadet/local-mode/src/index.ts +5 -0
- package/node_modules/@mycadet/local-mode/src/local-signing-key.ts +73 -0
- package/node_modules/@mycadet/local-mode/src/lockfile.ts +18 -3
- package/package.json +4 -3
- package/src/__tests__/assistant-config.test.ts +30 -28
- package/src/__tests__/debug.test.ts +8 -8
- package/src/__tests__/default-main-screen-formatting.test.ts +5 -4
- package/src/__tests__/flags.test.ts +2 -2
- package/src/__tests__/hatch-provider-secrets.test.ts +3 -3
- package/src/__tests__/ingress-config.test.ts +2 -2
- package/src/__tests__/login-loopback.test.ts +1 -1
- package/src/__tests__/nginx-ingress.test.ts +68 -76
- package/src/__tests__/pair.test.ts +135 -121
- package/src/__tests__/platform-client.test.ts +2 -2
- package/src/__tests__/recover.test.ts +96 -3
- package/src/__tests__/retire.test.ts +5 -3
- package/src/__tests__/signing-key-continuity.test.ts +261 -0
- package/src/__tests__/sleep.test.ts +189 -6
- package/src/__tests__/tailscale-tunnel.test.ts +1 -1
- package/src/__tests__/uninstall.test.ts +171 -0
- package/src/__tests__/upgrade-local.test.ts +16 -3
- package/src/__tests__/use.test.ts +2 -2
- package/src/__tests__/wake-error.test.ts +76 -0
- package/src/__tests__/wake.test.ts +309 -36
- package/src/commands/backup.ts +6 -2
- package/src/commands/clean.ts +6 -2
- package/src/commands/client.ts +91 -41
- package/src/commands/confirm.ts +6 -2
- package/src/commands/connect.ts +6 -2
- package/src/commands/debug.ts +6 -2
- package/src/commands/devices.ts +6 -2
- package/src/commands/events.ts +6 -2
- package/src/commands/exec.ts +50 -12
- package/src/commands/flags.ts +6 -2
- package/src/commands/gateway.ts +6 -2
- package/src/commands/hatch.ts +6 -2
- package/src/commands/login.ts +26 -356
- package/src/commands/logs.ts +6 -2
- package/src/commands/message.ts +6 -2
- package/src/commands/pair.ts +6 -2
- package/src/commands/ps.ts +7 -2
- package/src/commands/recover.ts +21 -16
- package/src/commands/restore.ts +6 -2
- package/src/commands/retire.ts +6 -2
- package/src/commands/roadmap.ts +6 -2
- package/src/commands/rollback.ts +6 -2
- package/src/commands/setup.ts +76 -9
- package/src/commands/sleep.ts +131 -31
- package/src/commands/ssh.ts +6 -2
- package/src/commands/teleport.ts +6 -2
- package/src/commands/terminal.ts +6 -2
- package/src/commands/tunnel.ts +6 -2
- package/src/commands/uninstall.ts +228 -0
- package/src/commands/unpair.ts +6 -2
- package/src/commands/upgrade.ts +13 -8
- package/src/commands/use.ts +6 -2
- package/src/commands/voice.ts +6 -2
- package/src/commands/wake.ts +69 -37
- package/src/commands/workflows.ts +6 -2
- package/src/components/DefaultMainScreen.tsx +250 -36
- package/src/index.ts +23 -55
- package/src/lib/assistant-config.ts +27 -13
- package/src/lib/backup-ops.ts +3 -4
- package/src/lib/cli-error.ts +5 -0
- package/src/lib/client-identity.ts +8 -10
- package/src/lib/cloud-login.ts +422 -0
- package/src/lib/color.ts +16 -0
- package/src/lib/default-main-screen-formatting.ts +32 -8
- package/src/lib/environments/__tests__/paths.test.ts +9 -11
- package/src/lib/environments/paths.ts +2 -20
- package/src/lib/guardian-token.ts +72 -18
- package/src/lib/hatch-local.ts +52 -1
- package/src/lib/health-check.ts +24 -15
- package/src/lib/lifecycle-reporter.ts +9 -1
- package/src/lib/local-hatch-lock.ts +19 -27
- package/src/lib/local-runtime-client.ts +25 -14
- package/src/lib/local.ts +17 -43
- package/src/lib/ngrok.ts +48 -16
- package/src/lib/platform-client.ts +141 -72
- package/src/lib/retire-archive.ts +4 -4
- package/src/lib/retire-local.ts +2 -25
- package/src/lib/teleport-backup.ts +23 -9
- package/src/lib/terminal-client.ts +21 -9
- package/src/lib/tunnel-edge.ts +11 -13
- package/src/lib/upgrade-lifecycle.ts +16 -4
- package/src/lib/workos-pkce.ts +52 -21
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
spyOn,
|
|
9
9
|
test,
|
|
10
10
|
} from "bun:test";
|
|
11
|
-
import {
|
|
11
|
+
import { resolveExistingLocalSigningKey } from "@mycadet/local-mode";
|
|
12
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
12
13
|
import { tmpdir } from "node:os";
|
|
13
14
|
import { join } from "node:path";
|
|
14
15
|
|
|
@@ -21,6 +22,7 @@ import * as local from "../lib/local.js";
|
|
|
21
22
|
import * as nginxIngress from "../lib/nginx-ingress.js";
|
|
22
23
|
import * as ngrok from "../lib/ngrok.js";
|
|
23
24
|
import * as processLib from "../lib/process.js";
|
|
25
|
+
import * as portAllocator from "../lib/port-allocator.js";
|
|
24
26
|
import type { AssistantEntry } from "../lib/assistant-config.js";
|
|
25
27
|
|
|
26
28
|
const realAssistantConfig = { ...assistantConfig };
|
|
@@ -29,6 +31,7 @@ const realGuardianToken = { ...guardianToken };
|
|
|
29
31
|
const realLocal = { ...local };
|
|
30
32
|
const realNgrok = { ...ngrok };
|
|
31
33
|
const realProcessLib = { ...processLib };
|
|
34
|
+
const realPortAllocator = { ...portAllocator };
|
|
32
35
|
|
|
33
36
|
const resolveTargetAssistantMock =
|
|
34
37
|
mock<typeof assistantConfig.resolveTargetAssistant>();
|
|
@@ -113,6 +116,15 @@ mock.module("../lib/process", () => ({
|
|
|
113
116
|
isProcessAlive: isProcessAliveMock,
|
|
114
117
|
}));
|
|
115
118
|
|
|
119
|
+
const findOpenPortMock = mock<typeof portAllocator.findOpenPort>(
|
|
120
|
+
async (port) => port,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
mock.module("../lib/port-allocator.js", () => ({
|
|
124
|
+
...realPortAllocator,
|
|
125
|
+
findOpenPort: findOpenPortMock,
|
|
126
|
+
}));
|
|
127
|
+
|
|
116
128
|
const generateLocalSigningKeyMock = mock<typeof local.generateLocalSigningKey>(
|
|
117
129
|
() => "generated-bootstrap-secret",
|
|
118
130
|
);
|
|
@@ -216,7 +228,7 @@ function makeLocalEntry(): AssistantEntry {
|
|
|
216
228
|
gatewayPort: 7830,
|
|
217
229
|
qdrantPort: 6333,
|
|
218
230
|
cesPort: 7822,
|
|
219
|
-
signingKey: "
|
|
231
|
+
signingKey: "ab".repeat(32),
|
|
220
232
|
},
|
|
221
233
|
};
|
|
222
234
|
}
|
|
@@ -250,6 +262,8 @@ beforeEach(() => {
|
|
|
250
262
|
);
|
|
251
263
|
stopProcessByPidFileMock.mockReset();
|
|
252
264
|
stopProcessByPidFileMock.mockResolvedValue(true);
|
|
265
|
+
findOpenPortMock.mockReset();
|
|
266
|
+
findOpenPortMock.mockImplementation(async (port) => port);
|
|
253
267
|
generateLocalSigningKeyMock.mockReset();
|
|
254
268
|
generateLocalSigningKeyMock.mockReturnValue("generated-bootstrap-secret");
|
|
255
269
|
isAssistantWatchModeAvailableMock.mockReset();
|
|
@@ -314,6 +328,7 @@ afterAll(() => {
|
|
|
314
328
|
mock.module("../lib/docker.js", () => realDocker);
|
|
315
329
|
mock.module("../lib/guardian-token.js", () => realGuardianToken);
|
|
316
330
|
mock.module("../lib/process", () => realProcessLib);
|
|
331
|
+
mock.module("../lib/port-allocator.js", () => realPortAllocator);
|
|
317
332
|
mock.module("../lib/local", () => realLocal);
|
|
318
333
|
mock.module("../lib/ngrok", () => realNgrok);
|
|
319
334
|
mock.module("../lib/ingress-config.js", () => realIngressConfig);
|
|
@@ -322,6 +337,286 @@ afterAll(() => {
|
|
|
322
337
|
});
|
|
323
338
|
|
|
324
339
|
describe("cadet wake", () => {
|
|
340
|
+
test("missing identity refuses before probing, stopping, starting, or repairing", async () => {
|
|
341
|
+
delete localEntry.resources!.signingKey;
|
|
342
|
+
await expect(wake()).rejects.toThrow(
|
|
343
|
+
/signing key missing.*cadet wake.*--repair-guardian/,
|
|
344
|
+
);
|
|
345
|
+
expect(resolveProcessStateMock).not.toHaveBeenCalled();
|
|
346
|
+
expect(stopProcessByPidFileMock).not.toHaveBeenCalled();
|
|
347
|
+
expect(startLocalDaemonMock).not.toHaveBeenCalled();
|
|
348
|
+
expect(startGatewayMock).not.toHaveBeenCalled();
|
|
349
|
+
expect(generateLocalSigningKeyMock).not.toHaveBeenCalled();
|
|
350
|
+
expect(resetGuardianBootstrapMock).not.toHaveBeenCalled();
|
|
351
|
+
expect(saveAssistantEntryMock).not.toHaveBeenCalled();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("recovers the existing deprecated key before process startup", async () => {
|
|
355
|
+
delete localEntry.resources!.signingKey;
|
|
356
|
+
localEntry.guardianBootstrapSecret = "existing-bootstrap";
|
|
357
|
+
const dir = join(tempDir, "workspace", "deprecated");
|
|
358
|
+
mkdirSync(dir, { recursive: true });
|
|
359
|
+
writeFileSync(
|
|
360
|
+
join(dir, "actor-token-signing-key"),
|
|
361
|
+
Buffer.from("cd".repeat(32), "hex"),
|
|
362
|
+
);
|
|
363
|
+
process.argv = ["bun", "cadet", "wake", "local-assistant"];
|
|
364
|
+
resolveProcessStateMock.mockImplementation(async () => {
|
|
365
|
+
expect(localEntry.resources!.signingKey).toBe("cd".repeat(32));
|
|
366
|
+
expect(saveAssistantEntryMock).toHaveBeenCalled();
|
|
367
|
+
return { status: "needs_start", pid: null };
|
|
368
|
+
});
|
|
369
|
+
isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
|
|
370
|
+
await wake();
|
|
371
|
+
expect(startLocalDaemonMock).toHaveBeenCalledWith(
|
|
372
|
+
false,
|
|
373
|
+
localEntry.resources,
|
|
374
|
+
{ foreground: false, signingKey: "cd".repeat(32) },
|
|
375
|
+
);
|
|
376
|
+
expect(startGatewayMock).toHaveBeenCalledWith(false, localEntry.resources, {
|
|
377
|
+
signingKey: "cd".repeat(32),
|
|
378
|
+
bootstrapSecret: "existing-bootstrap",
|
|
379
|
+
});
|
|
380
|
+
expect(generateLocalSigningKeyMock).not.toHaveBeenCalled();
|
|
381
|
+
expect(resetGuardianBootstrapMock).not.toHaveBeenCalled();
|
|
382
|
+
expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("a conflicting key refuses before any process is stopped", async () => {
|
|
386
|
+
delete localEntry.resources!.signingKey;
|
|
387
|
+
const legacy = join(tempDir, "workspace", "deprecated");
|
|
388
|
+
mkdirSync(legacy, { recursive: true });
|
|
389
|
+
writeFileSync(
|
|
390
|
+
join(legacy, "actor-token-signing-key"),
|
|
391
|
+
Buffer.from("ab".repeat(32), "hex"),
|
|
392
|
+
);
|
|
393
|
+
const dir = join(tempDir, "gateway-security");
|
|
394
|
+
mkdirSync(dir, { recursive: true });
|
|
395
|
+
writeFileSync(
|
|
396
|
+
join(dir, "actor-token-signing-key"),
|
|
397
|
+
Buffer.from("cd".repeat(32), "hex"),
|
|
398
|
+
);
|
|
399
|
+
await expect(wake()).rejects.toThrow("signing key conflict");
|
|
400
|
+
expect(resolveProcessStateMock).not.toHaveBeenCalled();
|
|
401
|
+
expect(stopProcessByPidFileMock).not.toHaveBeenCalled();
|
|
402
|
+
expect(saveAssistantEntryMock).not.toHaveBeenCalled();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
for (const failure of [
|
|
406
|
+
"missing",
|
|
407
|
+
"invalid",
|
|
408
|
+
"conflict",
|
|
409
|
+
"unreadable",
|
|
410
|
+
] as const) {
|
|
411
|
+
test(`explicit repair recovers ${failure} identity and the next ordinary wake reuses it`, async () => {
|
|
412
|
+
delete localEntry.resources!.signingKey;
|
|
413
|
+
localEntry.guardianBootstrapSecret = "existing-bootstrap";
|
|
414
|
+
const protectedDir = join(tempDir, "gateway-security");
|
|
415
|
+
const legacyDir = join(tempDir, "workspace", "deprecated");
|
|
416
|
+
if (failure === "invalid") localEntry.resources!.signingKey = "bad-key";
|
|
417
|
+
if (failure === "unreadable")
|
|
418
|
+
mkdirSync(join(protectedDir, "actor-token-signing-key"), {
|
|
419
|
+
recursive: true,
|
|
420
|
+
});
|
|
421
|
+
if (failure === "conflict") {
|
|
422
|
+
mkdirSync(protectedDir, { recursive: true });
|
|
423
|
+
mkdirSync(legacyDir, { recursive: true });
|
|
424
|
+
writeFileSync(
|
|
425
|
+
join(protectedDir, "actor-token-signing-key"),
|
|
426
|
+
Buffer.from("ab".repeat(32), "hex"),
|
|
427
|
+
);
|
|
428
|
+
writeFileSync(
|
|
429
|
+
join(legacyDir, "actor-token-signing-key"),
|
|
430
|
+
Buffer.from("cd".repeat(32), "hex"),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
process.argv = [
|
|
434
|
+
"bun",
|
|
435
|
+
"cadet",
|
|
436
|
+
"wake",
|
|
437
|
+
"local-assistant",
|
|
438
|
+
"--repair-guardian",
|
|
439
|
+
];
|
|
440
|
+
const replacement = "ef".repeat(32);
|
|
441
|
+
generateLocalSigningKeyMock.mockReturnValue(replacement);
|
|
442
|
+
resolveProcessStateMock.mockImplementation(async () => {
|
|
443
|
+
expect(saveAssistantEntryMock).toHaveBeenCalled();
|
|
444
|
+
expect(stopProcessByPidFileMock).toHaveBeenCalledTimes(2);
|
|
445
|
+
return { status: "needs_start", pid: null };
|
|
446
|
+
});
|
|
447
|
+
isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
|
|
448
|
+
await wake();
|
|
449
|
+
expect(startLocalDaemonMock).toHaveBeenCalledWith(
|
|
450
|
+
false,
|
|
451
|
+
localEntry.resources,
|
|
452
|
+
{ foreground: false, signingKey: replacement },
|
|
453
|
+
);
|
|
454
|
+
expect(startGatewayMock).toHaveBeenCalledWith(
|
|
455
|
+
false,
|
|
456
|
+
localEntry.resources,
|
|
457
|
+
{ signingKey: replacement, bootstrapSecret: "existing-bootstrap" },
|
|
458
|
+
);
|
|
459
|
+
expect(resetGuardianBootstrapMock).toHaveBeenCalledTimes(1);
|
|
460
|
+
expect(leaseGuardianTokenMock).toHaveBeenCalledTimes(1);
|
|
461
|
+
expect(warnSpy.mock.calls.flat().join(" ")).toContain(
|
|
462
|
+
"all clients must authenticate again",
|
|
463
|
+
);
|
|
464
|
+
expect(resolveExistingLocalSigningKey(localEntry.resources)).toBe(
|
|
465
|
+
replacement,
|
|
466
|
+
);
|
|
467
|
+
process.argv = ["bun", "cadet", "wake", "local-assistant"];
|
|
468
|
+
generateLocalSigningKeyMock.mockClear();
|
|
469
|
+
resetGuardianBootstrapMock.mockClear();
|
|
470
|
+
leaseGuardianTokenMock.mockClear();
|
|
471
|
+
await wake();
|
|
472
|
+
expect(generateLocalSigningKeyMock).not.toHaveBeenCalled();
|
|
473
|
+
expect(resetGuardianBootstrapMock).not.toHaveBeenCalled();
|
|
474
|
+
expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
test("explicit repair preserves a recoverable key without restarting healthy services", async () => {
|
|
479
|
+
localEntry.guardianBootstrapSecret = "existing-bootstrap";
|
|
480
|
+
process.argv = [
|
|
481
|
+
"bun",
|
|
482
|
+
"cadet",
|
|
483
|
+
"wake",
|
|
484
|
+
"local-assistant",
|
|
485
|
+
"--repair-guardian",
|
|
486
|
+
];
|
|
487
|
+
isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
|
|
488
|
+
await wake();
|
|
489
|
+
expect(findOpenPortMock).not.toHaveBeenCalled();
|
|
490
|
+
expect(generateLocalSigningKeyMock).not.toHaveBeenCalled();
|
|
491
|
+
expect(stopProcessByPidFileMock).not.toHaveBeenCalled();
|
|
492
|
+
expect(startLocalDaemonMock).not.toHaveBeenCalled();
|
|
493
|
+
expect(startGatewayMock).not.toHaveBeenCalled();
|
|
494
|
+
expect(leaseGuardianTokenMock).toHaveBeenCalledTimes(1);
|
|
495
|
+
expect(localEntry.resources!.signingKey).toBe("ab".repeat(32));
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("repair cannot replace the persisted key while a service refuses to stop", async () => {
|
|
499
|
+
delete localEntry.resources!.signingKey;
|
|
500
|
+
process.argv = [
|
|
501
|
+
"bun",
|
|
502
|
+
"cadet",
|
|
503
|
+
"wake",
|
|
504
|
+
"local-assistant",
|
|
505
|
+
"--repair-guardian",
|
|
506
|
+
];
|
|
507
|
+
stopProcessByPidFileMock.mockResolvedValue(false);
|
|
508
|
+
isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
|
|
509
|
+
await expect(wake()).rejects.toThrow("could not be stopped");
|
|
510
|
+
expect(saveAssistantEntryMock).not.toHaveBeenCalled();
|
|
511
|
+
expect(startLocalDaemonMock).not.toHaveBeenCalled();
|
|
512
|
+
expect(startGatewayMock).not.toHaveBeenCalled();
|
|
513
|
+
expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
for (const [portField, label] of [
|
|
517
|
+
["daemonPort", "assistant"],
|
|
518
|
+
["gatewayPort", "gateway"],
|
|
519
|
+
] as const) {
|
|
520
|
+
test(`repair refuses an untracked ${label} listener before saving a key and can retry after it stops`, async () => {
|
|
521
|
+
delete localEntry.resources!.signingKey;
|
|
522
|
+
localEntry.guardianBootstrapSecret = "existing-bootstrap";
|
|
523
|
+
process.argv = [
|
|
524
|
+
"bun",
|
|
525
|
+
"cadet",
|
|
526
|
+
"wake",
|
|
527
|
+
"local-assistant",
|
|
528
|
+
"--repair-guardian",
|
|
529
|
+
];
|
|
530
|
+
const orphan = Bun.serve({
|
|
531
|
+
hostname: "127.0.0.1",
|
|
532
|
+
port: 0,
|
|
533
|
+
fetch: () => new Response("not ready", { status: 503 }),
|
|
534
|
+
});
|
|
535
|
+
const port = orphan.port!;
|
|
536
|
+
localEntry.resources![portField] = port;
|
|
537
|
+
stopProcessByPidFileMock.mockImplementation(
|
|
538
|
+
realProcessLib.stopProcessByPidFile,
|
|
539
|
+
);
|
|
540
|
+
findOpenPortMock.mockImplementation((candidate, options) =>
|
|
541
|
+
candidate === port
|
|
542
|
+
? realPortAllocator.findOpenPort(candidate, options)
|
|
543
|
+
: Promise.resolve(candidate),
|
|
544
|
+
);
|
|
545
|
+
generateLocalSigningKeyMock.mockReturnValue("ef".repeat(32));
|
|
546
|
+
resolveProcessStateMock.mockResolvedValue({
|
|
547
|
+
status: "needs_start",
|
|
548
|
+
pid: null,
|
|
549
|
+
});
|
|
550
|
+
startLocalDaemonMock.mockImplementation(async () => {
|
|
551
|
+
expect(saveAssistantEntryMock).toHaveBeenCalled();
|
|
552
|
+
isProcessAliveMock.mockReturnValue({ alive: true, pid: 123 });
|
|
553
|
+
});
|
|
554
|
+
try {
|
|
555
|
+
await expect(wake()).rejects.toThrow(
|
|
556
|
+
`could not confirm that ${label} port ${port} is free`,
|
|
557
|
+
);
|
|
558
|
+
expect(localEntry.resources!.signingKey).toBeUndefined();
|
|
559
|
+
expect(saveAssistantEntryMock).not.toHaveBeenCalled();
|
|
560
|
+
expect(startLocalDaemonMock).not.toHaveBeenCalled();
|
|
561
|
+
expect(startGatewayMock).not.toHaveBeenCalled();
|
|
562
|
+
expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
|
|
563
|
+
expect(findOpenPortMock).toHaveBeenCalledWith(port, {
|
|
564
|
+
maxAttempts: 1,
|
|
565
|
+
host: "127.0.0.1",
|
|
566
|
+
});
|
|
567
|
+
expect((await fetch(`http://127.0.0.1:${port}/healthz`)).status).toBe(
|
|
568
|
+
503,
|
|
569
|
+
);
|
|
570
|
+
await orphan.stop(true);
|
|
571
|
+
await wake();
|
|
572
|
+
expect(localEntry.resources).toMatchObject({
|
|
573
|
+
signingKey: "ef".repeat(32),
|
|
574
|
+
});
|
|
575
|
+
expect(startLocalDaemonMock).toHaveBeenCalledTimes(1);
|
|
576
|
+
expect(startGatewayMock).toHaveBeenCalledTimes(1);
|
|
577
|
+
expect(leaseGuardianTokenMock).toHaveBeenCalledTimes(1);
|
|
578
|
+
} finally {
|
|
579
|
+
await orphan.stop(true);
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
test("repair refuses to save a replacement key when a port cannot be checked", async () => {
|
|
585
|
+
delete localEntry.resources!.signingKey;
|
|
586
|
+
process.argv = [
|
|
587
|
+
"bun",
|
|
588
|
+
"cadet",
|
|
589
|
+
"wake",
|
|
590
|
+
"local-assistant",
|
|
591
|
+
"--repair-guardian",
|
|
592
|
+
];
|
|
593
|
+
findOpenPortMock.mockRejectedValue(new Error("EPERM"));
|
|
594
|
+
await expect(wake()).rejects.toThrow("no replacement key was saved");
|
|
595
|
+
expect(localEntry.resources!.signingKey).toBeUndefined();
|
|
596
|
+
expect(saveAssistantEntryMock).not.toHaveBeenCalled();
|
|
597
|
+
expect(startLocalDaemonMock).not.toHaveBeenCalled();
|
|
598
|
+
expect(startGatewayMock).not.toHaveBeenCalled();
|
|
599
|
+
expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("repair persistence failure prevents launching with an ephemeral replacement key", async () => {
|
|
603
|
+
delete localEntry.resources!.signingKey;
|
|
604
|
+
process.argv = [
|
|
605
|
+
"bun",
|
|
606
|
+
"cadet",
|
|
607
|
+
"wake",
|
|
608
|
+
"local-assistant",
|
|
609
|
+
"--repair-guardian",
|
|
610
|
+
];
|
|
611
|
+
saveAssistantEntryMock.mockImplementation(() => {
|
|
612
|
+
throw new Error("registry write failed");
|
|
613
|
+
});
|
|
614
|
+
await expect(wake()).rejects.toThrow("registry write failed");
|
|
615
|
+
expect(startLocalDaemonMock).not.toHaveBeenCalled();
|
|
616
|
+
expect(startGatewayMock).not.toHaveBeenCalled();
|
|
617
|
+
expect(leaseGuardianTokenMock).not.toHaveBeenCalled();
|
|
618
|
+
});
|
|
619
|
+
|
|
325
620
|
test("restarts a running gateway without watch mode when backfilling the bootstrap secret", async () => {
|
|
326
621
|
await wake();
|
|
327
622
|
|
|
@@ -338,7 +633,7 @@ describe("cadet wake", () => {
|
|
|
338
633
|
false,
|
|
339
634
|
expect.objectContaining({ instanceDir: tempDir }),
|
|
340
635
|
{
|
|
341
|
-
signingKey: "
|
|
636
|
+
signingKey: "ab".repeat(32),
|
|
342
637
|
bootstrapSecret: "generated-bootstrap-secret",
|
|
343
638
|
},
|
|
344
639
|
);
|
|
@@ -642,9 +937,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
642
937
|
});
|
|
643
938
|
|
|
644
939
|
test("a docker wake restores the shared default-workspace edge", async () => {
|
|
645
|
-
const defaultWorkspace = mkdtempSync(
|
|
646
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
647
|
-
);
|
|
940
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
648
941
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
649
942
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
650
943
|
resolveTargetAssistantMock.mockReturnValue({
|
|
@@ -679,9 +972,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
679
972
|
});
|
|
680
973
|
|
|
681
974
|
test("a docker wake leaves an edge fronting another container alone", async () => {
|
|
682
|
-
const defaultWorkspace = mkdtempSync(
|
|
683
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
684
|
-
);
|
|
975
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
685
976
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
686
977
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
687
978
|
resolveTargetAssistantMock.mockReturnValue({
|
|
@@ -715,9 +1006,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
715
1006
|
});
|
|
716
1007
|
|
|
717
1008
|
test("a docker wake adopts an edge that already fronts it", async () => {
|
|
718
|
-
const defaultWorkspace = mkdtempSync(
|
|
719
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
720
|
-
);
|
|
1009
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
721
1010
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
722
1011
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
723
1012
|
resolveTargetAssistantMock.mockReturnValue({
|
|
@@ -764,9 +1053,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
764
1053
|
});
|
|
765
1054
|
|
|
766
1055
|
test("a failed docker edge restore does not claim webhooks still work", async () => {
|
|
767
|
-
const defaultWorkspace = mkdtempSync(
|
|
768
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
769
|
-
);
|
|
1056
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
770
1057
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
771
1058
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
772
1059
|
resolveTargetAssistantMock.mockReturnValue({
|
|
@@ -801,9 +1088,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
801
1088
|
});
|
|
802
1089
|
|
|
803
1090
|
test("a docker wake leaves a saved ingress owned by another container alone", async () => {
|
|
804
|
-
const defaultWorkspace = mkdtempSync(
|
|
805
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
806
|
-
);
|
|
1091
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
807
1092
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
808
1093
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
809
1094
|
// The edge is stopped, so its gatewayPort record is gone; only the saved
|
|
@@ -842,9 +1127,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
842
1127
|
});
|
|
843
1128
|
|
|
844
1129
|
test("a docker wake waits for the gateway before restoring the edge", async () => {
|
|
845
|
-
const defaultWorkspace = mkdtempSync(
|
|
846
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
847
|
-
);
|
|
1130
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
848
1131
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
849
1132
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
850
1133
|
resolveTargetAssistantMock.mockReturnValue({
|
|
@@ -872,9 +1155,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
872
1155
|
});
|
|
873
1156
|
|
|
874
1157
|
test("a docker wake skips the edge when the gateway never comes up", async () => {
|
|
875
|
-
const defaultWorkspace = mkdtempSync(
|
|
876
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
877
|
-
);
|
|
1158
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
878
1159
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
879
1160
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
880
1161
|
resolveTargetAssistantMock.mockReturnValue({
|
|
@@ -906,9 +1187,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
906
1187
|
});
|
|
907
1188
|
|
|
908
1189
|
test("a docker wake with no ingress config never waits for the gateway", async () => {
|
|
909
|
-
const defaultWorkspace = mkdtempSync(
|
|
910
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
911
|
-
);
|
|
1190
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
912
1191
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
913
1192
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
914
1193
|
resolveTargetAssistantMock.mockReturnValue({
|
|
@@ -934,9 +1213,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
934
1213
|
});
|
|
935
1214
|
|
|
936
1215
|
test("a docker wake restores a saved ingress that no entry claims", async () => {
|
|
937
|
-
const defaultWorkspace = mkdtempSync(
|
|
938
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
939
|
-
);
|
|
1216
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
940
1217
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
941
1218
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
942
1219
|
// Tunneled before the lockfile mirror existed: publicBaseUrl is saved but
|
|
@@ -976,9 +1253,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
976
1253
|
});
|
|
977
1254
|
|
|
978
1255
|
test("a docker wake respects an ingress claim from another environment", async () => {
|
|
979
|
-
const defaultWorkspace = mkdtempSync(
|
|
980
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
981
|
-
);
|
|
1256
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
982
1257
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
983
1258
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
984
1259
|
resolveTargetAssistantMock.mockReturnValue({
|
|
@@ -1013,9 +1288,7 @@ describe("cadet wake — tunnel edge restore", () => {
|
|
|
1013
1288
|
});
|
|
1014
1289
|
|
|
1015
1290
|
test("a docker wake without an ingress config leaves the edge alone", async () => {
|
|
1016
|
-
const defaultWorkspace = mkdtempSync(
|
|
1017
|
-
join(tmpdir(), "cadet-wake-default-"),
|
|
1018
|
-
);
|
|
1291
|
+
const defaultWorkspace = mkdtempSync(join(tmpdir(), "cadet-wake-default-"));
|
|
1019
1292
|
const originalWorkspaceDir = process.env.CADET_WORKSPACE_DIR;
|
|
1020
1293
|
process.env.CADET_WORKSPACE_DIR = defaultWorkspace;
|
|
1021
1294
|
resolveTargetAssistantMock.mockReturnValue({
|
package/src/commands/backup.ts
CHANGED
|
@@ -23,7 +23,8 @@ import { loopbackSafeFetch } from "../lib/loopback-fetch.js";
|
|
|
23
23
|
// --export-timeout.
|
|
24
24
|
const DEFAULT_EXPORT_TIMEOUT_MS = 300_000;
|
|
25
25
|
|
|
26
|
-
export
|
|
26
|
+
export const backup = Object.assign(
|
|
27
|
+
async function backup(): Promise<void> {
|
|
27
28
|
const args = process.argv.slice(3);
|
|
28
29
|
|
|
29
30
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -224,7 +225,10 @@ export async function backup(): Promise<void> {
|
|
|
224
225
|
if (manifestSha) {
|
|
225
226
|
console.log(`Manifest SHA-256: ${manifestSha}`);
|
|
226
227
|
}
|
|
227
|
-
}
|
|
228
|
+
},
|
|
229
|
+
{ description: "Export a backup of a running assistant" },
|
|
230
|
+
);
|
|
231
|
+
|
|
228
232
|
|
|
229
233
|
// ---------------------------------------------------------------------------
|
|
230
234
|
// Platform-managed (cloud="cadet") backup over GCS.
|
package/src/commands/clean.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { detectOrphanedProcesses } from "../lib/orphan-detection";
|
|
2
2
|
import { stopProcess } from "../lib/process";
|
|
3
3
|
|
|
4
|
-
export
|
|
4
|
+
export const clean = Object.assign(
|
|
5
|
+
async function clean(): Promise<void> {
|
|
5
6
|
const args = process.argv.slice(3);
|
|
6
7
|
if (args.includes("--help") || args.includes("-h")) {
|
|
7
8
|
console.log("Usage: cadet clean");
|
|
@@ -36,4 +37,7 @@ export async function clean(): Promise<void> {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
console.log(`\nCleaned up ${killed} process${killed === 1 ? "" : "es"}.`);
|
|
39
|
-
}
|
|
40
|
+
},
|
|
41
|
+
{ description: "Kill orphaned cadet processes" },
|
|
42
|
+
);
|
|
43
|
+
|