@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/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
- * an ephemeral key each time and passes it as `ACTOR_TOKEN_SIGNING_KEY`
560
- * to both processes — the daemon and gateway each persist it on their
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
- resources.instanceDir,
607
- "workspace",
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> { writeAssistantWrapper(resources);
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
- resources.instanceDir,
1866
- "workspace",
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
- interface NgrokTunnel {
21
- public_url: string;
22
- config?: { addr?: string };
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
- interface NgrokTunnelsResponse {
26
- tunnels: NgrokTunnel[];
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 data = (await res.json()) as NgrokTunnelsResponse;
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 as Record<string, unknown> | undefined;
201
- if (telegram?.botUsername) return true;
202
- const twilio = config.twilio as Record<string, unknown> | undefined;
203
- if (twilio?.accountSid || twilio?.phoneNumber) return true;
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 ingress = config.ingress as Record<string, unknown> | undefined;
230
- const publicBaseUrl = ingress?.publicBaseUrl;
231
- if (!publicBaseUrl || typeof publicBaseUrl !== "string") return false;
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()) as EnsureRegistrationResponse;
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()) as ReprovisionApiKeyResponse;
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()) as {
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()) as HatchedAssistant;
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 = (await response.json().catch(() => ({}))) as {
562
- detail?: string;
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 = (await response.json().catch(() => ({}))) as {
575
- detail?: string;
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 body = (await response.json()) as {
618
- results?: HatchedAssistant[];
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 body = (await response.json()) as {
653
- results?: HatchedAssistant[];
654
- };
744
+ const parsed = HatchedListResponseSchema.safeParse(await response.json());
745
+ const results = parsed.success ? (parsed.data.results ?? []) : [];
655
746
 
656
- return (body.results ?? []).filter((a) => a.status === "active");
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 body = (await response.json()) as {
689
- current_release_version?: string | null;
690
- release_channel?: string;
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 body = (await response.json()) as { in_progress?: boolean };
724
- return typeof body.in_progress === "boolean" ? body.in_progress : null;
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 body = (await response.json()) as OrganizationListResponse;
766
- const orgId = body.results?.[0]?.id;
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()) as AllauthSessionResponse;
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 = (await response.json().catch(() => ({}))) as {
847
- detail?: string;
848
- version?: string | null;
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 = (await response.json().catch(() => ({}))) as Record<
911
- string,
912
- unknown
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 = (await response.json().catch(() => ({}))) as Record<
938
- string,
939
- unknown
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()) as {
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 = (await response.json().catch(() => ({}))) as {
1149
- detail?: string;
1150
- reason?: string;
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()) as RawUnifiedJobStatus;
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 xdgData =
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
  }
@@ -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
- const gatewayPidFile = join(cadetDir, "gateway.pid");
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
- const cesPidFile = join(cadetDir, "ces.pid");
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.