@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
package/eslint.config.mjs CHANGED
@@ -10,6 +10,13 @@ const eslintConfig = defineConfig([
10
10
  "error",
11
11
  { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
12
12
  ],
13
+ // Ban `as` casts (the platform repo's rule) at warning level: the
14
+ // lint script pins --max-warnings to the current count and each PR
15
+ // chips away at it — prefer zod schemas or `in`-narrowing.
16
+ "@typescript-eslint/consistent-type-assertions": [
17
+ "warn",
18
+ { assertionStyle: "never" },
19
+ ],
13
20
  },
14
21
  },
15
22
  ]);
@@ -117,8 +117,7 @@ describe("path resolvers honor the persisted default", () => {
117
117
  CADET_ENVIRONMENT: "production",
118
118
  };
119
119
  expect(resolveLockfilePaths(env)).toEqual([
120
- path.join(os.homedir(), ".cadet.lock.json"),
121
- path.join(os.homedir(), ".cadet.lockfile.json"),
120
+ path.join(configHome, "cadet", "lockfile.json"),
122
121
  ]);
123
122
  });
124
123
  });
@@ -162,8 +161,6 @@ describe("Windows path resolution", () => {
162
161
  const productionEnv = { CADET_ENVIRONMENT: "production" };
163
162
  expect(resolveLockfilePaths(productionEnv, options)).toEqual([
164
163
  "C:\\Users\\Example\\AppData\\Roaming\\cadet\\lockfile.json",
165
- "C:\\Users\\Example\\.cadet.lock.json",
166
- "C:\\Users\\Example\\.cadet.lockfile.json",
167
164
  ]);
168
165
  expect(defaultEnvironmentFilePaths(env, options)).toEqual([
169
166
  "C:\\Users\\Example\\AppData\\Roaming\\cadet\\environment",
@@ -52,31 +52,26 @@ export function resolveLockfilePaths(
52
52
  if ((options.platform ?? process.platform) === "win32") {
53
53
  const canonicalDir = lockfileDir ?? resolveConfigDir(env, options);
54
54
  const canonical = joinLocalPath(options, canonicalDir, "lockfile.json");
55
- const legacyDir = lockfileDir ?? options.homeDir ?? os.homedir();
56
- const legacy =
57
- cadetEnv === PRODUCTION_ENVIRONMENT_NAME
58
- ? [".cadet.lock.json", ".cadet.lockfile.json"].map((name) =>
59
- joinLocalPath(options, legacyDir, name),
60
- )
61
- : [
62
- joinLocalPath(
63
- options,
64
- lockfileDir ?? resolveConfigDirPaths(env, options).at(-1)!,
65
- "lockfile.json",
66
- ),
67
- ];
55
+ if (cadetEnv === PRODUCTION_ENVIRONMENT_NAME) {
56
+ return [canonical];
57
+ }
68
58
  return [
69
59
  canonical,
70
- ...legacy.filter((candidate) => candidate !== canonical),
60
+ joinLocalPath(
61
+ options,
62
+ lockfileDir ?? resolveConfigDirPaths(env, options).at(-1)!,
63
+ "lockfile.json",
64
+ ),
71
65
  ];
72
66
  }
73
67
 
74
68
  if (cadetEnv === PRODUCTION_ENVIRONMENT_NAME) {
75
- const dir = lockfileDir ?? options.homeDir ?? os.homedir();
76
- return [
77
- joinLocalPath(options, dir, ".cadet.lock.json"),
78
- joinLocalPath(options, dir, ".cadet.lockfile.json"),
79
- ];
69
+ // Canonical: the XDG config dir, where every other environment keeps
70
+ // its lockfile. Installs that predate the move are handled by the
71
+ // vellum → cadet migration, which moves the home-level file into
72
+ // place — no legacy candidates here.
73
+ const dir = lockfileDir ?? resolveConfigDir(env, options);
74
+ return [joinLocalPath(options, dir, "lockfile.json")];
80
75
  }
81
76
 
82
77
  const dir = lockfileDir ?? resolveConfigDir(env, options);
@@ -140,3 +140,8 @@ export type {
140
140
  PairedForwardAuthorizationResult,
141
141
  PairedGuardianTokenProvider,
142
142
  } from "./gateway-proxy";
143
+
144
+ export {
145
+ resolveExistingLocalSigningKey,
146
+ SigningKeyContinuityError,
147
+ } from "./local-signing-key";
@@ -0,0 +1,73 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { z } from "zod";
5
+
6
+ const KeySchema = z.string().regex(/^[0-9a-f]{64}$/i);
7
+ const ResourcesSchema = z.object({
8
+ instanceDir: z.string().min(1),
9
+ signingKey: KeySchema.optional(),
10
+ });
11
+
12
+ export class SigningKeyContinuityError extends Error {
13
+ constructor(
14
+ public readonly code: "missing" | "invalid" | "unreadable" | "conflict",
15
+ ) {
16
+ super(
17
+ `Cannot resume authentication: signing key ${code}. Preserve the existing authentication state and restore its original key; no replacement key was generated.`,
18
+ );
19
+ this.name = "SigningKeyContinuityError";
20
+ }
21
+ }
22
+
23
+ /** Resolve existing identity material. This operation never creates or rotates a key. */
24
+ export function resolveExistingLocalSigningKey(
25
+ resources: unknown,
26
+ stateRoot?: string,
27
+ ): string {
28
+ const parsed = ResourcesSchema.safeParse(resources);
29
+ if (!parsed.success) {
30
+ throw new SigningKeyContinuityError("invalid");
31
+ }
32
+ const { instanceDir, signingKey } = parsed.data;
33
+ // The host registry is authoritative after provisioning or migration.
34
+ // Legacy files can be stale after an explicit identity repair.
35
+ if (signingKey) {
36
+ return signingKey.toLowerCase();
37
+ }
38
+ const candidates: Buffer[] = [];
39
+ const root = stateRoot ?? instanceDir;
40
+ // Read-only migration sources, scoped to this instance rather than the user's home.
41
+ const legacyPaths = [
42
+ join(root, "gateway-security", "actor-token-signing-key"),
43
+ join(root, "workspace", "deprecated", "actor-token-signing-key"),
44
+ ];
45
+ for (const keyPath of legacyPaths) {
46
+ let raw: Buffer;
47
+ try {
48
+ raw = readFileSync(keyPath);
49
+ } catch (error) {
50
+ if (
51
+ error &&
52
+ typeof error === "object" &&
53
+ "code" in error &&
54
+ error.code === "ENOENT"
55
+ ) {
56
+ continue;
57
+ }
58
+ throw new SigningKeyContinuityError("unreadable");
59
+ }
60
+ if (raw.length !== 32) {
61
+ throw new SigningKeyContinuityError("invalid");
62
+ }
63
+ candidates.push(raw);
64
+ }
65
+ const key = candidates[0];
66
+ if (!key) {
67
+ throw new SigningKeyContinuityError("missing");
68
+ }
69
+ if (candidates.some((candidate) => !timingSafeEqual(candidate, key))) {
70
+ throw new SigningKeyContinuityError("conflict");
71
+ }
72
+ return key.toString("hex");
73
+ }
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
 
4
4
  import {
5
+ LockfileAssistantSchema,
5
6
  parseLockfile,
6
7
  resolveCloud,
7
8
  type Lockfile,
@@ -207,7 +208,8 @@ export function stampLockfileAssistantOnboardedIfPresent(
207
208
  lockfilePaths,
208
209
  assistantId,
209
210
  { onboardedAt },
210
- (entry) => typeof entry.onboardedAt === "string" && entry.onboardedAt !== "",
211
+ (entry) =>
212
+ typeof entry.onboardedAt === "string" && entry.onboardedAt !== "",
211
213
  );
212
214
  }
213
215
 
@@ -289,6 +291,10 @@ export function upsertLockfileAssistant(
289
291
  return locked.ok ? locked.value : lockFailure(locked.error);
290
292
  }
291
293
 
294
+ const RendererAssistantUpdateSchema = LockfileAssistantSchema.omit({
295
+ resources: true,
296
+ });
297
+
292
298
  const PAIRED_LOCKFILE_WRITE_ERROR =
293
299
  "Paired assistant identity can only be changed through the connect flow";
294
300
 
@@ -296,6 +302,7 @@ const PAIRED_LOCKFILE_WRITE_ERROR =
296
302
  * Apply a renderer-originated lockfile upsert without allowing it to create,
297
303
  * retarget, or reclassify a paired assistant. Existing paired entries may
298
304
  * still update non-security fields and become the active assistant.
305
+ * Resources and secrets are host-owned and never accepted from a renderer.
299
306
  */
300
307
  export function upsertRendererLockfileAssistant(
301
308
  lockfilePaths: string[],
@@ -309,7 +316,11 @@ export function upsertRendererLockfileAssistant(
309
316
  // Lock spans the paired-guard read and the upsert (which reenters) so the
310
317
  // guard cannot be judged against a snapshot another writer replaces.
311
318
  const locked = withLockfileLock(lockfilePaths, (): WriteResult => {
312
- const lockfile = readRawLockfile(lockfilePaths);
319
+ const read = readRawLockfileStrict(lockfilePaths);
320
+ if (!read.ok) {
321
+ return { ok: false, status: 409, error: read.error };
322
+ }
323
+ const lockfile = read.lockfile;
313
324
  const assistants = Array.isArray(lockfile.assistants)
314
325
  ? (lockfile.assistants as Array<Record<string, unknown>>)
315
326
  : [];
@@ -335,7 +346,11 @@ export function upsertRendererLockfileAssistant(
335
346
  return { ok: false, status: 403, error: PAIRED_LOCKFILE_WRITE_ERROR };
336
347
  }
337
348
 
338
- return upsertLockfileAssistant(lockfilePaths, assistant, activeAssistant);
349
+ const update = RendererAssistantUpdateSchema.safeParse(assistant);
350
+ if (!update.success) {
351
+ return { ok: false, status: 400, error: "Invalid assistant metadata" };
352
+ }
353
+ return upsertLockfileAssistant(lockfilePaths, update.data, activeAssistant);
339
354
  });
340
355
  return locked.ok ? locked.value : lockFailure(locked.error);
341
356
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mycadet/cli",
3
- "version": "0.12.6-dev.202610082233.54244ee",
3
+ "version": "0.12.7",
4
4
  "description": "CLI tools for mycadet",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,7 +20,7 @@
20
20
  "scripts": {
21
21
  "format": "prettier --write .",
22
22
  "format:check": "prettier --check .",
23
- "lint": "eslint",
23
+ "lint": "eslint --max-warnings 519",
24
24
  "lint:unused": "knip --include files,dependencies,unlisted",
25
25
  "compile:check": "bun build --compile src/index.ts --outfile /dev/null",
26
26
  "test": "bun run scripts/run-tests.ts",
@@ -40,7 +40,8 @@
40
40
  "nanoid": "5.1.7",
41
41
  "qrcode-terminal": "0.12.0",
42
42
  "react": "19.2.6",
43
- "react-devtools-core": "7.0.1"
43
+ "react-devtools-core": "7.0.1",
44
+ "zod": "4.3.6"
44
45
  },
45
46
  "bundledDependencies": [
46
47
  "@mycadet/assistant-api",
@@ -46,7 +46,7 @@ afterAll(() => {
46
46
 
47
47
  function writeLockfile(data: unknown): void {
48
48
  writeFileSync(
49
- join(testDir, ".cadet.lock.json"),
49
+ join(testDir, "lockfile.json"),
50
50
  JSON.stringify(data, null, 2),
51
51
  );
52
52
  }
@@ -64,14 +64,9 @@ const makeEntry = (
64
64
 
65
65
  describe("assistant-config", () => {
66
66
  beforeEach(() => {
67
- // Reset lockfile between tests
67
+ // Reset the canonical lockfile between tests
68
68
  try {
69
- rmSync(join(testDir, ".cadet.lock.json"));
70
- } catch {
71
- // file may not exist
72
- }
73
- try {
74
- rmSync(join(testDir, ".cadet.lockfile.json"));
69
+ rmSync(join(testDir, "lockfile.json"));
75
70
  } catch {
76
71
  // file may not exist
77
72
  }
@@ -82,7 +77,17 @@ describe("assistant-config", () => {
82
77
  });
83
78
 
84
79
  test("loadAllAssistants returns empty array for malformed lockfile", () => {
85
- writeFileSync(join(testDir, ".cadet.lock.json"), "not json");
80
+ writeFileSync(join(testDir, "lockfile.json"), "not json");
81
+ expect(loadAllAssistants()).toEqual([]);
82
+ });
83
+
84
+ test("legacy home-level lockfile names are no longer read (the migration moves them into place)", () => {
85
+ // A pre-move home-level file alone must NOT be picked up — the
86
+ // migration tooling is what moves it to the canonical config-dir path.
87
+ writeFileSync(
88
+ join(testDir, ".cadet.lock.json"),
89
+ JSON.stringify({ assistants: [makeEntry("legacy-1")] }, null, 2),
90
+ );
86
91
  expect(loadAllAssistants()).toEqual([]);
87
92
  });
88
93
 
@@ -103,11 +108,11 @@ describe("assistant-config", () => {
103
108
  saveAssistantEntry(makeEntry("locked-1"));
104
109
  expect(loadAllAssistants()).toHaveLength(1);
105
110
  // The shared `${lockfilePath}.lock` key hosts contend on is released.
106
- expect(existsSync(join(testDir, ".cadet.lock.json.lock"))).toBe(false);
111
+ expect(existsSync(join(testDir, "lockfile.json.lock"))).toBe(false);
107
112
  });
108
113
 
109
114
  test("saveAssistantEntry breaks a stale lock and still lands the write", () => {
110
- const lockDir = join(testDir, ".cadet.lock.json.lock");
115
+ const lockDir = join(testDir, "lockfile.json.lock");
111
116
  mkdirSync(lockDir);
112
117
  const past = new Date(Date.now() - 60_000);
113
118
  utimesSync(lockDir, past, past);
@@ -118,7 +123,7 @@ describe("assistant-config", () => {
118
123
  });
119
124
 
120
125
  test("saveAssistantEntry fails fast without writing when another holder is fresh", () => {
121
- const lockDir = join(testDir, ".cadet.lock.json.lock");
126
+ const lockDir = join(testDir, "lockfile.json.lock");
122
127
  mkdirSync(lockDir);
123
128
  try {
124
129
  const start = Date.now();
@@ -615,9 +620,11 @@ describe("legacy migration via loadAllAssistants", () => {
615
620
  );
616
621
  expect(all[0].resources!.gatewayPort).toBe(7830);
617
622
 
618
- // AND the lockfile on disk should reflect the migration
623
+ // AND the lockfile on disk should reflect the migration — persisted to
624
+ // the canonical config-dir path (the legacy seed file is left in place
625
+ // as a read-only fallback).
619
626
  const rawDisk = JSON.parse(
620
- readFileSync(join(testDir, ".cadet.lock.json"), "utf-8"),
627
+ readFileSync(join(testDir, "lockfile.json"), "utf-8"),
621
628
  );
622
629
  const diskEntry = rawDisk.assistants[0];
623
630
  expect(diskEntry.baseDataDir).toBeUndefined();
@@ -715,21 +722,17 @@ describe("env-scoped lockfile and migration", () => {
715
722
  }
716
723
  });
717
724
 
718
- test("production lockfile path is unchanged — uses .cadet.lock.json", () => {
725
+ test("production writes land at the config-dir lockfile.json, never the legacy home-level name", () => {
719
726
  // With CADET_ENVIRONMENT unset, the env package resolves production,
720
- // whose canonical lockfile filename is `.cadet.lock.json`. This test
721
- // uses the existing CADET_LOCKFILE_DIR=testDir override to route the
722
- // lockfile to a scratch directory but verifies the FILENAME matches
723
- // the production convention (not the non-prod "lockfile.json").
727
+ // whose canonical lockfile is the config-dir `lockfile.json` — the same
728
+ // convention every environment uses. The home-level `.cadet.lock.json`
729
+ // name is a read-only fallback and is never (re)created by writes.
724
730
  const prevEnv = process.env.CADET_ENVIRONMENT;
725
731
  delete process.env.CADET_ENVIRONMENT;
726
732
  try {
727
733
  // Clear any existing lockfile from earlier tests
728
- try {
729
- rmSync(join(testDir, ".cadet.lock.json"));
730
- } catch {
731
- // ignore
732
- }
734
+ rmSync(join(testDir, "lockfile.json"), { force: true });
735
+ rmSync(join(testDir, ".cadet.lock.json"), { force: true });
733
736
 
734
737
  saveAssistantEntry({
735
738
  assistantId: "prod-bot",
@@ -737,12 +740,10 @@ describe("env-scoped lockfile and migration", () => {
737
740
  cloud: "local",
738
741
  });
739
742
 
740
- // The file should land at testDir/.cadet.lock.json — the production
741
- // filename — rather than testDir/lockfile.json.
742
- const prodPath = join(testDir, ".cadet.lock.json");
743
- const raw = JSON.parse(readFileSync(prodPath, "utf-8"));
743
+ const raw = JSON.parse(readFileSync(join(testDir, "lockfile.json"), "utf-8"));
744
744
  expect(raw.assistants).toHaveLength(1);
745
745
  expect(raw.assistants[0].assistantId).toBe("prod-bot");
746
+ expect(existsSync(join(testDir, ".cadet.lock.json"))).toBe(false);
746
747
  } finally {
747
748
  if (prevEnv !== undefined) {
748
749
  process.env.CADET_ENVIRONMENT = prevEnv;
@@ -765,6 +766,7 @@ describe("cross-environment hint on resolution failure", () => {
765
766
  // copy is deterministic. The current-env lockfile is emptied so resolution
766
767
  // genuinely finds nothing.
767
768
  process.env.CADET_ENVIRONMENT = "production";
769
+ rmSync(join(testDir, "lockfile.json"), { force: true });
768
770
  rmSync(join(testDir, ".cadet.lock.json"), { force: true });
769
771
  rmSync(join(testDir, ".cadet.lockfile.json"), { force: true });
770
772
  });
@@ -64,7 +64,7 @@ const leaseGuardianTokenSpy = spyOn(
64
64
  accessTokenExpiresAt: new Date(Date.now() + 10 * 60_000).toISOString(),
65
65
  } as unknown as Awaited<ReturnType<typeof guardianToken.leaseGuardianToken>>);
66
66
 
67
- const { debugBundle } = await import("../commands/debug.js");
67
+ const { debug } = await import("../commands/debug.js");
68
68
 
69
69
  const LOCAL_ENTRY = {
70
70
  name: "mine",
@@ -132,7 +132,7 @@ afterAll(() => {
132
132
 
133
133
  describe("cadet debug", () => {
134
134
  test("mints the URL on the platform, exports with the debug profile, and waits", async () => {
135
- await debugBundle();
135
+ await debug();
136
136
 
137
137
  expect(fetchCalls).toHaveLength(1);
138
138
  expect(fetchCalls[0].url).toBe(
@@ -161,7 +161,7 @@ describe("cadet debug", () => {
161
161
 
162
162
  test("passes an unquoted multi-word display name through to the shared resolver", async () => {
163
163
  process.argv = ["bun", "cadet", "debug", "Support", "Bot"];
164
- await debugBundle();
164
+ await debug();
165
165
  expect(resolveTargetMock).toHaveBeenCalledWith("Support Bot");
166
166
  });
167
167
 
@@ -174,7 +174,7 @@ describe("cadet debug", () => {
174
174
  errors.push(String(msg));
175
175
  });
176
176
  try {
177
- await expect(debugBundle()).rejects.toThrow("process.exit:1");
177
+ await expect(debug()).rejects.toThrow("process.exit:1");
178
178
  } finally {
179
179
  errorSpy.mockRestore();
180
180
  }
@@ -201,7 +201,7 @@ describe("cadet debug", () => {
201
201
  >;
202
202
  });
203
203
 
204
- await debugBundle();
204
+ await debug();
205
205
 
206
206
  expect(leaseGuardianTokenSpy).toHaveBeenCalledTimes(1);
207
207
  expect(polls).toBe(2);
@@ -214,7 +214,7 @@ describe("cadet debug", () => {
214
214
  errors.push(String(msg));
215
215
  });
216
216
  try {
217
- await expect(debugBundle()).rejects.toThrow("process.exit:1");
217
+ await expect(debug()).rejects.toThrow("process.exit:1");
218
218
  } finally {
219
219
  errorSpy.mockRestore();
220
220
  }
@@ -229,7 +229,7 @@ describe("cadet debug", () => {
229
229
  } as unknown as assistantConfig.AssistantEntry);
230
230
  const errorSpy = spyOn(console, "error").mockImplementation(() => {});
231
231
  try {
232
- await expect(debugBundle()).rejects.toThrow("process.exit:1");
232
+ await expect(debug()).rejects.toThrow("process.exit:1");
233
233
  } finally {
234
234
  errorSpy.mockRestore();
235
235
  }
@@ -246,7 +246,7 @@ describe("cadet debug", () => {
246
246
  errors.push(String(msg));
247
247
  });
248
248
  try {
249
- await expect(debugBundle()).rejects.toThrow("process.exit:1");
249
+ await expect(debug()).rejects.toThrow("process.exit:1");
250
250
  } finally {
251
251
  errorSpy.mockRestore();
252
252
  }
@@ -4,8 +4,8 @@ import {
4
4
  calculateHeaderHeight,
5
5
  estimateItemHeight,
6
6
  formatConfirmationPreview,
7
- formatHeaderEyebrow,
8
7
  formatHeaderTitle,
8
+ HELP_COMMANDS,
9
9
  formatTimestamp,
10
10
  formatToolCallPreview,
11
11
  stripAnsi,
@@ -125,8 +125,7 @@ describe("truncateValue", () => {
125
125
 
126
126
  describe("header formatting", () => {
127
127
  test("uses the default title and eyebrow", () => {
128
- expect(formatHeaderEyebrow()).toBe(" Assistant");
129
- expect(formatHeaderTitle()).toBe(" Meet your Assistant!");
128
+ expect(formatHeaderTitle()).toBe(" Meet your Cadet!");
130
129
  });
131
130
 
132
131
  test("normalizes title whitespace", () => {
@@ -169,7 +168,9 @@ describe("estimateItemHeight", () => {
169
168
  });
170
169
 
171
170
  test("estimates non-message feed items", () => {
172
- expect(estimateItemHeight({ type: "help" }, 80)).toBe(5);
171
+ expect(estimateItemHeight({ type: "help" }, 80)).toBe(
172
+ HELP_COMMANDS.length + 1,
173
+ );
173
174
  expect(estimateItemHeight({ type: "status", text: "123456\n12" }, 5)).toBe(
174
175
  3,
175
176
  );
@@ -42,7 +42,7 @@ function writeLockfile(
42
42
  ): void {
43
43
  mkdirSync(testDir, { recursive: true });
44
44
  writeFileSync(
45
- join(testDir, ".cadet.lock.json"),
45
+ join(testDir, "lockfile.json"),
46
46
  JSON.stringify(
47
47
  {
48
48
  assistants: entries,
@@ -70,7 +70,7 @@ function jsonResponse(body: unknown, status = 200): Response {
70
70
  describe("cadet flags --assistant routing", () => {
71
71
  beforeEach(() => {
72
72
  process.env.CADET_LOCKFILE_DIR = testDir;
73
- rmSync(join(testDir, ".cadet.lock.json"), { force: true });
73
+ rmSync(join(testDir, "lockfile.json"), { force: true });
74
74
  fetchCalls = [];
75
75
  // Capture every outgoing fetch and respond with a stub matching the
76
76
  // subcommand's expected shape. The URL is what the test asserts on.
@@ -330,10 +330,10 @@ describe("hatch provider secrets", () => {
330
330
  expect(logs.join("\n")).toBe("");
331
331
  });
332
332
 
333
- test("endpoint-supplied providers are listed last but cannot back llm.defaultProvider", () => {
333
+ test("endpoint-supplied providers are listed last but cannot back llm.default", () => {
334
334
  // Listed so their keys can be collected, but hatch must not write them
335
- // as llm.default.provider — the assistant's DefaultProviderEnum rejects
336
- // them and hatch would fail at config parse.
335
+ // as llm.default.provider — the assistant's selection schema rejects
336
+ // them and the defaults would fall back to anthropic at resolution.
337
337
  expect(HATCH_PROVIDER_CHOICES.slice(-2)).toEqual(["litellm", "opencode"]);
338
338
  });
339
339
 
@@ -23,14 +23,14 @@ import {
23
23
 
24
24
  function writeLockfile(entry: Record<string, unknown>): void {
25
25
  writeFileSync(
26
- join(testDir, ".cadet.lock.json"),
26
+ join(testDir, "lockfile.json"),
27
27
  JSON.stringify({ assistants: [entry], activeAssistant: "ingress-test" }),
28
28
  );
29
29
  }
30
30
 
31
31
  function readLockfileEntry(): Record<string, unknown> {
32
32
  const data = JSON.parse(
33
- readFileSync(join(testDir, ".cadet.lock.json"), "utf-8"),
33
+ readFileSync(join(testDir, "lockfile.json"), "utf-8"),
34
34
  ) as { assistants: Record<string, unknown>[] };
35
35
  return data.assistants[0];
36
36
  }
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
 
3
- import { startLoopbackListener } from "../commands/login.js";
3
+ import { startLoopbackListener } from "../lib/cloud-login.js";
4
4
 
5
5
  /** Resolve "settled"/"pending" — proves whether `waitForCode` resolved. */
6
6
  async function settleState(