@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/src/lib/local.ts
CHANGED
|
@@ -133,9 +133,7 @@ function packagePath(
|
|
|
133
133
|
function hasLocalRuntimeComponents(installDir: string): boolean {
|
|
134
134
|
return (
|
|
135
135
|
existsSync(packagePath(installDir, "@mycadet/assistant", "src/index.ts")) &&
|
|
136
|
-
existsSync(
|
|
137
|
-
packagePath(installDir, "@mycadet/gateway", "src/index.ts"),
|
|
138
|
-
) &&
|
|
136
|
+
existsSync(packagePath(installDir, "@mycadet/gateway", "src/index.ts")) &&
|
|
139
137
|
existsSync(
|
|
140
138
|
packagePath(installDir, "@mycadet/credential-executor", "src/main.ts"),
|
|
141
139
|
)
|
|
@@ -556,10 +554,11 @@ function resolveDaemonMainPath(assistantIndex: string): string {
|
|
|
556
554
|
*
|
|
557
555
|
* Both the daemon and gateway must use the same HMAC signing key so JWT
|
|
558
556
|
* tokens minted by one can be verified by the other. The CLI generates
|
|
559
|
-
*
|
|
560
|
-
*
|
|
557
|
+
* a key at hatch and passes it as `ACTOR_TOKEN_SIGNING_KEY` to both
|
|
558
|
+
* processes — the daemon and gateway each persist it on their
|
|
561
559
|
* own terms (the `.cadet/` directory layout is their concern, not the
|
|
562
|
-
* CLI's).
|
|
560
|
+
* CLI's). Lifecycle resumes resolve the existing identity rather than
|
|
561
|
+
* generating a key.
|
|
563
562
|
*/
|
|
564
563
|
export function generateLocalSigningKey(): string {
|
|
565
564
|
return randomBytes(32).toString("hex");
|
|
@@ -602,18 +601,9 @@ function applyDaemonEnvOverrides(
|
|
|
602
601
|
options?: DaemonStartOptions,
|
|
603
602
|
): void {
|
|
604
603
|
if (resources) {
|
|
605
|
-
env.CADET_WORKSPACE_DIR = join(
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
);
|
|
609
|
-
env.GATEWAY_SECURITY_DIR = join(
|
|
610
|
-
resources.instanceDir,
|
|
611
|
-
"gateway-security",
|
|
612
|
-
);
|
|
613
|
-
env.CREDENTIAL_SECURITY_DIR = join(
|
|
614
|
-
resources.instanceDir,
|
|
615
|
-
"ces-security",
|
|
616
|
-
);
|
|
604
|
+
env.CADET_WORKSPACE_DIR = join(resources.instanceDir, "workspace");
|
|
605
|
+
env.GATEWAY_SECURITY_DIR = join(resources.instanceDir, "gateway-security");
|
|
606
|
+
env.CREDENTIAL_SECURITY_DIR = join(resources.instanceDir, "ces-security");
|
|
617
607
|
env.CES_DATA_DIR = join(resources.instanceDir, "ces-data");
|
|
618
608
|
env.RUNTIME_HTTP_PORT = String(resources.daemonPort);
|
|
619
609
|
env.GATEWAY_PORT = String(resources.gatewayPort);
|
|
@@ -1075,9 +1065,7 @@ export async function startCes(
|
|
|
1075
1065
|
watch: boolean = false,
|
|
1076
1066
|
resources?: LocalInstanceResources,
|
|
1077
1067
|
): Promise<void> {
|
|
1078
|
-
const cadetDir = resources
|
|
1079
|
-
? resources.instanceDir
|
|
1080
|
-
: defaultCadetRoot();
|
|
1068
|
+
const cadetDir = resources ? resources.instanceDir : defaultCadetRoot();
|
|
1081
1069
|
const cesPidFile = join(cadetDir, "ces.pid");
|
|
1082
1070
|
|
|
1083
1071
|
// Kill any existing sibling first — a stale CES holds the socket and would
|
|
@@ -1513,7 +1501,8 @@ export async function startLocalDaemon(
|
|
|
1513
1501
|
watch: boolean = false,
|
|
1514
1502
|
resources: LocalInstanceResources,
|
|
1515
1503
|
options?: DaemonStartOptions,
|
|
1516
|
-
): Promise<void> {
|
|
1504
|
+
): Promise<void> {
|
|
1505
|
+
writeAssistantWrapper(resources);
|
|
1517
1506
|
|
|
1518
1507
|
const runtimeAssistantIndex = !watch
|
|
1519
1508
|
? localRuntimeAssistantIndex(resources)
|
|
@@ -1823,9 +1812,7 @@ export async function startGateway(
|
|
|
1823
1812
|
// Without this, crashed/stale gateways accumulate as zombies — the old
|
|
1824
1813
|
// process holds the port (or lingers after losing it), and every restart
|
|
1825
1814
|
// attempt spawns yet another process that fails with EADDRINUSE.
|
|
1826
|
-
const gwPidDir = resources
|
|
1827
|
-
? resources.instanceDir
|
|
1828
|
-
: defaultCadetRoot();
|
|
1815
|
+
const gwPidDir = resources ? resources.instanceDir : defaultCadetRoot();
|
|
1829
1816
|
const gwPidFile = join(gwPidDir, "gateway.pid");
|
|
1830
1817
|
await stopProcessByPidFile(gwPidFile, "gateway");
|
|
1831
1818
|
|
|
@@ -1861,18 +1848,9 @@ export async function startGateway(
|
|
|
1861
1848
|
// assistant DB directly for guardian bootstrap.
|
|
1862
1849
|
...(resources
|
|
1863
1850
|
? {
|
|
1864
|
-
CADET_WORKSPACE_DIR: join(
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
),
|
|
1868
|
-
GATEWAY_SECURITY_DIR: join(
|
|
1869
|
-
resources.instanceDir,
|
|
1870
|
-
"gateway-security",
|
|
1871
|
-
),
|
|
1872
|
-
CREDENTIAL_SECURITY_DIR: join(
|
|
1873
|
-
resources.instanceDir,
|
|
1874
|
-
"ces-security",
|
|
1875
|
-
),
|
|
1851
|
+
CADET_WORKSPACE_DIR: join(resources.instanceDir, "workspace"),
|
|
1852
|
+
GATEWAY_SECURITY_DIR: join(resources.instanceDir, "gateway-security"),
|
|
1853
|
+
CREDENTIAL_SECURITY_DIR: join(resources.instanceDir, "ces-security"),
|
|
1876
1854
|
CES_DATA_DIR: join(resources.instanceDir, "ces-data"),
|
|
1877
1855
|
}
|
|
1878
1856
|
: {}),
|
|
@@ -1928,9 +1906,7 @@ export async function startGateway(
|
|
|
1928
1906
|
gateway.unref();
|
|
1929
1907
|
|
|
1930
1908
|
if (gateway.pid) {
|
|
1931
|
-
const gwPidDir = resources
|
|
1932
|
-
? resources.instanceDir
|
|
1933
|
-
: defaultCadetRoot();
|
|
1909
|
+
const gwPidDir = resources ? resources.instanceDir : defaultCadetRoot();
|
|
1934
1910
|
writeFileSync(join(gwPidDir, "gateway.pid"), String(gateway.pid), "utf-8");
|
|
1935
1911
|
}
|
|
1936
1912
|
|
|
@@ -1976,9 +1952,7 @@ function isNgrokProcess(pid: number): boolean {
|
|
|
1976
1952
|
export async function stopLocalProcesses(
|
|
1977
1953
|
resources?: LocalInstanceResources,
|
|
1978
1954
|
): Promise<void> {
|
|
1979
|
-
const cadetDir = resources
|
|
1980
|
-
? resources.instanceDir
|
|
1981
|
-
: defaultCadetRoot();
|
|
1955
|
+
const cadetDir = resources ? resources.instanceDir : defaultCadetRoot();
|
|
1982
1956
|
const daemonPidFile = getDaemonPidPath(resources);
|
|
1983
1957
|
await stopProcessByPidFile(
|
|
1984
1958
|
daemonPidFile,
|
package/src/lib/ngrok.ts
CHANGED
|
@@ -12,19 +12,35 @@ import {
|
|
|
12
12
|
saveNgrokDomain,
|
|
13
13
|
} from "./ingress-config.js";
|
|
14
14
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
15
|
+
import { z } from "zod";
|
|
15
16
|
|
|
16
17
|
const NGROK_API_URL = "http://127.0.0.1:4040/api/tunnels";
|
|
17
18
|
const NGROK_POLL_INTERVAL_MS = 500;
|
|
18
19
|
const NGROK_POLL_TIMEOUT_MS = 15_000;
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
public_url: string
|
|
22
|
-
config
|
|
23
|
-
|
|
21
|
+
const NgrokTunnelSchema = z.object({
|
|
22
|
+
public_url: z.string(),
|
|
23
|
+
config: z
|
|
24
|
+
.object({
|
|
25
|
+
addr: z.string().optional(),
|
|
26
|
+
})
|
|
27
|
+
.optional(),
|
|
28
|
+
});
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
type NgrokTunnel = z.infer<typeof NgrokTunnelSchema>;
|
|
31
|
+
|
|
32
|
+
const NgrokTunnelsResponseSchema = z.object({
|
|
33
|
+
tunnels: z.array(NgrokTunnelSchema).optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The ingress block of the workspace config, as read by the tunnel edge:
|
|
38
|
+
* explicitly enabled with a saved public URL.
|
|
39
|
+
*/
|
|
40
|
+
export const IngressConfigSchema = z.object({
|
|
41
|
+
enabled: z.boolean().optional(),
|
|
42
|
+
publicBaseUrl: z.string().optional(),
|
|
43
|
+
});
|
|
28
44
|
|
|
29
45
|
/**
|
|
30
46
|
* Check whether ngrok is installed and accessible on the PATH.
|
|
@@ -54,8 +70,8 @@ async function queryNgrokTunnels(): Promise<NgrokTunnel[] | null> {
|
|
|
54
70
|
signal: AbortSignal.timeout(2_000),
|
|
55
71
|
});
|
|
56
72
|
if (!res.ok) return null;
|
|
57
|
-
const
|
|
58
|
-
return data.tunnels ?? [];
|
|
73
|
+
const parsed = NgrokTunnelsResponseSchema.safeParse(await res.json());
|
|
74
|
+
return parsed.success ? (parsed.data.tunnels ?? []) : null;
|
|
59
75
|
} catch {
|
|
60
76
|
return null;
|
|
61
77
|
}
|
|
@@ -194,13 +210,27 @@ export async function waitForNgrokUrl(
|
|
|
194
210
|
* Check whether an already-loaded workspace config has webhook-based
|
|
195
211
|
* integrations (e.g. Telegram, Twilio) that require a public ingress URL.
|
|
196
212
|
*/
|
|
213
|
+
const TelegramIntegrationSchema = z.object({
|
|
214
|
+
botUsername: z.unknown().optional(),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const TwilioIntegrationSchema = z.object({
|
|
218
|
+
accountSid: z.unknown().optional(),
|
|
219
|
+
phoneNumber: z.unknown().optional(),
|
|
220
|
+
});
|
|
221
|
+
|
|
197
222
|
export function hasWebhookIntegrations(
|
|
198
223
|
config: Record<string, unknown>,
|
|
199
224
|
): boolean {
|
|
200
|
-
const telegram = config.telegram
|
|
201
|
-
if (telegram
|
|
202
|
-
const twilio = config.twilio
|
|
203
|
-
if (
|
|
225
|
+
const telegram = TelegramIntegrationSchema.safeParse(config.telegram);
|
|
226
|
+
if (telegram.success && telegram.data.botUsername) return true;
|
|
227
|
+
const twilio = TwilioIntegrationSchema.safeParse(config.twilio);
|
|
228
|
+
if (
|
|
229
|
+
twilio.success &&
|
|
230
|
+
(twilio.data.accountSid || twilio.data.phoneNumber)
|
|
231
|
+
) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
204
234
|
return false;
|
|
205
235
|
}
|
|
206
236
|
|
|
@@ -226,9 +256,11 @@ export function hasWebhookIntegrationsConfigured(
|
|
|
226
256
|
function hasNonNgrokIngressUrl(workspaceDir: string): boolean {
|
|
227
257
|
try {
|
|
228
258
|
const config = loadRawConfig(workspaceDir);
|
|
229
|
-
const
|
|
230
|
-
const publicBaseUrl =
|
|
231
|
-
|
|
259
|
+
const parsed = IngressConfigSchema.safeParse(config.ingress);
|
|
260
|
+
const publicBaseUrl = parsed.success
|
|
261
|
+
? parsed.data.publicBaseUrl
|
|
262
|
+
: undefined;
|
|
263
|
+
if (!publicBaseUrl) return false;
|
|
232
264
|
return !publicBaseUrl.includes("ngrok");
|
|
233
265
|
} catch {
|
|
234
266
|
return false;
|
|
@@ -16,6 +16,15 @@ import { getLockfilePlatformBaseUrl } from "./assistant-config.js";
|
|
|
16
16
|
import { getConfigDir, getConfigDirs } from "./environments/paths.js";
|
|
17
17
|
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
18
18
|
import { loopbackSafeFetch } from "./loopback-fetch.js";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
|
|
21
|
+
const PlatformErrorDetailSchema = z.object({
|
|
22
|
+
detail: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const InProgressResponseSchema = z.object({
|
|
26
|
+
in_progress: z.boolean().optional(),
|
|
27
|
+
});
|
|
19
28
|
|
|
20
29
|
function getPlatformTokenPath(): string {
|
|
21
30
|
return join(getConfigDir(getCurrentEnvironment()), "platform-token");
|
|
@@ -200,6 +209,95 @@ export interface HatchedAssistant {
|
|
|
200
209
|
status: string;
|
|
201
210
|
}
|
|
202
211
|
|
|
212
|
+
const HatchedAssistantSchema = z.object({
|
|
213
|
+
id: z.string(),
|
|
214
|
+
name: z.string(),
|
|
215
|
+
status: z.string(),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const HatchedListResponseSchema = z.object({
|
|
219
|
+
results: z.array(HatchedAssistantSchema).optional(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const EnsureRegistrationResponseSchema = z.object({
|
|
223
|
+
assistant: z.object({ id: z.string(), name: z.string() }),
|
|
224
|
+
registration: z.object({
|
|
225
|
+
client_installation_id: z.string(),
|
|
226
|
+
runtime_assistant_id: z.string(),
|
|
227
|
+
client_platform: z.string(),
|
|
228
|
+
}),
|
|
229
|
+
assistant_api_key: z.string().nullable(),
|
|
230
|
+
webhook_secret: z.string(),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const ReprovisionApiKeyResponseSchema = z.object({
|
|
234
|
+
provisioning: z.object({ assistant_api_key: z.string() }),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const SecretReadResponseSchema = z
|
|
238
|
+
.object({
|
|
239
|
+
found: z.boolean(),
|
|
240
|
+
value: z.string().optional(),
|
|
241
|
+
unreachable: z.boolean().optional(),
|
|
242
|
+
})
|
|
243
|
+
.catch({ found: false });
|
|
244
|
+
|
|
245
|
+
const ReleaseChannelResponseSchema = z.object({
|
|
246
|
+
current_release_version: z.string().nullable().optional(),
|
|
247
|
+
release_channel: z.string().optional(),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const OrganizationListResponseSchema = z.object({
|
|
251
|
+
results: z.array(z.object({ id: z.string(), name: z.string() })).optional(),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const AllauthSessionResponseSchema = z.object({
|
|
255
|
+
status: z.number(),
|
|
256
|
+
data: z.object({
|
|
257
|
+
user: z.object({ id: z.string(), email: z.string(), display: z.string() }),
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const VersionCheckResponseSchema = z
|
|
262
|
+
.object({
|
|
263
|
+
detail: z.string().optional(),
|
|
264
|
+
version: z.string().nullable().optional(),
|
|
265
|
+
})
|
|
266
|
+
.catch({});
|
|
267
|
+
|
|
268
|
+
const PassthroughBodySchema = z.record(z.string(), z.unknown()).catch({});
|
|
269
|
+
|
|
270
|
+
const BundleUploadUrlSchema = z.object({
|
|
271
|
+
url: z.string(),
|
|
272
|
+
bundle_key: z.string(),
|
|
273
|
+
expires_at: z.string(),
|
|
274
|
+
max_content_length: z.number().optional(),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
const BundleImportErrorSchema = z
|
|
278
|
+
.object({
|
|
279
|
+
detail: z.string().optional(),
|
|
280
|
+
reason: z.string().optional(),
|
|
281
|
+
bundle_compat: z
|
|
282
|
+
.object({
|
|
283
|
+
min_runtime_version: z.string(),
|
|
284
|
+
max_runtime_version: z.string().nullable(),
|
|
285
|
+
})
|
|
286
|
+
.optional()
|
|
287
|
+
.catch(undefined),
|
|
288
|
+
target_runtime_version: z.string().optional(),
|
|
289
|
+
})
|
|
290
|
+
.catch({});
|
|
291
|
+
|
|
292
|
+
const RawUnifiedJobStatusSchema = z.object({
|
|
293
|
+
job_id: z.string(),
|
|
294
|
+
type: z.enum(["export", "import"]),
|
|
295
|
+
status: z.enum(["processing", "complete", "failed"]),
|
|
296
|
+
bundle_key: z.string().optional(),
|
|
297
|
+
result: z.unknown().optional(),
|
|
298
|
+
error: z.string().optional(),
|
|
299
|
+
});
|
|
300
|
+
|
|
203
301
|
export interface HatchAssistantResult {
|
|
204
302
|
assistant: HatchedAssistant;
|
|
205
303
|
/** true when the platform returned an existing assistant (HTTP 200) */
|
|
@@ -276,7 +374,7 @@ export async function ensureSelfHostedLocalRegistration(
|
|
|
276
374
|
);
|
|
277
375
|
}
|
|
278
376
|
|
|
279
|
-
return (await response.json())
|
|
377
|
+
return EnsureRegistrationResponseSchema.parse(await response.json());
|
|
280
378
|
}
|
|
281
379
|
|
|
282
380
|
// ---------------------------------------------------------------------------
|
|
@@ -342,7 +440,7 @@ export async function reprovisionAssistantApiKey(
|
|
|
342
440
|
);
|
|
343
441
|
}
|
|
344
442
|
|
|
345
|
-
return (await response.json())
|
|
443
|
+
return ReprovisionApiKeyResponseSchema.parse(await response.json());
|
|
346
444
|
}
|
|
347
445
|
|
|
348
446
|
// ---------------------------------------------------------------------------
|
|
@@ -442,11 +540,7 @@ export async function readGatewayCredential(
|
|
|
442
540
|
return { value: null, unreachable: response.status >= 500 };
|
|
443
541
|
}
|
|
444
542
|
|
|
445
|
-
const json = (await response.json())
|
|
446
|
-
found: boolean;
|
|
447
|
-
value?: string;
|
|
448
|
-
unreachable?: boolean;
|
|
449
|
-
};
|
|
543
|
+
const json = SecretReadResponseSchema.parse(await response.json());
|
|
450
544
|
// The daemon's /v1/secrets/read returns `unreachable: true` when the
|
|
451
545
|
// credential backend (CES) can't be reached. Respect that signal.
|
|
452
546
|
if (json.unreachable) {
|
|
@@ -553,14 +647,14 @@ export async function hatchAssistant(
|
|
|
553
647
|
});
|
|
554
648
|
|
|
555
649
|
if (response.ok) {
|
|
556
|
-
const assistant = (await response.json())
|
|
650
|
+
const assistant = HatchedAssistantSchema.parse(await response.json());
|
|
557
651
|
return { assistant, reusedExisting: response.status === 200 };
|
|
558
652
|
}
|
|
559
653
|
|
|
560
654
|
if (response.status === 401 || response.status === 403) {
|
|
561
|
-
const detail =
|
|
562
|
-
|
|
563
|
-
|
|
655
|
+
const detail = PlatformErrorDetailSchema
|
|
656
|
+
.catch({})
|
|
657
|
+
.parse(await response.json().catch(() => ({})));
|
|
564
658
|
throw new Error(
|
|
565
659
|
detail.detail ??
|
|
566
660
|
"Invalid or expired token. Run `cadet login` to re-authenticate.",
|
|
@@ -571,9 +665,9 @@ export async function hatchAssistant(
|
|
|
571
665
|
throw new Error("Insufficient balance to hatch a new assistant.");
|
|
572
666
|
}
|
|
573
667
|
|
|
574
|
-
const errorBody =
|
|
575
|
-
|
|
576
|
-
|
|
668
|
+
const errorBody = PlatformErrorDetailSchema
|
|
669
|
+
.catch({})
|
|
670
|
+
.parse(await response.json().catch(() => ({})));
|
|
577
671
|
throw new Error(
|
|
578
672
|
errorBody.detail ??
|
|
579
673
|
`Platform API error: ${response.status} ${response.statusText}`,
|
|
@@ -614,11 +708,9 @@ export async function checkExistingPlatformAssistant(
|
|
|
614
708
|
return null;
|
|
615
709
|
}
|
|
616
710
|
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
const active = body.results?.find((a) => a.status === "active");
|
|
621
|
-
return active ?? null;
|
|
711
|
+
const parsed = HatchedListResponseSchema.safeParse(await response.json());
|
|
712
|
+
const results = parsed.success ? parsed.data.results : undefined;
|
|
713
|
+
return results?.find((a) => a.status === "active") ?? null;
|
|
622
714
|
} finally {
|
|
623
715
|
clearTimeout(timeoutId);
|
|
624
716
|
}
|
|
@@ -649,11 +741,10 @@ export async function fetchPlatformAssistants(
|
|
|
649
741
|
|
|
650
742
|
if (!response.ok) return [];
|
|
651
743
|
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
};
|
|
744
|
+
const parsed = HatchedListResponseSchema.safeParse(await response.json());
|
|
745
|
+
const results = parsed.success ? (parsed.data.results ?? []) : [];
|
|
655
746
|
|
|
656
|
-
return
|
|
747
|
+
return results.filter((a) => a.status === "active");
|
|
657
748
|
} finally {
|
|
658
749
|
clearTimeout(timeoutId);
|
|
659
750
|
}
|
|
@@ -685,10 +776,10 @@ export async function fetchAssistantDetail(
|
|
|
685
776
|
|
|
686
777
|
if (!response.ok) return null;
|
|
687
778
|
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
};
|
|
779
|
+
const parsed = ReleaseChannelResponseSchema.safeParse(
|
|
780
|
+
await response.json(),
|
|
781
|
+
);
|
|
782
|
+
const body = parsed.success ? parsed.data : {};
|
|
692
783
|
return {
|
|
693
784
|
currentReleaseVersion: body.current_release_version ?? null,
|
|
694
785
|
releaseChannel: body.release_channel === "preview" ? "preview" : "stable",
|
|
@@ -720,8 +811,10 @@ export async function fetchUpgradeInProgress(
|
|
|
720
811
|
|
|
721
812
|
if (!response.ok) return null;
|
|
722
813
|
|
|
723
|
-
const
|
|
724
|
-
return
|
|
814
|
+
const parsed = InProgressResponseSchema.safeParse(await response.json());
|
|
815
|
+
return parsed.success && parsed.data.in_progress !== undefined
|
|
816
|
+
? parsed.data.in_progress
|
|
817
|
+
: null;
|
|
725
818
|
} catch {
|
|
726
819
|
return null;
|
|
727
820
|
}
|
|
@@ -733,10 +826,6 @@ export interface PlatformUser {
|
|
|
733
826
|
display: string;
|
|
734
827
|
}
|
|
735
828
|
|
|
736
|
-
interface OrganizationListResponse {
|
|
737
|
-
results: { id: string; name: string }[];
|
|
738
|
-
}
|
|
739
|
-
|
|
740
829
|
export async function fetchOrganizationId(
|
|
741
830
|
token: string,
|
|
742
831
|
platformUrl?: string,
|
|
@@ -762,8 +851,10 @@ export async function fetchOrganizationId(
|
|
|
762
851
|
);
|
|
763
852
|
}
|
|
764
853
|
|
|
765
|
-
const
|
|
766
|
-
|
|
854
|
+
const parsed = OrganizationListResponseSchema.safeParse(
|
|
855
|
+
await response.json(),
|
|
856
|
+
);
|
|
857
|
+
const orgId = parsed.success ? parsed.data.results?.[0]?.id : undefined;
|
|
767
858
|
if (!orgId) {
|
|
768
859
|
throw new Error("No organization found for this account.");
|
|
769
860
|
}
|
|
@@ -773,17 +864,6 @@ export async function fetchOrganizationId(
|
|
|
773
864
|
}
|
|
774
865
|
}
|
|
775
866
|
|
|
776
|
-
interface AllauthSessionResponse {
|
|
777
|
-
status: number;
|
|
778
|
-
data: {
|
|
779
|
-
user: {
|
|
780
|
-
id: string;
|
|
781
|
-
email: string;
|
|
782
|
-
display: string;
|
|
783
|
-
};
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
|
|
787
867
|
export async function fetchCurrentUser(
|
|
788
868
|
token: string,
|
|
789
869
|
platformUrl?: string,
|
|
@@ -816,7 +896,7 @@ export async function fetchCurrentUser(
|
|
|
816
896
|
);
|
|
817
897
|
}
|
|
818
898
|
|
|
819
|
-
const body = (await response.json())
|
|
899
|
+
const body = AllauthSessionResponseSchema.parse(await response.json());
|
|
820
900
|
return body.data.user;
|
|
821
901
|
} finally {
|
|
822
902
|
clearTimeout(timeoutId);
|
|
@@ -843,10 +923,9 @@ export async function rollbackPlatformAssistant(
|
|
|
843
923
|
},
|
|
844
924
|
);
|
|
845
925
|
|
|
846
|
-
const body =
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
};
|
|
926
|
+
const body = VersionCheckResponseSchema.parse(
|
|
927
|
+
await response.json().catch(() => ({})),
|
|
928
|
+
);
|
|
850
929
|
|
|
851
930
|
if (response.status === 200) {
|
|
852
931
|
return { detail: body.detail ?? "", version: body.version ?? null };
|
|
@@ -907,10 +986,9 @@ export async function platformImportPreflightFromGcs(
|
|
|
907
986
|
},
|
|
908
987
|
);
|
|
909
988
|
|
|
910
|
-
const body =
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
>;
|
|
989
|
+
const body = PassthroughBodySchema.parse(
|
|
990
|
+
await response.json().catch(() => ({})),
|
|
991
|
+
);
|
|
914
992
|
return { statusCode: response.status, body };
|
|
915
993
|
}
|
|
916
994
|
|
|
@@ -934,10 +1012,9 @@ export async function platformImportBundleFromGcs(
|
|
|
934
1012
|
throw new Error("Bundle too large to import");
|
|
935
1013
|
}
|
|
936
1014
|
|
|
937
|
-
const body =
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
>;
|
|
1015
|
+
const body = PassthroughBodySchema.parse(
|
|
1016
|
+
await response.json().catch(() => ({})),
|
|
1017
|
+
);
|
|
941
1018
|
return { statusCode: response.status, body };
|
|
942
1019
|
}
|
|
943
1020
|
|
|
@@ -1128,12 +1205,7 @@ export async function platformRequestSignedUrl(
|
|
|
1128
1205
|
}
|
|
1129
1206
|
|
|
1130
1207
|
if (response.status === 201 || response.status === 200) {
|
|
1131
|
-
const json = (await response.json())
|
|
1132
|
-
url: string;
|
|
1133
|
-
bundle_key: string;
|
|
1134
|
-
expires_at: string;
|
|
1135
|
-
max_content_length?: number;
|
|
1136
|
-
};
|
|
1208
|
+
const json = BundleUploadUrlSchema.parse(await response.json());
|
|
1137
1209
|
return {
|
|
1138
1210
|
url: json.url,
|
|
1139
1211
|
bundleKey: json.bundle_key,
|
|
@@ -1145,12 +1217,9 @@ export async function platformRequestSignedUrl(
|
|
|
1145
1217
|
// Non-success body. Read once and reuse for both the 422 version-mismatch
|
|
1146
1218
|
// branch and the generic-error fallthrough — `response.json()` consumes
|
|
1147
1219
|
// the body, so a second read would always return undefined.
|
|
1148
|
-
const errorBody =
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
bundle_compat?: BundleCompatibility;
|
|
1152
|
-
target_runtime_version?: string;
|
|
1153
|
-
};
|
|
1220
|
+
const errorBody = BundleImportErrorSchema.parse(
|
|
1221
|
+
await response.json().catch(() => ({})),
|
|
1222
|
+
);
|
|
1154
1223
|
|
|
1155
1224
|
if (
|
|
1156
1225
|
response.status === 422 &&
|
|
@@ -1197,6 +1266,6 @@ export async function platformPollJobStatus(
|
|
|
1197
1266
|
);
|
|
1198
1267
|
}
|
|
1199
1268
|
|
|
1200
|
-
const raw = (await response.json())
|
|
1269
|
+
const raw = RawUnifiedJobStatusSchema.parse(await response.json());
|
|
1201
1270
|
return parseUnifiedJobStatus(raw);
|
|
1202
1271
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { mkdirSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { join, posix, win32 } from "path";
|
|
4
3
|
|
|
4
|
+
import { getStateDir } from "./environments/paths.js";
|
|
5
|
+
import { getCurrentEnvironment } from "./environments/resolve.js";
|
|
6
|
+
|
|
5
7
|
function isPathInside(
|
|
6
8
|
parent: string,
|
|
7
9
|
candidate: string,
|
|
@@ -17,9 +19,7 @@ function isPathInside(
|
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
export function getRetiredDir(): string {
|
|
20
|
-
const
|
|
21
|
-
process.env.XDG_DATA_HOME?.trim() || join(homedir(), ".local", "share");
|
|
22
|
-
const dir = join(xdgData, "cadet", "retired");
|
|
22
|
+
const dir = join(getStateDir(getCurrentEnvironment()), "retired");
|
|
23
23
|
mkdirSync(dir, { recursive: true });
|
|
24
24
|
return dir;
|
|
25
25
|
}
|
package/src/lib/retire-local.ts
CHANGED
|
@@ -128,34 +128,11 @@ export async function retireLocal(
|
|
|
128
128
|
|
|
129
129
|
// Stop gateway via PID file — use a longer timeout because the gateway has a
|
|
130
130
|
// drain window (5s) before it exits.
|
|
131
|
-
|
|
132
|
-
await stopProcessByPidFile(gatewayPidFile, "gateway", undefined, 7000);
|
|
131
|
+
await stopProcessByPidFile(join(cadetDir, "gateway.pid"), "gateway", undefined, 7000);
|
|
133
132
|
|
|
134
133
|
// Stop the CES sibling — it is stopped by its PID file, a no-op when the
|
|
135
134
|
// PID file is absent (e.g. the sibling was never started or already exited).
|
|
136
|
-
|
|
137
|
-
const cesStopped = await stopProcessByPidFile(
|
|
138
|
-
cesPidFile,
|
|
139
|
-
"credential-executor",
|
|
140
|
-
);
|
|
141
|
-
if (cesStopped) {
|
|
142
|
-
reporter.log("credential-executor stopped.");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Stop Qdrant. The daemon's graceful shutdown tries to stop it via
|
|
146
|
-
// qdrantManager.stop(), but a daemon that never completes that shutdown
|
|
147
|
-
// (crash, OOM kill, or the SIGKILL ceiling above) leaves Qdrant running as
|
|
148
|
-
// an orphan. Check both the current PID file location and the legacy one.
|
|
149
|
-
const qdrantPidFile = join(
|
|
150
|
-
cadetDir,
|
|
151
|
-
"workspace",
|
|
152
|
-
"data",
|
|
153
|
-
"qdrant",
|
|
154
|
-
"qdrant.pid",
|
|
155
|
-
);
|
|
156
|
-
const qdrantLegacyPidFile = join(cadetDir, "qdrant.pid");
|
|
157
|
-
await stopProcessByPidFile(qdrantPidFile, "qdrant", undefined, 5000);
|
|
158
|
-
await stopProcessByPidFile(qdrantLegacyPidFile, "qdrant", undefined, 5000);
|
|
135
|
+
await stopProcessByPidFile(join(cadetDir, "ces.pid"), "credential-executor");
|
|
159
136
|
|
|
160
137
|
// Stop the nginx ingress if one is fronting this gateway — it would
|
|
161
138
|
// otherwise be orphaned when the instance directory is archived.
|