@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
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(
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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);
|
|
@@ -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) =>
|
|
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
|
|
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
|
-
|
|
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.
|
|
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, ".
|
|
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, ".
|
|
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, ".
|
|
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, ".
|
|
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, ".
|
|
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, ".
|
|
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, ".
|
|
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
|
|
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
|
|
721
|
-
//
|
|
722
|
-
//
|
|
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
|
-
|
|
729
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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, ".
|
|
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, ".
|
|
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.
|
|
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
|
|
336
|
-
// them and
|
|
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, ".
|
|
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, ".
|
|
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 "../
|
|
3
|
+
import { startLoopbackListener } from "../lib/cloud-login.js";
|
|
4
4
|
|
|
5
5
|
/** Resolve "settled"/"pending" — proves whether `waitForCode` resolved. */
|
|
6
6
|
async function settleState(
|