@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
@@ -24,6 +24,22 @@ import {
24
24
  import type { AssistantEntry } from "./assistant-config.js";
25
25
  import { loopbackSafeFetch } from "./loopback-fetch.js";
26
26
  import { authHeaders, invalidateOrgIdCache } from "./platform-client.js";
27
+ import { z } from "zod";
28
+
29
+ const ManagedBackupEntrySchema = z.object({
30
+ snapshot_name: z.string().optional(),
31
+ created_at: z.string().optional(),
32
+ ready_to_use: z.boolean().optional(),
33
+ });
34
+
35
+ const ManagedBackupsResponseSchema = z.object({
36
+ backups: z.array(ManagedBackupEntrySchema).optional(),
37
+ });
38
+
39
+ const CreatedBackupResponseSchema = z.object({
40
+ snapshot_name: z.string().optional(),
41
+ ready_to_use: z.boolean().optional(),
42
+ });
27
43
 
28
44
  type PlatformEntry = Pick<AssistantEntry, "runtimeUrl" | "assistantId">;
29
45
 
@@ -72,8 +88,8 @@ async function fetchPlatformBackups(
72
88
  `Cadet Cloud backup list failed ${await errorSuffix(response)}`,
73
89
  );
74
90
  }
75
- const body = (await response.json()) as { backups?: ManagedBackupEntry[] };
76
- return body.backups ?? [];
91
+ const parsed = ManagedBackupsResponseSchema.safeParse(await response.json());
92
+ return parsed.success ? (parsed.data.backups ?? []) : [];
77
93
  }
78
94
 
79
95
  /**
@@ -113,17 +129,15 @@ export async function createPlatformBackup(
113
129
  `Cadet Cloud backup create failed ${await errorSuffix(response)}`,
114
130
  );
115
131
  }
116
- const created = (await response.json().catch(() => null)) as {
117
- snapshot_name?: string;
118
- ready_to_use?: boolean;
119
- } | null;
120
- const snapshotName = created?.snapshot_name;
121
- if (!snapshotName) {
132
+ const body: unknown = await response.json().catch(() => null);
133
+ const created = CreatedBackupResponseSchema.safeParse(body);
134
+ if (!created.success || !created.data.snapshot_name) {
122
135
  throw new Error("Cadet Cloud backup create returned no snapshot name");
123
136
  }
124
- if (created?.ready_to_use === true) {
137
+ if (created.data.ready_to_use === true) {
125
138
  return;
126
139
  }
140
+ const snapshotName = created.data.snapshot_name;
127
141
 
128
142
  const deadline = Date.now() + timeoutMs;
129
143
  while (Date.now() < deadline) {
@@ -6,8 +6,19 @@
6
6
  * the web UI's xterm.js terminal uses.
7
7
  */
8
8
 
9
- import { authHeaders, getPlatformUrl } from "./platform-client.js";
10
9
  import { loopbackSafeFetch } from "./loopback-fetch.js";
10
+ import { authHeaders, getPlatformUrl } from "./platform-client.js";
11
+ import { z } from "zod";
12
+
13
+ const TerminalSessionResponseSchema = z.object({ session_id: z.string() });
14
+
15
+ const TerminalOutputEventSchema = z.object({
16
+ seq: z.number(),
17
+ /** Base64-encoded PTY output bytes. */
18
+ data: z.string(),
19
+ });
20
+
21
+ export type TerminalOutputEvent = z.infer<typeof TerminalOutputEventSchema>;
11
22
 
12
23
  // ---------------------------------------------------------------------------
13
24
  // Create / Close
@@ -40,7 +51,11 @@ export async function createTerminalSession(
40
51
  `Failed to create terminal session (${response.status}): ${detail || response.statusText}`,
41
52
  );
42
53
  }
43
- return (await response.json()) as { session_id: string };
54
+ const parsed = TerminalSessionResponseSchema.safeParse(await response.json());
55
+ if (!parsed.success) {
56
+ throw new Error("Terminal session response did not include a session_id.");
57
+ }
58
+ return parsed.data;
44
59
  }
45
60
 
46
61
  export async function closeTerminalSession(
@@ -120,12 +135,6 @@ export async function resizeTerminalSession(
120
135
  // SSE event stream
121
136
  // ---------------------------------------------------------------------------
122
137
 
123
- export interface TerminalOutputEvent {
124
- seq: number;
125
- /** Base64-encoded PTY output bytes. */
126
- data: string;
127
- }
128
-
129
138
  /**
130
139
  * Subscribe to the terminal output SSE stream. Yields parsed events as they
131
140
  * arrive. The generator completes when the stream ends or is aborted.
@@ -170,7 +179,10 @@ export async function* subscribeTerminalEvents(
170
179
  const trimmed = line.trimEnd();
171
180
  if (trimmed.startsWith("data: ")) {
172
181
  try {
173
- yield JSON.parse(trimmed.slice(6)) as TerminalOutputEvent;
182
+ const frame = TerminalOutputEventSchema.safeParse(
183
+ JSON.parse(trimmed.slice(6)),
184
+ );
185
+ if (frame.success) yield frame.data;
174
186
  } catch {
175
187
  // Skip malformed SSE frames
176
188
  }
@@ -14,6 +14,7 @@ import {
14
14
  loadRawConfig,
15
15
  parseGatewayPortFromEntryUrls,
16
16
  } from "./ingress-config.js";
17
+ import { IngressConfigSchema } from "./ngrok.js";
17
18
  import {
18
19
  ensureTunnelEdge,
19
20
  formatEdgeMode,
@@ -33,13 +34,11 @@ export const DOCKER_GATEWAY_READY_TIMEOUT_MS = 5 * 60_000;
33
34
  * enabled with a saved public URL.
34
35
  */
35
36
  function wantsWebIngress(config: Record<string, unknown>): boolean {
36
- const ingress = config.ingress as
37
- | { enabled?: unknown; publicBaseUrl?: unknown }
38
- | undefined;
37
+ const parsed = IngressConfigSchema.safeParse(config.ingress);
39
38
  return (
40
- ingress?.enabled === true &&
41
- typeof ingress.publicBaseUrl === "string" &&
42
- ingress.publicBaseUrl.trim() !== ""
39
+ parsed.success &&
40
+ parsed.data.enabled === true &&
41
+ (parsed.data.publicBaseUrl?.trim() ?? "") !== ""
43
42
  );
44
43
  }
45
44
 
@@ -189,17 +188,16 @@ function ownsSharedIngress(
189
188
  ) {
190
189
  return false;
191
190
  }
192
- let publicBaseUrl: unknown;
191
+ let publicBaseUrl: string | undefined;
193
192
  try {
194
- publicBaseUrl = (
195
- loadRawConfig(workspaceDir).ingress as
196
- | { publicBaseUrl?: unknown }
197
- | undefined
198
- )?.publicBaseUrl;
193
+ const parsed = IngressConfigSchema.safeParse(
194
+ loadRawConfig(workspaceDir).ingress,
195
+ );
196
+ publicBaseUrl = parsed.success ? parsed.data.publicBaseUrl : undefined;
199
197
  } catch {
200
198
  return true;
201
199
  }
202
- if (typeof publicBaseUrl !== "string" || !publicBaseUrl.trim()) {
200
+ if (!publicBaseUrl || !publicBaseUrl.trim()) {
203
201
  return true;
204
202
  }
205
203
  if (entry.ingressUrl === publicBaseUrl) {
@@ -25,6 +25,16 @@ import { getCurrentEnvironment } from "./environments/resolve.js";
25
25
  import { loadGuardianToken } from "./guardian-token.js";
26
26
  import { classifyReadyzResponse } from "./http-client.js";
27
27
  import { fetchReleases, resolveImageRefs } from "./platform-releases.js";
28
+ import { z } from "zod";
29
+
30
+ const GatewayHealthSchema = z.object({
31
+ version: z.string().optional(),
32
+ });
33
+
34
+ const MigrationGateBodySchema = z.object({
35
+ status: z.string().optional(),
36
+ reason: z.string().optional(),
37
+ });
28
38
  import {
29
39
  getBuilderManagedEnvKeys,
30
40
  type DockerStatefulSetSpec,
@@ -289,8 +299,8 @@ export async function fetchCurrentVersion(
289
299
  signal: AbortSignal.timeout(5000),
290
300
  });
291
301
  if (resp.ok) {
292
- const body = (await resp.json()) as { version?: string };
293
- return body.version;
302
+ const parsed = GatewayHealthSchema.safeParse(await resp.json());
303
+ return parsed.success ? parsed.data.version : undefined;
294
304
  }
295
305
  } catch {
296
306
  // Best-effort
@@ -532,9 +542,11 @@ export async function rollbackMigrations(
532
542
  // The daemon's running-migrations 503 also says "starting" but
533
543
  // carries a `reason` field — don't retry against that one (the
534
544
  // rollback stays blocked until migrations leave the running state).
535
- const parsed = JSON.parse(text) as { status?: string; reason?: string };
545
+ const parsed = MigrationGateBodySchema.safeParse(JSON.parse(text));
536
546
  gatewayStarting =
537
- parsed.status === "starting" && parsed.reason === undefined;
547
+ parsed.success &&
548
+ parsed.data.status === "starting" &&
549
+ parsed.data.reason === undefined;
538
550
  } catch {
539
551
  // Non-JSON error body — not the starting gate.
540
552
  }
@@ -5,6 +5,7 @@
5
5
  import crypto from "node:crypto";
6
6
 
7
7
  import { loopbackSafeFetch } from "./loopback-fetch.js";
8
+ import { z } from "zod";
8
9
 
9
10
  const WORKOS_API_BASE_URL = "https://api.workos.com";
10
11
  const PROVIDER_ID = "workos";
@@ -50,13 +51,39 @@ export function buildAuthorizeUrl(options: AuthorizeUrlOptions): string {
50
51
  return url.toString();
51
52
  }
52
53
 
53
- interface HeadlessProviderEntry {
54
- id: string;
55
- name?: string;
56
- client_id?: string;
57
- flows?: string[];
58
- openid_configuration_url?: string;
59
- }
54
+ const HeadlessProviderEntrySchema = z.object({
55
+ id: z.string().optional(),
56
+ name: z.string().optional(),
57
+ client_id: z.string().optional(),
58
+ flows: z.array(z.string()).optional(),
59
+ openid_configuration_url: z.string().optional(),
60
+ });
61
+
62
+ type HeadlessProviderEntry = z.infer<typeof HeadlessProviderEntrySchema>;
63
+
64
+ const WorkosAuthConfigSchema = z.object({
65
+ data: z
66
+ .object({
67
+ socialaccount: z
68
+ .object({
69
+ providers: z.array(HeadlessProviderEntrySchema).optional(),
70
+ })
71
+ .optional(),
72
+ })
73
+ .optional(),
74
+ });
75
+
76
+ const WorkosAccessTokenResponseSchema = z.object({
77
+ access_token: z.string(),
78
+ });
79
+
80
+ const WorkosSessionTokenResponseSchema = z.object({
81
+ meta: z
82
+ .object({
83
+ session_token: z.string(),
84
+ })
85
+ .optional(),
86
+ });
60
87
 
61
88
  /**
62
89
  * Pick the OAuth2 WorkOS provider from the headless config. During the
@@ -83,12 +110,11 @@ export async function fetchWorkosClientId(
83
110
  if (!response.ok) {
84
111
  throw new Error(`Failed to fetch auth config (${response.status})`);
85
112
  }
86
- const body = (await response.json()) as {
87
- data?: { socialaccount?: { providers?: HeadlessProviderEntry[] } };
88
- };
89
- const clientId = selectWorkosClientId(
90
- body.data?.socialaccount?.providers ?? [],
91
- );
113
+ const parsed = WorkosAuthConfigSchema.safeParse(await response.json());
114
+ const providers = parsed.success
115
+ ? (parsed.data.data?.socialaccount?.providers ?? [])
116
+ : [];
117
+ const clientId = selectWorkosClientId(providers);
92
118
  if (!clientId) {
93
119
  throw new Error(
94
120
  "Cadet Cloud does not advertise a token-auth WorkOS provider; cannot start PKCE login.",
@@ -122,11 +148,13 @@ export async function exchangeCodeWithWorkos(options: {
122
148
  `WorkOS code exchange failed (${response.status}): ${body}`,
123
149
  );
124
150
  }
125
- const data = (await response.json()) as { access_token?: string };
126
- if (!data.access_token) {
151
+ const parsed = WorkosAccessTokenResponseSchema.safeParse(
152
+ await response.json(),
153
+ );
154
+ if (!parsed.success) {
127
155
  throw new Error("WorkOS code exchange returned no access token.");
128
156
  }
129
- return data.access_token;
157
+ return parsed.data.access_token;
130
158
  }
131
159
 
132
160
  /** Exchange the WorkOS access token for a platform session token. */
@@ -149,11 +177,14 @@ export async function exchangeAccessTokenForSession(
149
177
  const body = await response.text();
150
178
  throw new Error(`Session exchange failed (${response.status}): ${body}`);
151
179
  }
152
- const data = (await response.json()) as {
153
- meta?: { session_token?: string };
154
- };
155
- if (!data.meta?.session_token) {
180
+ const parsed = WorkosSessionTokenResponseSchema.safeParse(
181
+ await response.json(),
182
+ );
183
+ const sessionToken = parsed.success
184
+ ? parsed.data.meta?.session_token
185
+ : undefined;
186
+ if (!sessionToken) {
156
187
  throw new Error("Session exchange returned no session token.");
157
188
  }
158
- return data.meta.session_token;
189
+ return sessionToken;
159
190
  }