@mycadet/cli 0.12.9 → 0.12.11

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 (55) hide show
  1. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  2. package/node_modules/@mycadet/assistant-api/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  3. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  4. package/node_modules/@mycadet/local-mode/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  5. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +8 -8
  6. package/node_modules/@mycadet/service-contracts/src/ingress.ts +1 -1
  7. package/package.json +4 -3
  8. package/src/__tests__/client-web-config.test.ts +0 -1
  9. package/src/__tests__/cloud-login-poll.test.ts +69 -0
  10. package/src/__tests__/devices.test.ts +62 -0
  11. package/src/__tests__/ensure-binary.test.ts +52 -0
  12. package/src/__tests__/hatch-next-steps.test.ts +38 -0
  13. package/src/__tests__/multi-local.test.ts +3 -3
  14. package/src/__tests__/nginx-ingress.test.ts +58 -26
  15. package/src/__tests__/pair.test.ts +149 -78
  16. package/src/__tests__/ps-cloud-status.test.ts +6 -4
  17. package/src/__tests__/setup.test.ts +14 -4
  18. package/src/__tests__/teleport.test.ts +0 -15
  19. package/src/__tests__/tunnel-startup-summary.test.ts +110 -0
  20. package/src/__tests__/tunnel-stop.test.ts +226 -0
  21. package/src/__tests__/tunnel.test.ts +265 -70
  22. package/src/__tests__/uninstall.test.ts +140 -8
  23. package/src/commands/client.ts +4 -6
  24. package/src/commands/devices.ts +66 -30
  25. package/src/commands/hatch.ts +14 -53
  26. package/src/commands/pair.ts +147 -38
  27. package/src/commands/ps.ts +10 -20
  28. package/src/commands/teleport.ts +2 -21
  29. package/src/commands/tunnel.ts +67 -28
  30. package/src/commands/uninstall.ts +106 -71
  31. package/src/commands/upgrade.ts +6 -1
  32. package/src/lib/__tests__/local-gateway-cli.test.ts +42 -0
  33. package/src/lib/assistant-config.ts +20 -5
  34. package/src/lib/cloud-client.ts +0 -3
  35. package/src/lib/cloud-login-poll.ts +75 -0
  36. package/src/lib/cloud-login.ts +3 -2
  37. package/src/lib/color.ts +4 -0
  38. package/src/lib/docker.ts +46 -70
  39. package/src/lib/ensure-binary.ts +65 -6
  40. package/src/lib/environments/__tests__/paths.test.ts +27 -1
  41. package/src/lib/environments/paths.ts +16 -8
  42. package/src/lib/environments/resolve.ts +2 -4
  43. package/src/lib/hatch-local.ts +16 -59
  44. package/src/lib/hatch-next-steps.ts +18 -9
  45. package/src/lib/live-voice/tunnel.ts +2 -2
  46. package/src/lib/local.ts +42 -9
  47. package/src/lib/nginx-ingress.ts +124 -24
  48. package/src/lib/step-runner.ts +6 -1
  49. package/src/lib/tunnel.ts +86 -34
  50. package/src/lib/tunnels/cadet.ts +12 -1
  51. package/src/lib/tunnels/cloudflare.ts +36 -1
  52. package/src/lib/tunnels/ngrok.ts +16 -2
  53. package/src/lib/tunnels/process-lifecycle.ts +26 -0
  54. package/src/lib/tunnels/tailscale.ts +79 -1
  55. package/src/lib/xdg-log.ts +11 -0
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, renameSync, rmSync, unlinkSync } from "fs";
2
- import { basename, dirname, join } from "path";
2
+ import { basename, dirname, join, sep } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
 
5
5
  import {
@@ -14,8 +14,8 @@ import {
14
14
  import { retireDocker } from "../lib/docker.js";
15
15
  import {
16
16
  getConfigDirs,
17
- getInputHistoryPath,
18
17
  getLogDir,
18
+ getSharedStateDir,
19
19
  getStateDir,
20
20
  } from "../lib/environments/paths.js";
21
21
  import { getCurrentEnvironment } from "../lib/environments/resolve.js";
@@ -98,62 +98,84 @@ function findInstalledPackageRoot(startDir: string): string | null {
98
98
  return null;
99
99
  }
100
100
 
101
+ /** The CLI's own install location, when it can be deleted. */
102
+ interface SelfDeleteTarget {
103
+ path: string;
104
+ description: string;
105
+ }
106
+
101
107
  /**
102
- * Remove the running CLI. Handles the two real install shapes:
103
- * - the compiled `cadet` binary (process.execPath IS the install)
104
- * - a global package install (bun install -g mycadet / npm install -g)
105
- * Under a source checkout or a bare runtime (bun/node), nothing is deleted —
106
- * the runtime binary must never be a deletion target.
108
+ * Where this CLI is running from, when that is an INSTALL we can remove:
109
+ * the compiled `cadet` binary (process.execPath IS the install) or a global
110
+ * package install (bun install -g mycadet / npm install -g mycadet). Under
111
+ * a source checkout or a bare runtime (bun/node), null — the runtime binary
112
+ * must never be a deletion target. The uninstall prompt lists the same
113
+ * target it later deletes, so both share this resolver.
107
114
  */
108
- function selfDelete(): void {
115
+ function resolveSelfDeleteTarget(): SelfDeleteTarget | null {
109
116
  const execPath = process.execPath;
110
117
  const execBase = basename(execPath);
111
118
  if (execBase === "cadet" || execBase === "cadet.exe") {
119
+ return { path: execPath, description: "the Cadet executable" };
120
+ }
121
+ const runningFile = fileURLToPath(import.meta.url);
122
+ const pkgRoot = findInstalledPackageRoot(dirname(runningFile));
123
+ if (pkgRoot) {
124
+ return { path: pkgRoot, description: "the installed Cadet CLI package" };
125
+ }
126
+ return null;
127
+ }
128
+
129
+ /**
130
+ * Delete the install {@link resolveSelfDeleteTarget} found. The two shapes
131
+ * delete differently — the running binary needs the win32 rename dance, a
132
+ * package dir is a plain recursive delete — and the bin symlinks the
133
+ * package manager planted (e.g. ~/.bun/bin/cadet, the npm global bin) are
134
+ * left dangling — say so.
135
+ */
136
+ function selfDelete(): void {
137
+ const target = resolveSelfDeleteTarget();
138
+ if (!target) {
139
+ console.log(
140
+ "Cadet is running from a source checkout — skipping self-removal.",
141
+ );
142
+ console.log(
143
+ "Remove the installed copy with `bun remove -g mycadet` or `npm uninstall -g mycadet`.",
144
+ );
145
+ return;
146
+ }
147
+ const execBase = basename(target.path);
148
+ const isCompiledBinary = execBase === "cadet" || execBase === "cadet.exe";
149
+ if (isCompiledBinary) {
112
150
  try {
113
151
  if (process.platform === "win32") {
114
152
  // Windows refuses to unlink a running executable — stage it under a
115
153
  // junk name and delete that.
116
- const stale = `${execPath}.uninstalled`;
117
- renameSync(execPath, stale);
154
+ const stale = `${target.path}.uninstalled`;
155
+ renameSync(target.path, stale);
118
156
  rmSync(stale, { force: true, recursive: true });
119
157
  } else {
120
- unlinkSync(execPath);
158
+ unlinkSync(target.path);
121
159
  }
122
- console.log(`Removed ${execPath}.`);
123
- return;
160
+ console.log(`Removed ${target.path}.`);
124
161
  } catch (err) {
125
162
  console.warn(
126
- `⚠️ Could not remove ${execPath}: ${
163
+ `⚠️ Could not remove ${target.path}: ${
127
164
  err instanceof Error ? err.message : String(err)
128
165
  } — remove it by hand.`,
129
166
  );
130
- return;
131
167
  }
132
- }
133
-
134
- // Package-manager install: remove the package directory we are running
135
- // from, best-effort. Bin symlinks the package manager planted (e.g.
136
- // ~/.bun/bin/cadet, the npm global bin) are left dangling — say so.
137
- const runningFile = fileURLToPath(import.meta.url);
138
- const pkgRoot = findInstalledPackageRoot(dirname(runningFile));
139
- if (!pkgRoot) {
140
- console.log(
141
- "Cadet is running from a source checkout — skipping self-removal.",
142
- );
143
- console.log(
144
- "Remove the installed copy with `bun remove -g mycadet` or `npm uninstall -g mycadet`.",
145
- );
146
168
  return;
147
169
  }
148
170
  try {
149
- rmSync(pkgRoot, { recursive: true, force: true });
150
- console.log(`Removed ${pkgRoot}.`);
171
+ rmSync(target.path, { recursive: true, force: true });
172
+ console.log(`Removed ${target.path}.`);
151
173
  console.log(
152
174
  "The `cadet` command on your PATH is now stale — remove it if your package manager didn't (`bun remove -g mycadet` or `npm uninstall -g mycadet`).",
153
175
  );
154
176
  } catch (err) {
155
177
  console.warn(
156
- `⚠️ Could not remove ${pkgRoot}: ${
178
+ `⚠️ Could not remove ${target.path}: ${
157
179
  err instanceof Error ? err.message : String(err)
158
180
  } — remove it with \`bun remove -g mycadet\` (or \`npm uninstall -g mycadet\`).`,
159
181
  );
@@ -173,10 +195,24 @@ function collectDeleteTargets(env = getCurrentEnvironment()): DeleteTarget[] {
173
195
  { path: getLogDir(env), description: "logs" },
174
196
  // Not environment-scoped: deleting it affects every cadet CLI on the
175
197
  // machine — called out in the prompt, not hidden.
176
- { path: getInputHistoryPath(), description: "shared terminal input history" },
198
+ {
199
+ path: getSharedStateDir(),
200
+ description: "shared state, including the terminal input history",
201
+ },
177
202
  ];
178
203
  // Only targets that exist — the prompt lists what will actually go.
179
- return targets.filter((t) => existsSync(t.path));
204
+ const existing = targets.filter((t) => existsSync(t.path));
205
+ // The log dir lives INSIDE a listed target (the config dir on
206
+ // macOS/Linux, the state dir on Windows) and goes with its parent's
207
+ // recursive delete — never list a path twice.
208
+ const deduped: DeleteTarget[] = [];
209
+ for (const t of existing) {
210
+ const covered = deduped.some(
211
+ (k) => t.path === k.path || t.path.startsWith(k.path + sep),
212
+ );
213
+ if (!covered) deduped.push(t);
214
+ }
215
+ return deduped;
180
216
  }
181
217
 
182
218
  function rmrf(target: string): void {
@@ -190,63 +226,48 @@ export const uninstall = Object.assign(
190
226
  console.log("Usage: cadet uninstall [--yes]");
191
227
  console.log("");
192
228
  console.log(
193
- "Remove Cadet entirely: delete every location it uses for this environment (assistant state, retired archives, config, logs) and then delete the executable itself.",
229
+ "Remove Cadet entirely: retire any remaining assistants, delete every location it uses for this environment (assistant state, retired archives, config, logs), and remove the Cadet installation itself.",
194
230
  );
195
231
  process.exit(0);
196
232
  }
197
233
  const yes = args.includes("--yes");
198
234
 
199
- // Assistants still on the rack are retired — with their data deleted —
200
- // as part of the uninstall. Uninstall deletes every location cadet uses
201
- // INCLUDING the retired archives, so retiring first (which archives)
202
- // only matters for the teardown it performs: docker assistants keep
203
- // containers, volumes, and a network in docker that the filesystem
204
- // sweep below never reaches. A warning, not a refusal (v0.0.10 run
205
- // feedback: the hard error just blocked the uninstall).
235
+ const env = getCurrentEnvironment();
236
+ const envLabel = env.name ?? "production";
206
237
  const existing = loadAllAssistants();
238
+ const targets = collectDeleteTargets(env);
239
+ const selfTarget = resolveSelfDeleteTarget();
240
+
241
+ console.log(
242
+ `This removes Cadet entirely for the '${envLabel}' environment and cannot be undone.`,
243
+ );
244
+
245
+ // Assistants still on the rack are retired — with their data deleted —
246
+ // as part of the uninstall, and retiring happens only AFTER the
247
+ // confirmation below: it is part of what the prompt describes, never a
248
+ // side effect of running the command. Uninstall deletes every location
249
+ // cadet uses INCLUDING the retired archives, so retiring first (which
250
+ // archives) only matters for the teardown it performs: docker
251
+ // assistants keep containers, volumes, and a network in docker that
252
+ // the filesystem sweep below never reaches. A warning, not a refusal
253
+ // (v0.0.10 run feedback: the hard error just blocked the uninstall).
207
254
  if (existing.length > 0) {
208
255
  console.warn(
209
256
  `\n⚠️ ${existing.length} ${existing.length === 1 ? "assistant still exists" : "assistants still exist"}: ${existing.map((a) => a.assistantId).join(", ")}`,
210
257
  );
211
258
  console.warn(
212
- " They will be retired now — their data (and any retired archives) will be permanently deleted by this uninstall.",
259
+ " They will be retired as part of this uninstall — their data (and any retired archives) will be permanently deleted.",
213
260
  );
214
- for (const entry of existing) {
215
- if (entry.cloud === "apple-container" || entry.cloud === "paired") {
216
- console.log(` Skipping '${entry.assistantId}' on ${entry.cloud} — its lifecycle is managed elsewhere.`);
217
- continue;
218
- }
219
- try {
220
- await retireAssistantData(entry);
221
- } catch (err) {
222
- console.error(
223
- ` Warning: could not fully retire '${entry.assistantId}': ${err instanceof Error ? err.message : String(err)}`,
224
- );
225
- }
226
- }
227
261
  }
228
262
 
229
- const env = getCurrentEnvironment();
230
- const envLabel = env.name ?? "production";
231
- const targets = collectDeleteTargets(env);
232
- const execPath = process.execPath;
233
-
234
- console.log(
235
- `This removes Cadet entirely for the '${envLabel}' environment and cannot be undone.`,
236
- );
237
- if (targets.length === 0) {
263
+ if (targets.length === 0 && selfTarget === null) {
238
264
  console.log("No Cadet data directories found to delete.");
239
265
  } else {
240
266
  console.log("It will permanently delete:");
241
- for (const t of targets) {
267
+ for (const t of selfTarget ? [...targets, selfTarget] : targets) {
242
268
  console.log(` - ${t.path} — ${t.description}`);
243
269
  }
244
270
  }
245
- const execBase = basename(execPath);
246
- const isCompiledBinary = execBase === "cadet" || execBase === "cadet.exe";
247
- if (isCompiledBinary) {
248
- console.log(`It will then remove the Cadet executable at ${execPath}.`);
249
- }
250
271
 
251
272
  if (!yes) {
252
273
  if (!canPromptForConfirmation()) {
@@ -265,6 +286,20 @@ export const uninstall = Object.assign(
265
286
  }
266
287
  }
267
288
 
289
+ for (const entry of existing) {
290
+ if (entry.cloud === "apple-container" || entry.cloud === "paired") {
291
+ console.log(` Skipping '${entry.assistantId}' on ${entry.cloud} — its lifecycle is managed elsewhere.`);
292
+ continue;
293
+ }
294
+ try {
295
+ await retireAssistantData(entry);
296
+ } catch (err) {
297
+ console.error(
298
+ ` Warning: could not fully retire '${entry.assistantId}': ${err instanceof Error ? err.message : String(err)}`,
299
+ );
300
+ }
301
+ }
302
+
268
303
  for (const t of targets) {
269
304
  rmrf(t.path);
270
305
  console.log(`Deleted ${t.path}.`);
@@ -76,7 +76,10 @@ import {
76
76
  startLocalDaemon,
77
77
  stopLocalProcesses,
78
78
  } from "../lib/local.js";
79
- import { restoreTunnelEdgeAndAutoTunnel } from "../lib/tunnel.js";
79
+ import {
80
+ restoreContainerTunnelEdge,
81
+ restoreTunnelEdgeAndAutoTunnel,
82
+ } from "../lib/tunnel.js";
80
83
  import {
81
84
  leaseGuardianToken,
82
85
  resetGuardianBootstrap,
@@ -516,6 +519,8 @@ async function upgradeDocker(
516
519
  preUpgradeBackupPath: backupPath ?? undefined,
517
520
  };
518
521
  saveAssistantEntry(updatedEntry);
522
+ // Refresh the host-served web bundle before clients reload on completion.
523
+ await restoreContainerTunnelEdge(updatedEntry);
519
524
 
520
525
  // Notify clients on the new service group that the upgrade succeeded.
521
526
  await broadcastUpgradeEvent(
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import type { LocalInstanceResources } from "../assistant-config.js";
4
+ import { gatewayCliInvocation } from "../local.js";
5
+
6
+ function fakeResources(instanceDir: string): LocalInstanceResources {
7
+ // gatewayCliInvocation only reads instanceDir; the rest of the resources
8
+ // shape is irrelevant here.
9
+ return { instanceDir } as unknown as LocalInstanceResources;
10
+ }
11
+
12
+ describe("gatewayCliInvocation", () => {
13
+ test("runs the gateway package's CLI entry through bun", () => {
14
+ const invocation = gatewayCliInvocation();
15
+ expect(invocation.args[0]).toBe("run");
16
+ expect(invocation.args[1]).toMatch(/gateway\/src\/cli\/index\.ts$/);
17
+ });
18
+
19
+ test("pins CADET_WORKSPACE_DIR to the instance dir for named instances", () => {
20
+ const invocation = gatewayCliInvocation(
21
+ fakeResources("/data/assistants/testy"),
22
+ );
23
+ expect(invocation.env.CADET_WORKSPACE_DIR).toBe(
24
+ "/data/assistants/testy/workspace",
25
+ );
26
+ });
27
+
28
+ test("pins GATEWAY_SECURITY_DIR to the instance dir for named instances", () => {
29
+ const invocation = gatewayCliInvocation(
30
+ fakeResources("/data/assistants/testy"),
31
+ );
32
+ expect(invocation.env.GATEWAY_SECURITY_DIR).toBe(
33
+ "/data/assistants/testy/gateway-security",
34
+ );
35
+ });
36
+
37
+ test("leaves the workspace resolution untouched for the default instance", () => {
38
+ delete process.env.CADET_WORKSPACE_DIR;
39
+ const invocation = gatewayCliInvocation();
40
+ expect(invocation.env.CADET_WORKSPACE_DIR).toBeUndefined();
41
+ });
42
+ });
@@ -170,6 +170,10 @@ export interface RootProcessRecord {
170
170
  /** The assistant the process fronts, so `cadet ps` can nest the row. */
171
171
  assistantId?: string;
172
172
  startedAt: string;
173
+ /** Listening port for the nginx edge. */
174
+ port?: number;
175
+ /** Managed process temporarily displaced by a pairing-only tunnel. */
176
+ previous?: RootProcessRecord;
173
177
  }
174
178
 
175
179
  export type RootProcessKind = "tunnel" | "app";
@@ -247,12 +251,21 @@ export function stampRootProcess(
247
251
  });
248
252
  }
249
253
 
250
- /** Drop the recorded root process entry, e.g. once the process is stopped. */
251
- export function clearRootProcess(kind: RootProcessKind): void {
254
+ /** Release a recorded process, preserving a replacement or restoring a displaced record. */
255
+ export function clearRootProcess(
256
+ kind: RootProcessKind,
257
+ expectedPid?: number,
258
+ ): void {
252
259
  withCliLockfileLock(() => {
253
260
  const data = readLockfile();
254
- if (data[kind] === undefined) return;
255
- delete data[kind];
261
+ const record = data[kind];
262
+ if (
263
+ record === undefined ||
264
+ (expectedPid !== undefined && record.pid !== expectedPid)
265
+ )
266
+ return;
267
+ if (record.previous) data[kind] = record.previous;
268
+ else delete data[kind];
256
269
  writeLockfileUnlocked(data);
257
270
  });
258
271
  }
@@ -442,7 +455,9 @@ export function loadLatestAssistant(): AssistantEntry | null {
442
455
  }
443
456
 
444
457
  export function findAssistantByName(name: string): AssistantEntry | null {
445
- return loadAllAssistants().find((entry) => entry.assistantId === name) ?? null;
458
+ return (
459
+ loadAllAssistants().find((entry) => entry.assistantId === name) ?? null
460
+ );
446
461
  }
447
462
 
448
463
  export function getAssistantDisplayName(entry: AssistantEntry): string {
@@ -604,7 +604,6 @@ export interface CredentialInjectionParams {
604
604
  bearerToken?: string;
605
605
  assistantApiKey?: string | null;
606
606
  cloudAssistantId: string;
607
- cloudBaseUrl: string;
608
607
  organizationId: string;
609
608
  userId?: string;
610
609
  webhookSecret?: string | null;
@@ -631,8 +630,6 @@ export async function injectCredentialsIntoAssistant(
631
630
  promises.push(inject("cadet:assistant_api_key", params.assistantApiKey));
632
631
  }
633
632
 
634
- promises.push(inject("cadet:platform_base_url", params.cloudBaseUrl));
635
-
636
633
  if (params.webhookSecret) {
637
634
  promises.push(inject("cadet:webhook_secret", params.webhookSecret));
638
635
  }
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ import { loopbackSafeFetch } from "./loopback-fetch.js";
3
+
4
+ const requestSchema = z.object({
5
+ request_id: z.string().regex(/^[a-f0-9]{64}$/),
6
+ poll_secret: z.string().regex(/^[a-f0-9]{64}$/),
7
+ verification_uri: z.string().url(),
8
+ user_code: z.string().min(1),
9
+ interval: z.number().min(1).max(10),
10
+ });
11
+ const resultSchema = z.object({ session_token: z.string().min(1) });
12
+
13
+ /** Browser approval and polling work when the terminal and browser are on different hosts. */
14
+ export async function pollingCloudLogin(
15
+ cloudUrl: string,
16
+ options: {
17
+ timeoutMs?: number;
18
+ fetch?: (
19
+ input: string | URL | Request,
20
+ init?: RequestInit,
21
+ ) => Promise<Response>;
22
+ log?: (message: string) => void;
23
+ } = {},
24
+ ): Promise<string> {
25
+ const request = options.fetch ?? loopbackSafeFetch;
26
+ const log = options.log ?? console.log;
27
+ const signal = AbortSignal.timeout(options.timeoutMs ?? 300_000);
28
+ const origin = new URL(cloudUrl).origin;
29
+ try {
30
+ const started = await request(`${origin}/v1/auth/cli/start`, {
31
+ method: "POST",
32
+ signal,
33
+ });
34
+ if (!started.ok)
35
+ throw new Error(`Could not start terminal login (${started.status}).`);
36
+ const login = requestSchema.parse(await started.json());
37
+ log(`Open this URL to log in: ${login.verification_uri}`);
38
+ log(
39
+ `Confirm code ${login.user_code} in your browser. Waiting up to 5 minutes...`,
40
+ );
41
+ while (!signal.aborted) {
42
+ const response = await request(`${origin}/v1/auth/cli/poll`, {
43
+ method: "POST",
44
+ signal,
45
+ headers: { "Content-Type": "application/json" },
46
+ body: JSON.stringify({
47
+ request_id: login.request_id,
48
+ poll_secret: login.poll_secret,
49
+ }),
50
+ });
51
+ if (response.status === 200)
52
+ return resultSchema.parse(await response.json()).session_token;
53
+ if (response.status === 410)
54
+ throw new Error("Login request expired. Run cadet login again.");
55
+ if (response.status !== 202)
56
+ throw new Error(`Login polling failed (${response.status}).`);
57
+ await new Promise<void>((resolve, reject) => {
58
+ const abort = () => {
59
+ clearTimeout(timer);
60
+ reject(new Error("Login timed out after 5 minutes."));
61
+ };
62
+ const timer = setTimeout(() => {
63
+ signal.removeEventListener("abort", abort);
64
+ resolve();
65
+ }, login.interval * 1000);
66
+ signal.addEventListener("abort", abort, { once: true });
67
+ if (signal.aborted) abort();
68
+ });
69
+ }
70
+ throw new Error("Login timed out after 5 minutes.");
71
+ } catch (error) {
72
+ if (signal.aborted) throw new Error("Login timed out after 5 minutes.");
73
+ throw error;
74
+ }
75
+ }
@@ -6,6 +6,7 @@
6
6
  * login when the user picks that provider).
7
7
  */
8
8
 
9
+ import { pollingCloudLogin } from "./cloud-login-poll.js";
9
10
  import { randomBytes } from "crypto";
10
11
  import { createServer } from "http";
11
12
  import type { AddressInfo } from "net";
@@ -37,7 +38,7 @@ import {
37
38
  generatePkcePair,
38
39
  } from "./workos-pkce.js";
39
40
 
40
- const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
41
+ const LOGIN_TIMEOUT_MS = 300_000;
41
42
 
42
43
  function escapeHtml(s: string): string {
43
44
  return s
@@ -258,6 +259,7 @@ export function startLoopbackListener(
258
259
 
259
260
  /** App-held WorkOS PKCE login */
260
261
  export async function workosPkceLogin(cloudUrl: string): Promise<string> {
262
+ if (!process.stdin.isTTY) return pollingCloudLogin(cloudUrl);
261
263
  const clientId = await fetchWorkosClientId(cloudUrl);
262
264
  const { verifier, challenge } = generatePkcePair();
263
265
  const state = randomBytes(32).toString("hex");
@@ -409,7 +411,6 @@ export async function registerLocalAssistantWithPlatform(
409
411
  bearerToken: options.bearerToken ?? entry.bearerToken,
410
412
  assistantApiKey,
411
413
  cloudAssistantId: registration.assistant.id,
412
- cloudBaseUrl: cloudUrl,
413
414
  organizationId: orgId,
414
415
  userId: user.id,
415
416
  webhookSecret: registration.webhook_secret,
package/src/lib/color.ts CHANGED
@@ -14,3 +14,7 @@ export function red(text: string, stream?: NodeJS.WriteStream): string {
14
14
  export function yellow(text: string, stream?: NodeJS.WriteStream): string {
15
15
  return colorize("\x1b[33m", text, stream);
16
16
  }
17
+
18
+ export function green(text: string, stream?: NodeJS.WriteStream): string {
19
+ return colorize("\x1b[32m", text, stream);
20
+ }