@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.
Files changed (89) hide show
  1. package/eslint.config.mjs +7 -0
  2. package/node_modules/@mycadet/local-mode/src/__tests__/environment.test.ts +1 -4
  3. package/node_modules/@mycadet/local-mode/src/config.ts +14 -19
  4. package/node_modules/@mycadet/local-mode/src/index.ts +5 -0
  5. package/node_modules/@mycadet/local-mode/src/local-signing-key.ts +73 -0
  6. package/node_modules/@mycadet/local-mode/src/lockfile.ts +18 -3
  7. package/package.json +4 -3
  8. package/src/__tests__/assistant-config.test.ts +30 -28
  9. package/src/__tests__/debug.test.ts +8 -8
  10. package/src/__tests__/default-main-screen-formatting.test.ts +5 -4
  11. package/src/__tests__/flags.test.ts +2 -2
  12. package/src/__tests__/hatch-provider-secrets.test.ts +3 -3
  13. package/src/__tests__/ingress-config.test.ts +2 -2
  14. package/src/__tests__/login-loopback.test.ts +1 -1
  15. package/src/__tests__/nginx-ingress.test.ts +68 -76
  16. package/src/__tests__/pair.test.ts +135 -121
  17. package/src/__tests__/platform-client.test.ts +2 -2
  18. package/src/__tests__/recover.test.ts +96 -3
  19. package/src/__tests__/retire.test.ts +5 -3
  20. package/src/__tests__/signing-key-continuity.test.ts +261 -0
  21. package/src/__tests__/sleep.test.ts +189 -6
  22. package/src/__tests__/tailscale-tunnel.test.ts +1 -1
  23. package/src/__tests__/uninstall.test.ts +171 -0
  24. package/src/__tests__/upgrade-local.test.ts +16 -3
  25. package/src/__tests__/use.test.ts +2 -2
  26. package/src/__tests__/wake-error.test.ts +76 -0
  27. package/src/__tests__/wake.test.ts +309 -36
  28. package/src/commands/backup.ts +6 -2
  29. package/src/commands/clean.ts +6 -2
  30. package/src/commands/client.ts +91 -41
  31. package/src/commands/confirm.ts +6 -2
  32. package/src/commands/connect.ts +6 -2
  33. package/src/commands/debug.ts +6 -2
  34. package/src/commands/devices.ts +6 -2
  35. package/src/commands/events.ts +6 -2
  36. package/src/commands/exec.ts +50 -12
  37. package/src/commands/flags.ts +6 -2
  38. package/src/commands/gateway.ts +6 -2
  39. package/src/commands/hatch.ts +6 -2
  40. package/src/commands/login.ts +26 -356
  41. package/src/commands/logs.ts +6 -2
  42. package/src/commands/message.ts +6 -2
  43. package/src/commands/pair.ts +6 -2
  44. package/src/commands/ps.ts +7 -2
  45. package/src/commands/recover.ts +21 -16
  46. package/src/commands/restore.ts +6 -2
  47. package/src/commands/retire.ts +6 -2
  48. package/src/commands/roadmap.ts +6 -2
  49. package/src/commands/rollback.ts +6 -2
  50. package/src/commands/setup.ts +76 -9
  51. package/src/commands/sleep.ts +131 -31
  52. package/src/commands/ssh.ts +6 -2
  53. package/src/commands/teleport.ts +6 -2
  54. package/src/commands/terminal.ts +6 -2
  55. package/src/commands/tunnel.ts +6 -2
  56. package/src/commands/uninstall.ts +228 -0
  57. package/src/commands/unpair.ts +6 -2
  58. package/src/commands/upgrade.ts +13 -8
  59. package/src/commands/use.ts +6 -2
  60. package/src/commands/voice.ts +6 -2
  61. package/src/commands/wake.ts +69 -37
  62. package/src/commands/workflows.ts +6 -2
  63. package/src/components/DefaultMainScreen.tsx +250 -36
  64. package/src/index.ts +23 -55
  65. package/src/lib/assistant-config.ts +27 -13
  66. package/src/lib/backup-ops.ts +3 -4
  67. package/src/lib/cli-error.ts +5 -0
  68. package/src/lib/client-identity.ts +8 -10
  69. package/src/lib/cloud-login.ts +422 -0
  70. package/src/lib/color.ts +16 -0
  71. package/src/lib/default-main-screen-formatting.ts +32 -8
  72. package/src/lib/environments/__tests__/paths.test.ts +9 -11
  73. package/src/lib/environments/paths.ts +2 -20
  74. package/src/lib/guardian-token.ts +72 -18
  75. package/src/lib/hatch-local.ts +52 -1
  76. package/src/lib/health-check.ts +24 -15
  77. package/src/lib/lifecycle-reporter.ts +9 -1
  78. package/src/lib/local-hatch-lock.ts +19 -27
  79. package/src/lib/local-runtime-client.ts +25 -14
  80. package/src/lib/local.ts +17 -43
  81. package/src/lib/ngrok.ts +48 -16
  82. package/src/lib/platform-client.ts +141 -72
  83. package/src/lib/retire-archive.ts +4 -4
  84. package/src/lib/retire-local.ts +2 -25
  85. package/src/lib/teleport-backup.ts +23 -9
  86. package/src/lib/terminal-client.ts +21 -9
  87. package/src/lib/tunnel-edge.ts +11 -13
  88. package/src/lib/upgrade-lifecycle.ts +16 -4
  89. package/src/lib/workos-pkce.ts +52 -21
@@ -0,0 +1,171 @@
1
+ import { afterAll, afterEach, describe, expect, spyOn, test } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { dirname, join } from "path";
5
+
6
+ import { uninstall } from "../commands/uninstall.js";
7
+ import {
8
+ getConfigDirs,
9
+ getInputHistoryPath,
10
+ getLogDir,
11
+ getStateDir,
12
+ } from "../lib/environments/paths.js";
13
+ import { getCurrentEnvironment } from "../lib/environments/resolve.js";
14
+ import type { AssistantEntry } from "../lib/assistant-config.js";
15
+
16
+ // The command reads its argv at call time.
17
+ const ORIGINAL_ARGV = process.argv;
18
+
19
+ // Every XDG location the command touches MUST be redirected: the homedir
20
+ // fallback would otherwise point at the real user's data dirs.
21
+ function redirectXdg(): string {
22
+ const root = mkdtempSync(join(tmpdir(), "cadet-uninstall-"));
23
+ process.env.XDG_DATA_HOME = join(root, "data");
24
+ process.env.XDG_CONFIG_HOME = join(root, "config");
25
+ process.env.XDG_STATE_HOME = join(root, "state");
26
+ process.env.CADET_ENVIRONMENT = "production";
27
+ process.env.CADET_LOCKFILE_DIR = join(root, "lockdir");
28
+ mkdirSync(process.env.CADET_LOCKFILE_DIR, { recursive: true });
29
+ return root;
30
+ }
31
+
32
+ const RESTORED_ENV_KEYS = [
33
+ "XDG_DATA_HOME",
34
+ "XDG_CONFIG_HOME",
35
+ "XDG_STATE_HOME",
36
+ "CADET_ENVIRONMENT",
37
+ "CADET_LOCKFILE_DIR",
38
+ ] as const;
39
+
40
+ function withRestoredEnv(run: () => Promise<void>): Promise<void> {
41
+ const before = Object.fromEntries(
42
+ RESTORED_ENV_KEYS.map((k) => [k, process.env[k]]),
43
+ );
44
+ return run().finally(() => {
45
+ for (const k of RESTORED_ENV_KEYS) {
46
+ if (before[k] === undefined) delete process.env[k];
47
+ else process.env[k] = before[k]!;
48
+ }
49
+ process.argv = ORIGINAL_ARGV;
50
+ });
51
+ }
52
+
53
+ function makeEntry(assistantId: string): AssistantEntry {
54
+ return {
55
+ assistantId,
56
+ runtimeUrl: "http://127.0.0.1:7801",
57
+ cloud: "local",
58
+ resources: {
59
+ instanceDir: join(tmpdir(), "cadet-uninstall-instance"),
60
+ daemonPort: 7801,
61
+ gatewayPort: 7831,
62
+ qdrantPort: 6334,
63
+ cesPort: 7790,
64
+ },
65
+ };
66
+ }
67
+
68
+ describe("cadet uninstall", () => {
69
+ const logSpy = spyOn(console, "log").mockImplementation(() => {});
70
+ const errorSpy = spyOn(console, "error").mockImplementation(() => {});
71
+ const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
72
+
73
+ afterEach(() => {
74
+ logSpy.mockClear();
75
+ errorSpy.mockClear();
76
+ warnSpy.mockClear();
77
+ });
78
+
79
+ afterAll(() => {
80
+ logSpy.mockRestore();
81
+ errorSpy.mockRestore();
82
+ warnSpy.mockRestore();
83
+ });
84
+
85
+ test("refuses while assistants remain — retire them first", async () => {
86
+ await withRestoredEnv(async () => {
87
+ const root = redirectXdg();
88
+ // A remaining assistant in the lockfile means there is data that
89
+ // should be retired (and archived) first.
90
+ writeFileSync(
91
+ join(process.env.CADET_LOCKFILE_DIR!, "lockfile.json"),
92
+ JSON.stringify({ assistants: [makeEntry("test-assistant")] }) + "\n",
93
+ );
94
+
95
+ const exitMock = spyOn(process, "exit");
96
+ exitMock.mockImplementation((code?: number | string | undefined): never => {
97
+ throw new Error(`process.exit:${code}`);
98
+ });
99
+ try {
100
+ await expect(uninstall()).rejects.toThrow("process.exit:1");
101
+ expect(errorSpy).toHaveBeenCalledWith(
102
+ expect.stringContaining("Retire them first"),
103
+ );
104
+ } finally {
105
+ exitMock.mockRestore();
106
+ }
107
+ rmSync(root, { recursive: true, force: true });
108
+ });
109
+ });
110
+
111
+ test("refuses non-interactively without --yes", async () => {
112
+ await withRestoredEnv(async () => {
113
+ const root = redirectXdg();
114
+
115
+ const exitMock = spyOn(process, "exit");
116
+ exitMock.mockImplementation((code?: number | string | undefined): never => {
117
+ throw new Error(`process.exit:${code}`);
118
+ });
119
+ try {
120
+ process.argv = ["bun", "cadet", "uninstall"];
121
+ await expect(uninstall()).rejects.toThrow("process.exit:1");
122
+ expect(errorSpy).toHaveBeenCalledWith(
123
+ expect.stringContaining("non-interactive"),
124
+ );
125
+ } finally {
126
+ exitMock.mockRestore();
127
+ }
128
+ rmSync(root, { recursive: true, force: true });
129
+ });
130
+ });
131
+
132
+ test("--yes deletes the environment's XDG locations and leaves the source checkout alone", async () => {
133
+ await withRestoredEnv(async () => {
134
+ const root = redirectXdg();
135
+ const env = getCurrentEnvironment();
136
+
137
+ // Pre-create every target the command should delete, using the SAME
138
+ // path helpers so the test stays honest about what they resolve to.
139
+ const stateDir = getStateDir(env);
140
+ mkdirSync(join(stateDir, "assistants", "some-assistant"), {
141
+ recursive: true,
142
+ });
143
+ mkdirSync(join(stateDir, "retired"), { recursive: true });
144
+ const configDir = getConfigDirs(env)[0]!;
145
+ mkdirSync(configDir, { recursive: true });
146
+ const logDir = getLogDir(env);
147
+ mkdirSync(logDir, { recursive: true });
148
+ const historyPath = getInputHistoryPath();
149
+ mkdirSync(dirname(historyPath), { recursive: true });
150
+ writeFileSync(historyPath, "history");
151
+
152
+ process.argv = ["bun", "cadet", "uninstall", "--yes"];
153
+ await uninstall();
154
+
155
+ expect(existsSync(stateDir)).toBe(false);
156
+ expect(existsSync(configDir)).toBe(false);
157
+ expect(existsSync(logDir)).toBe(false);
158
+ expect(existsSync(historyPath)).toBe(false);
159
+
160
+ // Running under bun from a git checkout: self-removal must print the
161
+ // note and delete NOTHING outside the XDG targets.
162
+ const note = logSpy.mock.calls
163
+ .map((args) => args.map(String).join(" "))
164
+ .join("\n");
165
+ expect(note).toContain("source checkout");
166
+ expect(existsSync(process.execPath)).toBe(true);
167
+
168
+ rmSync(root, { recursive: true, force: true });
169
+ });
170
+ });
171
+ });
@@ -202,7 +202,7 @@ function makeLocalEntry(): AssistantEntry {
202
202
  gatewayPort: 7830,
203
203
  qdrantPort: 6333,
204
204
  cesPort: 7822,
205
- signingKey: "existing-signing-key",
205
+ signingKey: "ab".repeat(32),
206
206
  },
207
207
  guardianBootstrapSecret: "existing-bootstrap-secret",
208
208
  };
@@ -307,6 +307,19 @@ afterAll(() => {
307
307
  });
308
308
 
309
309
  describe("cadet upgrade local", () => {
310
+ test("missing identity refuses without backup, installation, restart or repair", async () => {
311
+ const entry = findAssistantByNameMock("local-assistant")!;
312
+ delete entry.resources!.signingKey;
313
+ await expect(upgrade()).rejects.toThrow("process.exit(1)");
314
+ expect(createBackupMock).not.toHaveBeenCalled();
315
+ expect(ensureLocalRuntimeMock).not.toHaveBeenCalled();
316
+ expect(startLocalDaemonMock).not.toHaveBeenCalled();
317
+ expect(startGatewayMock).not.toHaveBeenCalled();
318
+ expect(generateLocalSigningKeyMock).not.toHaveBeenCalled();
319
+ expect(resetGuardianBootstrapMock).not.toHaveBeenCalled();
320
+ expect(saveAssistantEntryMock).not.toHaveBeenCalled();
321
+ });
322
+
310
323
  test("uses explicit target versions as-is", async () => {
311
324
  const resolveLatest = mock(async () => "v0.9.9");
312
325
 
@@ -394,7 +407,7 @@ describe("cadet upgrade local", () => {
394
407
  instanceDir: tempDir,
395
408
  runtimeVersion: cliPkg.version ? `v${cliPkg.version}` : "v0.8.12",
396
409
  }),
397
- { signingKey: "existing-signing-key" },
410
+ { signingKey: "ab".repeat(32) },
398
411
  );
399
412
  expect(startGatewayMock).toHaveBeenCalledWith(
400
413
  false,
@@ -403,7 +416,7 @@ describe("cadet upgrade local", () => {
403
416
  runtimeVersion: cliPkg.version ? `v${cliPkg.version}` : "v0.8.12",
404
417
  }),
405
418
  {
406
- signingKey: "existing-signing-key",
419
+ signingKey: "ab".repeat(32),
407
420
  bootstrapSecret: "existing-bootstrap-secret",
408
421
  },
409
422
  );
@@ -43,7 +43,7 @@ function writeLockfile(
43
43
  ): void {
44
44
  mkdirSync(testDir, { recursive: true });
45
45
  writeFileSync(
46
- join(testDir, ".cadet.lock.json"),
46
+ join(testDir, "lockfile.json"),
47
47
  JSON.stringify(
48
48
  {
49
49
  assistants: entries,
@@ -58,7 +58,7 @@ function writeLockfile(
58
58
  describe("cadet use", () => {
59
59
  beforeEach(() => {
60
60
  process.env.CADET_LOCKFILE_DIR = testDir;
61
- rmSync(join(testDir, ".cadet.lock.json"), { force: true });
61
+ rmSync(join(testDir, "lockfile.json"), { force: true });
62
62
  process.argv = ["bun", "cadet", "use"];
63
63
  process.exit = ((code?: number) => {
64
64
  throw new Error(`process.exit:${code}`);
@@ -0,0 +1,76 @@
1
+ import { expect, test } from "bun:test";
2
+ import { resolveLockfilePaths } from "@mycadet/local-mode";
3
+ import {
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ rmSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { dirname, join } from "node:path";
12
+
13
+ test("CLI wake emits actionable structured identity error and exits 1 without mutating state", async () => {
14
+ const root = mkdtempSync(join(tmpdir(), "wake-error-"));
15
+ try {
16
+ const env = {
17
+ ...process.env,
18
+ HOME: root,
19
+ USERPROFILE: root,
20
+ CADET_ENVIRONMENT: "local",
21
+ CADET_LOCKFILE_DIR: root,
22
+ XDG_CONFIG_HOME: join(root, "config"),
23
+ XDG_DATA_HOME: join(root, "data"),
24
+ };
25
+ const registry = resolveLockfilePaths(env)[0]!;
26
+ mkdirSync(dirname(registry), { recursive: true });
27
+ const original = JSON.stringify({
28
+ assistants: [
29
+ {
30
+ assistantId: "example",
31
+ cloud: "local",
32
+ runtimeUrl: "http://127.0.0.1:27830",
33
+ resources: {
34
+ instanceDir: join(root, "instance"),
35
+ daemonPort: 27821,
36
+ gatewayPort: 27830,
37
+ cesPort: 27822,
38
+ qdrantPort: 26333,
39
+ },
40
+ },
41
+ ],
42
+ });
43
+ writeFileSync(registry, original);
44
+ const child = Bun.spawn(
45
+ [
46
+ process.execPath,
47
+ join(import.meta.dir, "../index.ts"),
48
+ "wake",
49
+ "example",
50
+ ],
51
+ {
52
+ env,
53
+ stdout: "pipe",
54
+ stderr: "pipe",
55
+ windowsHide: true,
56
+ },
57
+ );
58
+ const [exit, stderr] = await Promise.all([
59
+ child.exited,
60
+ new Response(child.stderr).text(),
61
+ ]);
62
+ expect(exit).toBe(1);
63
+ const line = stderr
64
+ .split("\n")
65
+ .find((value) => value.startsWith("CLI_ERROR:"));
66
+ expect(line).toBeDefined();
67
+ const payload = JSON.parse(line!.slice("CLI_ERROR:".length));
68
+ expect(payload.error).toBe("AUTH_IDENTITY_UNAVAILABLE");
69
+ expect(payload.detail).toBe("missing");
70
+ expect(payload.message).toContain('cadet wake "example" --repair-guardian');
71
+ expect(payload.message).toContain("all clients to authenticate again");
72
+ expect(readFileSync(registry, "utf8")).toBe(original);
73
+ } finally {
74
+ rmSync(root, { recursive: true, force: true });
75
+ }
76
+ });