@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
|
@@ -9,7 +9,7 @@ export const LEFT_PANEL_WIDTH = 36;
|
|
|
9
9
|
export const DEFAULT_TERMINAL_COLUMNS = 80;
|
|
10
10
|
export const COMPACT_THRESHOLD = 60;
|
|
11
11
|
|
|
12
|
-
const LEFT_HEADER_LINES =
|
|
12
|
+
const LEFT_HEADER_LINES = 3;
|
|
13
13
|
const LEFT_FOOTER_LINES = 1;
|
|
14
14
|
const HEADER_TOP_BORDER_LINES = 1;
|
|
15
15
|
const HEADER_BOTTOM_BORDER_LINES = 2;
|
|
@@ -24,19 +24,47 @@ const RIGHT_PANEL_TIPS_HEADING = 1;
|
|
|
24
24
|
const TOOL_CALL_CHROME_LINES = 2;
|
|
25
25
|
const MESSAGE_SPACING = 1;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Kept in sync with the web client's command set
|
|
29
|
+
* (clients/web slash-command-catalog) plus the CLI-local commands.
|
|
30
|
+
*/
|
|
27
31
|
export const HELP_COMMANDS = [
|
|
28
32
|
{
|
|
29
33
|
command: "/btw <question>",
|
|
30
34
|
description: "Ask a side question while the assistant is working",
|
|
31
35
|
},
|
|
32
36
|
{
|
|
33
|
-
command: "/
|
|
34
|
-
description: "
|
|
37
|
+
command: "/config [path] [value]",
|
|
38
|
+
description: "Read or set a runtime config value",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
command: "/clean",
|
|
42
|
+
description: "Strip injected runtime context from the conversation",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
command: "/commands",
|
|
46
|
+
description: "List all available commands",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
command: "/compact",
|
|
50
|
+
description: "Force context compaction immediately",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
command: "/models",
|
|
54
|
+
description: "List all available models",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
command: "/status",
|
|
58
|
+
description: "Show conversation status and context usage",
|
|
35
59
|
},
|
|
36
60
|
{
|
|
37
61
|
command: "/clear",
|
|
38
62
|
description: "Clear the screen",
|
|
39
63
|
},
|
|
64
|
+
{
|
|
65
|
+
command: "/quit, /exit, /q",
|
|
66
|
+
description: "Disconnect and exit",
|
|
67
|
+
},
|
|
40
68
|
{
|
|
41
69
|
command: "/help, ?",
|
|
42
70
|
description: "Show this help",
|
|
@@ -186,7 +214,7 @@ export function truncateValue(value: unknown, maxLength: number): string {
|
|
|
186
214
|
}
|
|
187
215
|
|
|
188
216
|
export function formatHeaderTitle(assistantName?: string): string {
|
|
189
|
-
const rawTitle = assistantName?.trim() || "Meet your
|
|
217
|
+
const rawTitle = assistantName?.trim() || "Meet your Cadet!";
|
|
190
218
|
const title = rawTitle.replace(/\s+/g, " ");
|
|
191
219
|
const maxTitleLength = LEFT_PANEL_WIDTH - 2;
|
|
192
220
|
const displayTitle =
|
|
@@ -196,10 +224,6 @@ export function formatHeaderTitle(assistantName?: string): string {
|
|
|
196
224
|
return ` ${displayTitle}`;
|
|
197
225
|
}
|
|
198
226
|
|
|
199
|
-
export function formatHeaderEyebrow(): string {
|
|
200
|
-
return " Assistant";
|
|
201
|
-
}
|
|
202
|
-
|
|
203
227
|
export function stripAnsi(value: string): string {
|
|
204
228
|
return value.replace(/\x1b\[[0-9;]*m/g, "");
|
|
205
229
|
}
|
|
@@ -130,8 +130,10 @@ describe("path helpers", () => {
|
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
describe("getLockfilePath", () => {
|
|
133
|
-
test("production returns
|
|
134
|
-
expect(getLockfilePath(prod)).toBe(
|
|
133
|
+
test("production returns the config-dir lockfile.json", () => {
|
|
134
|
+
expect(getLockfilePath(prod)).toBe(
|
|
135
|
+
join(TEST_HOME, ".config", "cadet", "lockfile.json"),
|
|
136
|
+
);
|
|
135
137
|
});
|
|
136
138
|
|
|
137
139
|
test("dev returns ~/.config/cadet-dev/lockfile.json", () => {
|
|
@@ -153,7 +155,7 @@ describe("path helpers", () => {
|
|
|
153
155
|
...prod,
|
|
154
156
|
lockfileDirOverride: "/tmp/lock",
|
|
155
157
|
};
|
|
156
|
-
expect(getLockfilePath(env)).toBe("/tmp/lock
|
|
158
|
+
expect(getLockfilePath(env)).toBe("/tmp/lock/lockfile.json");
|
|
157
159
|
});
|
|
158
160
|
|
|
159
161
|
test("non-prod respects lockfileDirOverride (overrides configDir)", () => {
|
|
@@ -167,10 +169,9 @@ describe("path helpers", () => {
|
|
|
167
169
|
});
|
|
168
170
|
|
|
169
171
|
describe("getLockfilePaths", () => {
|
|
170
|
-
test("production returns
|
|
172
|
+
test("production returns a single canonical path", () => {
|
|
171
173
|
expect(getLockfilePaths(prod)).toEqual([
|
|
172
|
-
join(TEST_HOME, ".cadet.
|
|
173
|
-
join(TEST_HOME, ".cadet.lockfile.json"),
|
|
174
|
+
join(TEST_HOME, ".config", "cadet", "lockfile.json"),
|
|
174
175
|
]);
|
|
175
176
|
});
|
|
176
177
|
|
|
@@ -180,15 +181,12 @@ describe("path helpers", () => {
|
|
|
180
181
|
]);
|
|
181
182
|
});
|
|
182
183
|
|
|
183
|
-
test("production with lockfileDirOverride applies to
|
|
184
|
+
test("production with lockfileDirOverride applies to the canonical path", () => {
|
|
184
185
|
const env: EnvironmentDefinition = {
|
|
185
186
|
...prod,
|
|
186
187
|
lockfileDirOverride: "/tmp/lock",
|
|
187
188
|
};
|
|
188
|
-
expect(getLockfilePaths(env)).toEqual([
|
|
189
|
-
"/tmp/lock/.cadet.lock.json",
|
|
190
|
-
"/tmp/lock/.cadet.lockfile.json",
|
|
191
|
-
]);
|
|
189
|
+
expect(getLockfilePaths(env)).toEqual(["/tmp/lock/lockfile.json"]);
|
|
192
190
|
});
|
|
193
191
|
|
|
194
192
|
test("non-prod with lockfileDirOverride overrides the config dir", () => {
|
|
@@ -33,31 +33,13 @@ export function getConfigDirs(env: EnvironmentDefinition): string[] {
|
|
|
33
33
|
return resolveConfigDirPaths(process.env, localPathOptions(env));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* Lockfile candidate paths for an environment, in priority order.
|
|
38
|
-
*
|
|
39
|
-
* For production, returns both the current `.cadet.lock.json` and the
|
|
40
|
-
* legacy `.cadet.lockfile.json` so read-side callers can fall back to the
|
|
41
|
-
* legacy filename on installs that predate the rename. Non-production
|
|
42
|
-
* environments are new and have a single canonical path under the env-scoped
|
|
43
|
-
* XDG config directory.
|
|
44
|
-
*
|
|
45
|
-
* Read-side callers should iterate this array and use the first existing
|
|
46
|
-
* file (matching `cli/src/lib/assistant-config.ts:readLockfile`). Write-side
|
|
47
|
-
* callers should use {@link getLockfilePath}, which returns the first
|
|
48
|
-
* (canonical) entry.
|
|
49
|
-
*
|
|
50
|
-
* `env.lockfileDirOverride` (populated by the resolver from
|
|
51
|
-
* `CADET_LOCKFILE_DIR`) overrides the directory the lockfile lives in for
|
|
52
|
-
* both production and non-production environments.
|
|
53
|
-
*/
|
|
54
36
|
export function getLockfilePaths(env: EnvironmentDefinition): string[] {
|
|
55
37
|
return resolveLockfilePaths(process.env, localPathOptions(env));
|
|
56
38
|
}
|
|
57
39
|
|
|
58
40
|
/**
|
|
59
|
-
* Canonical lockfile path for writes
|
|
60
|
-
*
|
|
41
|
+
* Canonical lockfile path for writes — always the config-dir
|
|
42
|
+
* `lockfile.json`.
|
|
61
43
|
*/
|
|
62
44
|
export function getLockfilePath(env: EnvironmentDefinition): string {
|
|
63
45
|
return getLockfilePaths(env)[0]!;
|
|
@@ -28,6 +28,39 @@ import {
|
|
|
28
28
|
import { getConfigDir, getConfigDirs } from "./environments/paths.js";
|
|
29
29
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
30
30
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The guardian token fields as they arrive untrusted (gateway refresh/init
|
|
35
|
+
* responses, persisted token files). Every field is independently optional
|
|
36
|
+
* and independently caught: one malformed field must not invalidate the
|
|
37
|
+
* others — callers fall back per field.
|
|
38
|
+
*/
|
|
39
|
+
const GuardianTokenFieldsSchema = z
|
|
40
|
+
.object({
|
|
41
|
+
guardianPrincipalId: z.string().optional().catch(undefined),
|
|
42
|
+
accessToken: z.string().optional().catch(undefined),
|
|
43
|
+
accessTokenExpiresAt: z
|
|
44
|
+
.union([z.string(), z.number()])
|
|
45
|
+
.optional()
|
|
46
|
+
.catch(undefined),
|
|
47
|
+
refreshToken: z.string().optional().catch(undefined),
|
|
48
|
+
refreshTokenExpiresAt: z
|
|
49
|
+
.union([z.string(), z.number()])
|
|
50
|
+
.optional()
|
|
51
|
+
.catch(undefined),
|
|
52
|
+
refreshAfter: z.string().optional().catch(undefined),
|
|
53
|
+
isNew: z.boolean().optional().catch(undefined),
|
|
54
|
+
deviceId: z.string().optional().catch(undefined),
|
|
55
|
+
})
|
|
56
|
+
.catch({});
|
|
57
|
+
|
|
58
|
+
/** Narrow untrusted JSON to the token fields whose values have the right
|
|
59
|
+
* shape; fields absent or malformed come back undefined for callers to
|
|
60
|
+
* fall back on or reject. */
|
|
61
|
+
function parseGuardianTokenFields(value: unknown): Partial<GuardianTokenData> {
|
|
62
|
+
return GuardianTokenFieldsSchema.parse(value);
|
|
63
|
+
}
|
|
31
64
|
|
|
32
65
|
const DEVICE_ID_SALT = "mycadet-host-id";
|
|
33
66
|
|
|
@@ -470,19 +503,24 @@ export async function refreshGuardianTokenResult(
|
|
|
470
503
|
return classifyRefreshHttpStatus(response.status);
|
|
471
504
|
}
|
|
472
505
|
|
|
473
|
-
const
|
|
506
|
+
const parsed = parseGuardianTokenFields(await response.json());
|
|
507
|
+
if (parsed.accessToken == null) {
|
|
508
|
+
return {
|
|
509
|
+
ok: false,
|
|
510
|
+
status: 502,
|
|
511
|
+
error: "Assistant gateway returned a malformed token refresh response",
|
|
512
|
+
};
|
|
513
|
+
}
|
|
474
514
|
const refreshed: GuardianTokenData = {
|
|
475
515
|
guardianPrincipalId:
|
|
476
|
-
|
|
477
|
-
accessToken:
|
|
516
|
+
parsed.guardianPrincipalId ?? tokenData.guardianPrincipalId,
|
|
517
|
+
accessToken: parsed.accessToken,
|
|
478
518
|
accessTokenExpiresAt:
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
refreshToken: (json.refreshToken as string) ?? tokenData.refreshToken,
|
|
519
|
+
parsed.accessTokenExpiresAt ?? tokenData.accessTokenExpiresAt,
|
|
520
|
+
refreshToken: parsed.refreshToken ?? tokenData.refreshToken,
|
|
482
521
|
refreshTokenExpiresAt:
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
refreshAfter: (json.refreshAfter as string) ?? tokenData.refreshAfter,
|
|
522
|
+
parsed.refreshTokenExpiresAt ?? tokenData.refreshTokenExpiresAt,
|
|
523
|
+
refreshAfter: parsed.refreshAfter ?? tokenData.refreshAfter,
|
|
486
524
|
isNew: false,
|
|
487
525
|
deviceId: tokenData.deviceId,
|
|
488
526
|
leasedAt: new Date().toISOString(),
|
|
@@ -583,15 +621,28 @@ export async function leaseGuardianToken(
|
|
|
583
621
|
throw new Error(`guardian/init failed (${response.status}): ${body}`);
|
|
584
622
|
}
|
|
585
623
|
|
|
586
|
-
const
|
|
624
|
+
const parsed = parseGuardianTokenFields(await response.json());
|
|
625
|
+
if (
|
|
626
|
+
parsed.guardianPrincipalId == null ||
|
|
627
|
+
parsed.accessToken == null ||
|
|
628
|
+
parsed.accessTokenExpiresAt == null ||
|
|
629
|
+
parsed.refreshToken == null ||
|
|
630
|
+
parsed.refreshTokenExpiresAt == null ||
|
|
631
|
+
parsed.refreshAfter == null ||
|
|
632
|
+
parsed.isNew == null
|
|
633
|
+
) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
`guardian/init returned a malformed token response (${response.status})`,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
587
638
|
const tokenData: GuardianTokenData = {
|
|
588
|
-
guardianPrincipalId:
|
|
589
|
-
accessToken:
|
|
590
|
-
accessTokenExpiresAt:
|
|
591
|
-
refreshToken:
|
|
592
|
-
refreshTokenExpiresAt:
|
|
593
|
-
refreshAfter:
|
|
594
|
-
isNew:
|
|
639
|
+
guardianPrincipalId: parsed.guardianPrincipalId,
|
|
640
|
+
accessToken: parsed.accessToken,
|
|
641
|
+
accessTokenExpiresAt: parsed.accessTokenExpiresAt,
|
|
642
|
+
refreshToken: parsed.refreshToken,
|
|
643
|
+
refreshTokenExpiresAt: parsed.refreshTokenExpiresAt,
|
|
644
|
+
refreshAfter: parsed.refreshAfter,
|
|
645
|
+
isNew: parsed.isNew,
|
|
595
646
|
deviceId,
|
|
596
647
|
leasedAt: new Date().toISOString(),
|
|
597
648
|
};
|
|
@@ -675,7 +726,10 @@ export function seedGuardianTokenFromSiblingEnv(assistantId: string): boolean {
|
|
|
675
726
|
for (const { path: sibling } of candidates) {
|
|
676
727
|
try {
|
|
677
728
|
const raw = readFileSync(sibling);
|
|
678
|
-
const parsed =
|
|
729
|
+
const parsed = parseGuardianTokenFields(
|
|
730
|
+
JSON.parse(raw.toString("utf-8")),
|
|
731
|
+
);
|
|
732
|
+
if (parsed.refreshTokenExpiresAt == null) continue;
|
|
679
733
|
const refreshExpiry = new Date(parsed.refreshTokenExpiresAt).getTime();
|
|
680
734
|
if (!Number.isFinite(refreshExpiry) || refreshExpiry <= now) continue;
|
|
681
735
|
const dir = dirname(destPath);
|
package/src/lib/hatch-local.ts
CHANGED
|
@@ -49,6 +49,12 @@ import { logHatchNextSteps } from "./hatch-next-steps.js";
|
|
|
49
49
|
import { printInstanceDetails } from "./instance-details.js";
|
|
50
50
|
import { checkProviderApiKey } from "./api-key-check.js";
|
|
51
51
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
52
|
+
import {
|
|
53
|
+
describeLoginError,
|
|
54
|
+
ensureCloudLoginInteractive,
|
|
55
|
+
registerLocalAssistantWithPlatform,
|
|
56
|
+
} from "./cloud-login.js";
|
|
57
|
+
import { fetchCurrentUser, readPlatformToken } from "./platform-client.js";
|
|
52
58
|
import { withLocalHatchLock } from "./local-hatch-lock.js";
|
|
53
59
|
|
|
54
60
|
/**
|
|
@@ -179,6 +185,8 @@ export interface HatchLocalResult {
|
|
|
179
185
|
guardianAccessToken?: string;
|
|
180
186
|
}
|
|
181
187
|
|
|
188
|
+
|
|
189
|
+
|
|
182
190
|
export async function hatchLocal(
|
|
183
191
|
params: HatchLocalParams = {},
|
|
184
192
|
): Promise<HatchLocalResult> {
|
|
@@ -247,6 +255,32 @@ export async function hatchLocal(
|
|
|
247
255
|
// Collect the key immediately, while the choice is fresh — the
|
|
248
256
|
// startup logs are quiet now, so nothing separates the two prompts.
|
|
249
257
|
precollectedApiKey = await collectProviderApiKey(picked);
|
|
258
|
+
|
|
259
|
+
// Picking Cadet Cloud should feel like signing in, not like a
|
|
260
|
+
// mystery later — open the browser login right here. Skipped for
|
|
261
|
+
// the desktop app (it runs its own login UI) and non-TTY runs.
|
|
262
|
+
if (
|
|
263
|
+
picked === "cadet" &&
|
|
264
|
+
!process.env.CADET_DESKTOP_APP &&
|
|
265
|
+
process.stdin.isTTY &&
|
|
266
|
+
!readPlatformToken()
|
|
267
|
+
) {
|
|
268
|
+
reporter.log("");
|
|
269
|
+
reporter.log(
|
|
270
|
+
"Cadet Cloud selected — opening your browser to log in...",
|
|
271
|
+
);
|
|
272
|
+
try {
|
|
273
|
+
await ensureCloudLoginInteractive();
|
|
274
|
+
reporter.log("✅ Logged in to Cadet Cloud");
|
|
275
|
+
} catch (error) {
|
|
276
|
+
reporter.warn(
|
|
277
|
+
`⚠️ Cadet Cloud login failed: ${describeLoginError(error)}`,
|
|
278
|
+
);
|
|
279
|
+
reporter.warn(
|
|
280
|
+
" You can retry later with `cadet login`.",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
250
284
|
}
|
|
251
285
|
}
|
|
252
286
|
|
|
@@ -267,7 +301,7 @@ export async function hatchLocal(
|
|
|
267
301
|
process.env.APP_VERSION = cliPkg.version;
|
|
268
302
|
}
|
|
269
303
|
|
|
270
|
-
reporter.progress(2, 6, "
|
|
304
|
+
reporter.progress(2, 6, "Setting up local assistant...");
|
|
271
305
|
const hatchConfigValues = buildHatchConfigValues(configValues, provider);
|
|
272
306
|
const defaultWorkspaceConfigPath = writeInitialConfig(hatchConfigValues);
|
|
273
307
|
|
|
@@ -381,6 +415,23 @@ export async function hatchLocal(
|
|
|
381
415
|
});
|
|
382
416
|
}
|
|
383
417
|
|
|
418
|
+
// Picking Cadet Cloud ends with the assistant registered on the platform
|
|
419
|
+
// and its cloud credentials injected — the same post-login flow
|
|
420
|
+
// `cadet login` runs — so messages route through the managed connection
|
|
421
|
+
// immediately after hatch. Non-fatal: the assistant is up either way.
|
|
422
|
+
if (provider === "cadet" && readPlatformToken()) {
|
|
423
|
+
try {
|
|
424
|
+
const token = readPlatformToken()!;
|
|
425
|
+
const user = await fetchCurrentUser(token);
|
|
426
|
+
await registerLocalAssistantWithPlatform(token, user, {
|
|
427
|
+
bearerToken: guardianAccessToken,
|
|
428
|
+
log: (m) => reporter.log(m),
|
|
429
|
+
});
|
|
430
|
+
} catch {
|
|
431
|
+
// Non-fatal — `cadet login` can complete the registration later.
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
384
435
|
reporter.log("");
|
|
385
436
|
reporter.log(`✅ Local assistant hatched!`);
|
|
386
437
|
printInstanceDetails(
|
package/src/lib/health-check.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const HealthResponseSchema = z.object({
|
|
5
|
+
status: z.string().optional(),
|
|
6
|
+
message: z.string().optional(),
|
|
7
|
+
version: z.string().optional(),
|
|
8
|
+
});
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
status: string;
|
|
7
|
-
message?: string;
|
|
8
|
-
version?: string;
|
|
9
|
-
}
|
|
10
|
+
export const HEALTH_CHECK_TIMEOUT_MS = 1500;
|
|
10
11
|
|
|
11
12
|
export interface HealthCheckResult {
|
|
12
13
|
status: string;
|
|
@@ -57,7 +58,9 @@ export async function checkManagedHealth(
|
|
|
57
58
|
return { status: `error (${response.status})`, detail: null };
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
const data =
|
|
61
|
+
const data = HealthResponseSchema
|
|
62
|
+
.catch({})
|
|
63
|
+
.parse(await response.json());
|
|
61
64
|
const status = data.status || "unknown";
|
|
62
65
|
return {
|
|
63
66
|
status,
|
|
@@ -168,12 +171,12 @@ export async function fetchManagedPs(
|
|
|
168
171
|
}
|
|
169
172
|
}
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
state: string
|
|
173
|
-
is_awake: boolean
|
|
174
|
-
pod_status: string
|
|
175
|
-
detail: string
|
|
176
|
-
}
|
|
174
|
+
const LegacyConnectionStatusSchema = z.object({
|
|
175
|
+
state: z.string(),
|
|
176
|
+
is_awake: z.boolean(),
|
|
177
|
+
pod_status: z.string().nullable(),
|
|
178
|
+
detail: z.string().nullable(),
|
|
179
|
+
});
|
|
177
180
|
|
|
178
181
|
async function fetchLegacyConnectionStatus(
|
|
179
182
|
runtimeUrl: string,
|
|
@@ -194,7 +197,11 @@ async function fetchLegacyConnectionStatus(
|
|
|
194
197
|
clearTimeout(timeoutId);
|
|
195
198
|
if (!response.ok) return null;
|
|
196
199
|
|
|
197
|
-
const
|
|
200
|
+
const parsed = LegacyConnectionStatusSchema.safeParse(
|
|
201
|
+
await response.json(),
|
|
202
|
+
);
|
|
203
|
+
if (!parsed.success) return null;
|
|
204
|
+
const data = parsed.data;
|
|
198
205
|
|
|
199
206
|
// Translate legacy shape into the ps process tree
|
|
200
207
|
const status: ManagedProcessEntry["status"] = data.is_awake
|
|
@@ -240,7 +247,9 @@ export async function checkHealth(
|
|
|
240
247
|
return { status: `error (${response.status})`, detail: null };
|
|
241
248
|
}
|
|
242
249
|
|
|
243
|
-
const data =
|
|
250
|
+
const data = HealthResponseSchema
|
|
251
|
+
.catch({})
|
|
252
|
+
.parse(await response.json());
|
|
244
253
|
const status = data.status || "unknown";
|
|
245
254
|
return {
|
|
246
255
|
status,
|
|
@@ -24,7 +24,15 @@ export interface LifecycleReporter {
|
|
|
24
24
|
* that existing subprocess consumers parse.
|
|
25
25
|
*/
|
|
26
26
|
export const consoleLifecycleReporter: LifecycleReporter = {
|
|
27
|
-
progress: (step, total, label) =>
|
|
27
|
+
progress: (step, total, label) => {
|
|
28
|
+
emitProgress(step, total, label);
|
|
29
|
+
// Plain terminals get an active status line per step — without this the
|
|
30
|
+
// startup goes silent between prompts (the desktop channel is the only
|
|
31
|
+
// consumer of emitProgress otherwise).
|
|
32
|
+
if (!process.env.CADET_DESKTOP_APP) {
|
|
33
|
+
console.log(`[${step}/${total}] ${label}`);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
28
36
|
log: (message) => console.log(message),
|
|
29
37
|
warn: (message) => console.warn(message),
|
|
30
38
|
error: (message) => console.error(message),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "child_process";
|
|
2
2
|
import { createHash, randomUUID } from "crypto";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
import {
|
|
4
5
|
closeSync,
|
|
5
6
|
mkdirSync,
|
|
@@ -57,7 +58,7 @@ function processIsAlive(pid: number): boolean {
|
|
|
57
58
|
process.kill(pid, 0);
|
|
58
59
|
return true;
|
|
59
60
|
} catch (error) {
|
|
60
|
-
return (error
|
|
61
|
+
return errnoCode(error) === "EPERM";
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -87,30 +88,21 @@ function processStartedAt(pid: number): string | undefined {
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
function errnoCode(error: unknown): string | undefined {
|
|
92
|
+
if (!(error instanceof Error)) return undefined;
|
|
93
|
+
if (!("code" in error) || typeof error.code !== "string") return undefined;
|
|
94
|
+
return error.code;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const LockOwnerFileSchema = z.object({
|
|
98
|
+
pid: z.number().int().positive(),
|
|
99
|
+
processStartedAt: z.string().min(1).optional().catch(undefined),
|
|
100
|
+
token: z.string().min(1),
|
|
101
|
+
});
|
|
102
|
+
|
|
90
103
|
function parseOwner(raw: string): LockOwner | undefined {
|
|
91
104
|
try {
|
|
92
|
-
|
|
93
|
-
pid?: unknown;
|
|
94
|
-
processStartedAt?: unknown;
|
|
95
|
-
token?: unknown;
|
|
96
|
-
};
|
|
97
|
-
if (
|
|
98
|
-
typeof parsed.pid === "number" &&
|
|
99
|
-
Number.isInteger(parsed.pid) &&
|
|
100
|
-
parsed.pid > 0 &&
|
|
101
|
-
typeof parsed.token === "string" &&
|
|
102
|
-
parsed.token.length > 0
|
|
103
|
-
) {
|
|
104
|
-
return {
|
|
105
|
-
pid: parsed.pid,
|
|
106
|
-
processStartedAt:
|
|
107
|
-
typeof parsed.processStartedAt === "string" &&
|
|
108
|
-
parsed.processStartedAt.length > 0
|
|
109
|
-
? parsed.processStartedAt
|
|
110
|
-
: undefined,
|
|
111
|
-
token: parsed.token,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
105
|
+
return LockOwnerFileSchema.parse(JSON.parse(raw));
|
|
114
106
|
} catch {}
|
|
115
107
|
return undefined;
|
|
116
108
|
}
|
|
@@ -195,7 +187,7 @@ function acquireRecoveryClaim(
|
|
|
195
187
|
writeOwnerFile(claimPath, owner);
|
|
196
188
|
return () => releaseLock(claimPath, owner.token);
|
|
197
189
|
} catch (error) {
|
|
198
|
-
if ((error
|
|
190
|
+
if (errnoCode(error) !== "EEXIST") {
|
|
199
191
|
throw error;
|
|
200
192
|
}
|
|
201
193
|
if (
|
|
@@ -248,7 +240,7 @@ function removeStaleLock(
|
|
|
248
240
|
unlinkSync(lockPath);
|
|
249
241
|
return true;
|
|
250
242
|
} catch (error) {
|
|
251
|
-
return (error
|
|
243
|
+
return errnoCode(error) === "ENOENT";
|
|
252
244
|
}
|
|
253
245
|
} finally {
|
|
254
246
|
releaseRecoveryClaim();
|
|
@@ -262,7 +254,7 @@ function releaseLock(lockPath: string, token: string): void {
|
|
|
262
254
|
try {
|
|
263
255
|
unlinkSync(lockPath);
|
|
264
256
|
} catch (error) {
|
|
265
|
-
if ((error
|
|
257
|
+
if (errnoCode(error) !== "ENOENT") {
|
|
266
258
|
throw error;
|
|
267
259
|
}
|
|
268
260
|
}
|
|
@@ -306,7 +298,7 @@ export async function withLocalHatchLock<T>(
|
|
|
306
298
|
try {
|
|
307
299
|
writeOwnerFile(lockPath, owner);
|
|
308
300
|
} catch (error) {
|
|
309
|
-
if ((error
|
|
301
|
+
if (errnoCode(error) !== "EEXIST") {
|
|
310
302
|
throw error;
|
|
311
303
|
}
|
|
312
304
|
if (removeStaleLock(lockPath, owner, liveOwnerRecoveryGraceMs)) {
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
resolveRuntimeUrl,
|
|
12
12
|
} from "./runtime-url.js";
|
|
13
13
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
const RuntimeIdentitySchema = z.object({ version: z.string().min(1) });
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* Thrown when the local runtime returns 409 for an export/import request
|
|
@@ -66,15 +69,23 @@ async function migrationRequestHeaders(
|
|
|
66
69
|
return bearerHeaders(token);
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
detail
|
|
72
|
+
const Raw409BodySchema = z.object({
|
|
73
|
+
detail: z.string().optional(),
|
|
71
74
|
// The runtime's current 409 contract nests the payload under `error`:
|
|
72
75
|
// { error: { code: "export_in_progress" | "import_in_progress", job_id } }
|
|
73
76
|
// We also tolerate a legacy flat shape ({ code, job_id }) for resilience.
|
|
74
|
-
error
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
error: z
|
|
78
|
+
.union([
|
|
79
|
+
z.string(),
|
|
80
|
+
z.object({
|
|
81
|
+
code: z.string().optional(),
|
|
82
|
+
job_id: z.string().optional(),
|
|
83
|
+
}),
|
|
84
|
+
])
|
|
85
|
+
.optional(),
|
|
86
|
+
code: z.string().optional(),
|
|
87
|
+
job_id: z.string().optional(),
|
|
88
|
+
});
|
|
78
89
|
|
|
79
90
|
/** Common 409 → MigrationInProgressError parsing used by the two POST helpers. */
|
|
80
91
|
async function throwIfInProgress(
|
|
@@ -82,11 +93,11 @@ async function throwIfInProgress(
|
|
|
82
93
|
defaultKind: "export_in_progress" | "import_in_progress",
|
|
83
94
|
): Promise<void> {
|
|
84
95
|
if (response.status !== 409) return;
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
const parsed = Raw409BodySchema.safeParse(
|
|
97
|
+
await response.json().catch(() => ({})),
|
|
98
|
+
);
|
|
99
|
+
const body = parsed.success ? parsed.data : {};
|
|
100
|
+
const nested = typeof body.error === "object" ? body.error : undefined;
|
|
90
101
|
const jobId = nested?.job_id ?? body.job_id ?? "";
|
|
91
102
|
const rawKind =
|
|
92
103
|
nested?.code ??
|
|
@@ -355,9 +366,9 @@ export async function localRuntimeIdentity(
|
|
|
355
366
|
`Failed to fetch runtime identity: ${response.status} ${response.statusText}`,
|
|
356
367
|
);
|
|
357
368
|
}
|
|
358
|
-
const
|
|
359
|
-
if (
|
|
369
|
+
const parsed = RuntimeIdentitySchema.safeParse(await response.json());
|
|
370
|
+
if (!parsed.success) {
|
|
360
371
|
throw new Error("Runtime identity response missing version");
|
|
361
372
|
}
|
|
362
|
-
return { version:
|
|
373
|
+
return { version: parsed.data.version };
|
|
363
374
|
}
|