aicomputer 0.1.2

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/dist/index.js ADDED
@@ -0,0 +1,1450 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command7 } from "commander";
5
+ import chalk7 from "chalk";
6
+ import { createRequire } from "module";
7
+ import { basename as basename2 } from "path";
8
+
9
+ // src/commands/access.ts
10
+ import { spawn } from "child_process";
11
+ import { Command } from "commander";
12
+ import chalk2 from "chalk";
13
+ import open from "open";
14
+ import ora from "ora";
15
+
16
+ // src/lib/config.ts
17
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
18
+ import { homedir } from "os";
19
+ import { join } from "path";
20
+ var CONFIG_DIR = join(homedir(), ".computer");
21
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
22
+ function ensureConfigDir() {
23
+ if (!existsSync(CONFIG_DIR)) {
24
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
25
+ }
26
+ }
27
+ function readConfig() {
28
+ ensureConfigDir();
29
+ if (!existsSync(CONFIG_FILE)) {
30
+ return {};
31
+ }
32
+ try {
33
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
34
+ } catch {
35
+ return {};
36
+ }
37
+ }
38
+ function writeConfig(config) {
39
+ ensureConfigDir();
40
+ const tempFile = `${CONFIG_FILE}.${process.pid}.tmp`;
41
+ writeFileSync(tempFile, JSON.stringify(config, null, 2), { mode: 384 });
42
+ renameSync(tempFile, CONFIG_FILE);
43
+ }
44
+ function getAPIKey() {
45
+ const envValue = process.env.COMPUTER_API_KEY ?? process.env.AGENTCOMPUTER_API_KEY;
46
+ if (envValue) {
47
+ return envValue.trim();
48
+ }
49
+ return getStoredAPIKey();
50
+ }
51
+ function getStoredAPIKey() {
52
+ return readConfig().auth?.apiKey?.trim() || null;
53
+ }
54
+ function hasEnvAPIKey() {
55
+ return Boolean(process.env.COMPUTER_API_KEY ?? process.env.AGENTCOMPUTER_API_KEY);
56
+ }
57
+ function setAPIKey(apiKey) {
58
+ const config = readConfig();
59
+ config.auth = { apiKey: apiKey.trim() };
60
+ writeConfig(config);
61
+ }
62
+ function clearAPIKey() {
63
+ const config = readConfig();
64
+ delete config.auth;
65
+ writeConfig(config);
66
+ }
67
+
68
+ // src/lib/api.ts
69
+ var BASE_URL = process.env.COMPUTER_API_URL ?? process.env.AGENTCOMPUTER_API_URL ?? "https://api.computer.agentcomputer.ai";
70
+ var WEB_URL = process.env.COMPUTER_WEB_URL ?? process.env.AGENTCOMPUTER_WEB_URL ?? resolveDefaultWebURL(BASE_URL);
71
+ var ApiError = class extends Error {
72
+ constructor(status, message) {
73
+ super(message);
74
+ this.status = status;
75
+ this.name = "ApiError";
76
+ }
77
+ };
78
+ function getBaseURL() {
79
+ return BASE_URL;
80
+ }
81
+ function getWebURL() {
82
+ return WEB_URL;
83
+ }
84
+ async function api(path, options = {}) {
85
+ const apiKey = getAPIKey();
86
+ if (!apiKey) {
87
+ throw new ApiError(401, "not logged in; run 'computer login' first");
88
+ }
89
+ return requestWithKey(apiKey, path, options);
90
+ }
91
+ function resolveDefaultWebURL(apiURL) {
92
+ try {
93
+ const parsed = new URL(apiURL);
94
+ if (parsed.hostname === "api.computer.agentcomputer.ai") {
95
+ return "https://agentcomputer.ai";
96
+ }
97
+ if (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") {
98
+ return `${parsed.protocol}//${parsed.hostname}:3000`;
99
+ }
100
+ } catch {
101
+ return "https://agentcomputer.ai";
102
+ }
103
+ return "https://agentcomputer.ai";
104
+ }
105
+ async function apiWithKey(apiKey, path, options = {}) {
106
+ return requestWithKey(apiKey, path, options);
107
+ }
108
+ async function requestWithKey(apiKey, path, options) {
109
+ const headers = {
110
+ Accept: "application/json",
111
+ ...options.headers ?? {}
112
+ };
113
+ if (options.body !== void 0) {
114
+ headers["Content-Type"] = "application/json";
115
+ }
116
+ if (apiKey) {
117
+ headers.Authorization = `Bearer ${apiKey}`;
118
+ }
119
+ const response = await fetch(`${BASE_URL}${path}`, {
120
+ ...options,
121
+ headers
122
+ });
123
+ if (!response.ok) {
124
+ throw new ApiError(response.status, await readErrorMessage(response));
125
+ }
126
+ if (response.status === 204) {
127
+ return void 0;
128
+ }
129
+ return await response.json();
130
+ }
131
+ async function readErrorMessage(response) {
132
+ const contentType = response.headers.get("content-type") ?? "";
133
+ if (contentType.includes("application/json")) {
134
+ try {
135
+ const payload = await response.json();
136
+ if (payload.error) {
137
+ return payload.error;
138
+ }
139
+ return JSON.stringify(payload);
140
+ } catch {
141
+ return response.statusText || "request failed";
142
+ }
143
+ }
144
+ const body = await response.text();
145
+ return body || response.statusText || "request failed";
146
+ }
147
+
148
+ // src/lib/computers.ts
149
+ async function listComputers() {
150
+ const response = await api("/v1/computers");
151
+ return response.computers;
152
+ }
153
+ async function getComputerByID(id) {
154
+ return api(`/v1/computers/${id}`);
155
+ }
156
+ async function createComputer(input) {
157
+ return api("/v1/computers", {
158
+ method: "POST",
159
+ body: JSON.stringify(input)
160
+ });
161
+ }
162
+ async function getConnectionInfo(computerID) {
163
+ return api(`/v1/computers/${computerID}/connection`);
164
+ }
165
+ async function createBrowserAccess(computerID) {
166
+ return api(`/v1/computers/${computerID}/access/browser`, {
167
+ method: "POST"
168
+ });
169
+ }
170
+ async function createTerminalAccess(computerID) {
171
+ return api(`/v1/computers/${computerID}/access/terminal`, {
172
+ method: "POST"
173
+ });
174
+ }
175
+ async function listPublishedPorts(computerID) {
176
+ const response = await api(`/v1/computers/${computerID}/ports`);
177
+ return response.ports;
178
+ }
179
+ async function publishPort(computerID, input) {
180
+ return api(`/v1/computers/${computerID}/ports`, {
181
+ method: "POST",
182
+ body: JSON.stringify(input)
183
+ });
184
+ }
185
+ async function deletePublishedPort(computerID, targetPort) {
186
+ return api(`/v1/computers/${computerID}/ports/${targetPort}`, {
187
+ method: "DELETE"
188
+ });
189
+ }
190
+ async function resolveComputer(identifier) {
191
+ try {
192
+ return await getComputerByID(identifier);
193
+ } catch (error) {
194
+ if (!(error instanceof Error) || !("status" in error)) {
195
+ throw error;
196
+ }
197
+ const status = Reflect.get(error, "status");
198
+ if (status !== 404) {
199
+ throw error;
200
+ }
201
+ }
202
+ const computers = await listComputers();
203
+ const exact = computers.find(
204
+ (computer) => computer.handle === identifier || computer.id === identifier
205
+ );
206
+ if (exact) {
207
+ return exact;
208
+ }
209
+ throw new Error(`computer '${identifier}' not found`);
210
+ }
211
+ function webURL(computer) {
212
+ return `https://${computer.primary_web_host}${normalizePrimaryPath(computer.primary_path)}`;
213
+ }
214
+ function vncURL(computer) {
215
+ if (!computer.vnc_enabled) {
216
+ return null;
217
+ }
218
+ const domain = computer.primary_web_host.replace(/^[^.]+\./, "");
219
+ return `https://6080--${computer.handle}.${domain}/vnc_lite.html`;
220
+ }
221
+ function terminalURL(computer) {
222
+ if (computer.runtime_family !== "managed-worker") {
223
+ return null;
224
+ }
225
+ const domain = computer.primary_web_host.replace(/^[^.]+\./, "");
226
+ return `https://8788--${computer.handle}.${domain}`;
227
+ }
228
+ function normalizePrimaryPath(primaryPath) {
229
+ const trimmed = primaryPath?.trim();
230
+ if (!trimmed) {
231
+ return "/";
232
+ }
233
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
234
+ }
235
+
236
+ // src/lib/ssh-keys.ts
237
+ import { basename } from "path";
238
+ import { homedir as homedir2 } from "os";
239
+ import { readFile } from "fs/promises";
240
+ var DEFAULT_PUBLIC_KEY_PATHS = [
241
+ `${homedir2()}/.ssh/id_ed25519.pub`,
242
+ `${homedir2()}/.ssh/id_ecdsa.pub`,
243
+ `${homedir2()}/.ssh/id_rsa.pub`
244
+ ];
245
+ async function ensureDefaultSSHKeyRegistered() {
246
+ for (const path of DEFAULT_PUBLIC_KEY_PATHS) {
247
+ try {
248
+ const publicKey = (await readFile(path, "utf8")).trim();
249
+ if (!publicKey) {
250
+ continue;
251
+ }
252
+ const key = await api("/v1/ssh-keys", {
253
+ method: "POST",
254
+ body: JSON.stringify({
255
+ name: basename(path),
256
+ public_key: publicKey
257
+ })
258
+ });
259
+ return {
260
+ key,
261
+ publicKeyPath: path,
262
+ privateKeyPath: path.replace(/\.pub$/, "")
263
+ };
264
+ } catch (error) {
265
+ if (error?.code === "ENOENT") {
266
+ continue;
267
+ }
268
+ throw error;
269
+ }
270
+ }
271
+ throw new Error("no SSH public key found in ~/.ssh (looked for id_ed25519.pub, id_ecdsa.pub, id_rsa.pub)");
272
+ }
273
+
274
+ // src/lib/format.ts
275
+ import chalk from "chalk";
276
+ function padEnd(str, len) {
277
+ const visible = str.replace(/\u001b\[[0-9;]*m/g, "");
278
+ return str + " ".repeat(Math.max(0, len - visible.length));
279
+ }
280
+ function timeAgo(dateStr) {
281
+ const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1e3);
282
+ if (seconds < 60) return `${seconds}s ago`;
283
+ const minutes = Math.floor(seconds / 60);
284
+ if (minutes < 60) return `${minutes}m ago`;
285
+ const hours = Math.floor(minutes / 60);
286
+ if (hours < 24) return `${hours}h ago`;
287
+ const days = Math.floor(hours / 24);
288
+ return `${days}d ago`;
289
+ }
290
+ function formatStatus(status) {
291
+ switch (status) {
292
+ case "running":
293
+ return chalk.green(status);
294
+ case "pending":
295
+ case "provisioning":
296
+ case "starting":
297
+ return chalk.yellow(status);
298
+ case "stopping":
299
+ case "stopped":
300
+ case "deleted":
301
+ return chalk.gray(status);
302
+ case "error":
303
+ return chalk.red(status);
304
+ default:
305
+ return status;
306
+ }
307
+ }
308
+
309
+ // src/commands/access.ts
310
+ var openCommand = new Command("open").description("Open a computer in your browser").argument("<id-or-handle>", "Computer id or handle").option("--vnc", "Open VNC desktop instead of gateway").option("--terminal", "Open the terminal surface instead of gateway").action(async (identifier, options) => {
311
+ const spinner = ora("Preparing access...").start();
312
+ try {
313
+ const computer = await resolveComputer(identifier);
314
+ const info = await getConnectionInfo(computer.id);
315
+ if (options.vnc && options.terminal) {
316
+ throw new Error("choose either --vnc or --terminal");
317
+ }
318
+ if (options.vnc) {
319
+ if (!info.connection.vnc_available) {
320
+ throw new Error("VNC is not available for this computer");
321
+ }
322
+ const url = info.connection.vnc_url;
323
+ spinner.succeed(`Opening VNC for ${chalk2.bold(computer.handle)}`);
324
+ await open(url);
325
+ console.log(chalk2.dim(` ${url}`));
326
+ return;
327
+ }
328
+ if (options.terminal) {
329
+ if (!info.connection.terminal_available) {
330
+ throw new Error("Terminal access is not available for this computer");
331
+ }
332
+ const access2 = await createTerminalAccess(computer.id);
333
+ spinner.succeed(`Opening terminal for ${chalk2.bold(computer.handle)}`);
334
+ await open(access2.access_url);
335
+ console.log(chalk2.dim(` ${access2.access_url}`));
336
+ return;
337
+ }
338
+ const access = await createBrowserAccess(computer.id);
339
+ spinner.succeed(`Opening ${chalk2.bold(computer.handle)}`);
340
+ await open(access.access_url);
341
+ console.log(chalk2.dim(` ${access.access_url}`));
342
+ } catch (error) {
343
+ spinner.fail(error instanceof Error ? error.message : "Failed to open computer");
344
+ process.exit(1);
345
+ }
346
+ });
347
+ var sshCommand = new Command("ssh").description("Open an SSH session to a computer").argument("<id-or-handle>", "Computer id or handle").action(async (identifier) => {
348
+ const spinner = ora("Preparing SSH access...").start();
349
+ try {
350
+ const computer = await resolveComputer(identifier);
351
+ const info = await getConnectionInfo(computer.id);
352
+ if (!info.connection.ssh_available) {
353
+ throw new Error("SSH is not available for this computer");
354
+ }
355
+ const registered = await ensureDefaultSSHKeyRegistered();
356
+ spinner.succeed(`Connecting to ${chalk2.bold(computer.handle)}`);
357
+ console.log(
358
+ chalk2.dim(` ssh -p ${info.connection.ssh_port} ${info.connection.ssh_user}@${info.connection.ssh_host}`)
359
+ );
360
+ console.log();
361
+ await runSSH([
362
+ "-i",
363
+ registered.privateKeyPath,
364
+ "-p",
365
+ String(info.connection.ssh_port),
366
+ `${info.connection.ssh_user}@${info.connection.ssh_host}`
367
+ ]);
368
+ } catch (error) {
369
+ spinner.fail(error instanceof Error ? error.message : "Failed to prepare SSH access");
370
+ process.exit(1);
371
+ }
372
+ });
373
+ var portsCommand = new Command("ports").description("Manage published app ports");
374
+ portsCommand.command("ls").description("List published ports for a computer").argument("<id-or-handle>", "Computer id or handle").action(async (identifier) => {
375
+ const spinner = ora("Fetching ports...").start();
376
+ try {
377
+ const computer = await resolveComputer(identifier);
378
+ const ports = await listPublishedPorts(computer.id);
379
+ spinner.stop();
380
+ if (ports.length === 0) {
381
+ console.log();
382
+ console.log(chalk2.dim(" No published ports."));
383
+ console.log();
384
+ return;
385
+ }
386
+ const subWidth = Math.max(10, ...ports.map((p) => p.subdomain.length));
387
+ console.log();
388
+ console.log(
389
+ ` ${chalk2.dim(padEnd("Subdomain", subWidth + 2))}${chalk2.dim(padEnd("Port", 8))}${chalk2.dim("Protocol")}`
390
+ );
391
+ console.log(
392
+ ` ${chalk2.dim("-".repeat(subWidth + 2))}${chalk2.dim("-".repeat(8))}${chalk2.dim("-".repeat(8))}`
393
+ );
394
+ for (const port of ports) {
395
+ const url = `https://${port.subdomain}--${computer.handle}.computer.agentcomputer.ai`;
396
+ console.log(
397
+ ` ${padEnd(port.subdomain, subWidth + 2)}${padEnd(String(port.target_port), 8)}${port.protocol}`
398
+ );
399
+ console.log(
400
+ ` ${chalk2.dim(url)}`
401
+ );
402
+ }
403
+ console.log();
404
+ } catch (error) {
405
+ spinner.fail(error instanceof Error ? error.message : "Failed to fetch ports");
406
+ process.exit(1);
407
+ }
408
+ });
409
+ portsCommand.command("publish").description("Publish an HTTP app port").argument("<id-or-handle>", "Computer id or handle").argument("<port>", "Target port").option("--subdomain <value>", "Custom left-hand host label before --<handle>").option("--protocol <value>", "http or https", "http").action(async (identifier, port, options) => {
410
+ const spinner = ora("Publishing port...").start();
411
+ try {
412
+ const targetPort = Number.parseInt(port, 10);
413
+ if (!Number.isFinite(targetPort)) {
414
+ throw new Error("port must be an integer");
415
+ }
416
+ const computer = await resolveComputer(identifier);
417
+ const published = await publishPort(computer.id, {
418
+ target_port: targetPort,
419
+ subdomain: options.subdomain,
420
+ protocol: options.protocol
421
+ });
422
+ const url = `https://${published.subdomain}--${computer.handle}.computer.agentcomputer.ai`;
423
+ spinner.succeed(`Published port ${chalk2.bold(String(published.target_port))}`);
424
+ console.log(chalk2.dim(` ${url}`));
425
+ } catch (error) {
426
+ spinner.fail(error instanceof Error ? error.message : "Failed to publish port");
427
+ process.exit(1);
428
+ }
429
+ });
430
+ portsCommand.command("rm").description("Unpublish an app port").argument("<id-or-handle>", "Computer id or handle").argument("<port>", "Target port").action(async (identifier, port) => {
431
+ const spinner = ora("Removing port...").start();
432
+ try {
433
+ const targetPort = Number.parseInt(port, 10);
434
+ if (!Number.isFinite(targetPort)) {
435
+ throw new Error("port must be an integer");
436
+ }
437
+ const computer = await resolveComputer(identifier);
438
+ await deletePublishedPort(computer.id, targetPort);
439
+ spinner.succeed(`Removed port ${chalk2.bold(String(targetPort))}`);
440
+ } catch (error) {
441
+ spinner.fail(error instanceof Error ? error.message : "Failed to remove port");
442
+ process.exit(1);
443
+ }
444
+ });
445
+ async function runSSH(args) {
446
+ await new Promise((resolve, reject) => {
447
+ const child = spawn("ssh", args, {
448
+ stdio: "inherit"
449
+ });
450
+ child.on("error", reject);
451
+ child.on("exit", (code) => {
452
+ if (code === 0) {
453
+ resolve();
454
+ return;
455
+ }
456
+ reject(new Error(`ssh exited with code ${code ?? 1}`));
457
+ });
458
+ });
459
+ }
460
+
461
+ // src/commands/computers.ts
462
+ import { Command as Command2 } from "commander";
463
+ import chalk3 from "chalk";
464
+ import ora2 from "ora";
465
+ import { select, input as textInput, confirm } from "@inquirer/prompts";
466
+ function isInternalCondition(value) {
467
+ return /^[A-Z][a-zA-Z]+$/.test(value);
468
+ }
469
+ function printComputer(computer) {
470
+ const vnc = vncURL(computer);
471
+ const terminal = terminalURL(computer);
472
+ const ssh = computer.ssh_enabled ? `ssh -p ${computer.ssh_port} ${computer.handle}@${computer.ssh_host}` : "disabled";
473
+ const isCustom = computer.runtime_family === "custom-machine";
474
+ console.log();
475
+ console.log(` ${chalk3.bold.white(computer.handle)} ${formatStatus(computer.status)}`);
476
+ console.log();
477
+ console.log(` ${chalk3.dim("ID")} ${computer.id}`);
478
+ console.log(` ${chalk3.dim("Tier")} ${computer.tier}`);
479
+ if (isCustom) {
480
+ console.log(` ${chalk3.dim("Runtime")} ${computer.runtime_family}`);
481
+ console.log(` ${chalk3.dim("Source")} ${computer.source_kind}`);
482
+ console.log(` ${chalk3.dim("Image")} ${computer.image_family}`);
483
+ }
484
+ console.log(` ${chalk3.dim("Primary")} :${computer.primary_port}${computer.primary_path}`);
485
+ console.log(` ${chalk3.dim("Health")} ${computer.healthcheck_type}${computer.healthcheck_value ? ` (${computer.healthcheck_value})` : ""}`);
486
+ console.log();
487
+ console.log(` ${chalk3.dim("Gateway")} ${chalk3.cyan(webURL(computer))}`);
488
+ console.log(` ${chalk3.dim("VNC")} ${vnc ? chalk3.cyan(vnc) : chalk3.dim("not available")}`);
489
+ console.log(` ${chalk3.dim("Terminal")} ${terminal ? chalk3.cyan(terminal) : chalk3.dim("not available")}`);
490
+ console.log(` ${chalk3.dim("SSH")} ${computer.ssh_enabled ? chalk3.white(ssh) : chalk3.dim(ssh)}`);
491
+ if (computer.last_error) {
492
+ console.log();
493
+ if (isInternalCondition(computer.last_error)) {
494
+ console.log(` ${chalk3.dim("Condition")} ${chalk3.dim(computer.last_error)}`);
495
+ } else {
496
+ console.log(` ${chalk3.dim("Error")} ${chalk3.red(computer.last_error)}`);
497
+ }
498
+ }
499
+ console.log();
500
+ console.log(` ${chalk3.dim("Created")} ${timeAgo(computer.created_at)}`);
501
+ console.log();
502
+ }
503
+ function printComputerTable(computers) {
504
+ const handleWidth = Math.max(6, ...computers.map((c) => c.handle.length));
505
+ const statusWidth = 12;
506
+ const createdWidth = 10;
507
+ console.log();
508
+ console.log(
509
+ ` ${chalk3.dim(padEnd("Handle", handleWidth + 2))}${chalk3.dim(padEnd("Status", statusWidth + 2))}${chalk3.dim(padEnd("Created", createdWidth + 2))}${chalk3.dim("URL")}`
510
+ );
511
+ console.log(
512
+ ` ${chalk3.dim("-".repeat(handleWidth + 2))}${chalk3.dim("-".repeat(statusWidth + 2))}${chalk3.dim("-".repeat(createdWidth + 2))}${chalk3.dim("-".repeat(20))}`
513
+ );
514
+ for (const computer of computers) {
515
+ const status = formatStatus(computer.status);
516
+ const created = chalk3.dim(timeAgo(computer.created_at));
517
+ console.log(
518
+ ` ${chalk3.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${padEnd(created, createdWidth + 2)}${chalk3.cyan(webURL(computer))}`
519
+ );
520
+ }
521
+ console.log();
522
+ }
523
+ function printComputerTableVerbose(computers) {
524
+ const handleWidth = Math.max(6, ...computers.map((c) => c.handle.length));
525
+ const statusWidth = 10;
526
+ console.log();
527
+ console.log(
528
+ ` ${chalk3.dim(padEnd("Handle", handleWidth + 2))}${chalk3.dim(padEnd("Status", statusWidth + 2))}${chalk3.dim("URLs")}`
529
+ );
530
+ console.log(
531
+ ` ${chalk3.dim("-".repeat(handleWidth + 2))}${chalk3.dim("-".repeat(statusWidth + 2))}${chalk3.dim("-".repeat(20))}`
532
+ );
533
+ for (const computer of computers) {
534
+ const status = formatStatus(computer.status);
535
+ const vnc = vncURL(computer);
536
+ const terminal = terminalURL(computer);
537
+ console.log(
538
+ ` ${chalk3.white(padEnd(computer.handle, handleWidth + 2))}${padEnd(status, statusWidth + 2)}${chalk3.cyan(webURL(computer))}`
539
+ );
540
+ console.log(
541
+ ` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk3.dim(vnc ?? "VNC not available")}`
542
+ );
543
+ console.log(
544
+ ` ${padEnd("", handleWidth + 2)}${padEnd("", statusWidth + 2)}${chalk3.dim(terminal ?? "Terminal not available")}`
545
+ );
546
+ }
547
+ console.log();
548
+ }
549
+ var lsCommand = new Command2("ls").description("List computers").option("--json", "Print raw JSON").option("-v, --verbose", "Show all URLs for each computer").action(async (options) => {
550
+ const spinner = options.json ? null : ora2("Fetching computers...").start();
551
+ try {
552
+ const computers = await listComputers();
553
+ spinner?.stop();
554
+ if (options.json) {
555
+ console.log(JSON.stringify({ computers }, null, 2));
556
+ return;
557
+ }
558
+ if (computers.length === 0) {
559
+ console.log();
560
+ console.log(chalk3.dim(" No computers found."));
561
+ console.log();
562
+ return;
563
+ }
564
+ if (options.verbose) {
565
+ printComputerTableVerbose(computers);
566
+ } else {
567
+ printComputerTable(computers);
568
+ }
569
+ } catch (error) {
570
+ if (spinner) {
571
+ spinner.fail(
572
+ error instanceof Error ? error.message : "Failed to fetch computers"
573
+ );
574
+ } else {
575
+ console.error(error instanceof Error ? error.message : "Failed to fetch computers");
576
+ }
577
+ process.exit(1);
578
+ }
579
+ });
580
+ var getCommand = new Command2("get").description("Show computer details").argument("<id-or-handle>", "Computer id or handle").option("--json", "Print raw JSON").action(async (identifier, options) => {
581
+ const spinner = options.json ? null : ora2("Fetching computer...").start();
582
+ try {
583
+ const computer = await resolveComputer(identifier);
584
+ spinner?.stop();
585
+ if (options.json) {
586
+ console.log(JSON.stringify(computer, null, 2));
587
+ return;
588
+ }
589
+ printComputer(computer);
590
+ } catch (error) {
591
+ if (spinner) {
592
+ spinner.fail(
593
+ error instanceof Error ? error.message : "Failed to fetch computer"
594
+ );
595
+ } else {
596
+ console.error(error instanceof Error ? error.message : "Failed to fetch computer");
597
+ }
598
+ process.exit(1);
599
+ }
600
+ });
601
+ var createCommand = new Command2("create").description("Create a computer").argument("[handle]", "Optional computer handle").option("--name <display-name>", "Display name").option("--tier <tier>", "Tier override").option("--interactive", "Prompt for runtime choices").option("--runtime-family <runtime-family>", "managed-worker or custom-machine").option("--source-kind <source-kind>", "none or oci-image").option("--image-family <family>", "Image family override").option("--image-ref <image>", "Resolved image override").option("--primary-port <port>", "Primary app port").option("--primary-path <path>", "Primary app path").option("--healthcheck-type <type>", "http or tcp").option("--healthcheck-value <value>", "Health check path or port").option("--ssh-enabled", "Enable SSH access").option("--ssh-disabled", "Disable SSH access").option("--vnc-enabled", "Enable VNC access").option("--vnc-disabled", "Disable VNC access").action(async (handle, options) => {
602
+ let spinner;
603
+ let timer;
604
+ let startTime = 0;
605
+ try {
606
+ const selectedOptions = await resolveCreateOptions(options);
607
+ spinner = ora2("Creating computer...").start();
608
+ startTime = Date.now();
609
+ timer = setInterval(() => {
610
+ const elapsed2 = ((Date.now() - startTime) / 1e3).toFixed(1);
611
+ if (spinner) {
612
+ spinner.text = `Creating computer... ${chalk3.dim(`${elapsed2}s`)}`;
613
+ }
614
+ }, 100);
615
+ const computer = await createComputer({
616
+ handle,
617
+ display_name: selectedOptions.name,
618
+ tier: selectedOptions.tier,
619
+ runtime_family: parseRuntimeFamilyOption(selectedOptions.runtimeFamily),
620
+ source_kind: parseSourceKindOption(selectedOptions.sourceKind),
621
+ image_family: selectedOptions.imageFamily,
622
+ image_ref: selectedOptions.imageRef,
623
+ primary_port: parseOptionalPort(selectedOptions.primaryPort),
624
+ primary_path: selectedOptions.primaryPath,
625
+ healthcheck_type: parseHealthcheckTypeOption(selectedOptions.healthcheckType),
626
+ healthcheck_value: selectedOptions.healthcheckValue,
627
+ ssh_enabled: resolveOptionalToggle(
628
+ selectedOptions.sshEnabled,
629
+ selectedOptions.sshDisabled,
630
+ "SSH"
631
+ ),
632
+ vnc_enabled: resolveOptionalToggle(
633
+ selectedOptions.vncEnabled,
634
+ selectedOptions.vncDisabled,
635
+ "VNC"
636
+ )
637
+ });
638
+ if (timer) {
639
+ clearInterval(timer);
640
+ }
641
+ const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
642
+ spinner.succeed(
643
+ chalk3.green(`Created ${chalk3.bold(computer.handle)} ${chalk3.dim(`[${elapsed}s]`)}`)
644
+ );
645
+ printComputer(computer);
646
+ } catch (error) {
647
+ if (timer) {
648
+ clearInterval(timer);
649
+ }
650
+ const message = error instanceof Error ? error.message : "Failed to create computer";
651
+ if (spinner) {
652
+ const suffix = startTime ? ` ${chalk3.dim(`[${((Date.now() - startTime) / 1e3).toFixed(1)}s]`)}` : "";
653
+ spinner.fail(`${message}${suffix}`);
654
+ } else {
655
+ console.error(chalk3.red(message));
656
+ }
657
+ process.exit(1);
658
+ }
659
+ });
660
+ async function resolveCreateOptions(options) {
661
+ const selectedOptions = { ...options };
662
+ const hasExplicitRuntimeSelection = Boolean(selectedOptions.runtimeFamily) || Boolean(selectedOptions.imageRef) || Boolean(selectedOptions.sourceKind);
663
+ const shouldPrompt = Boolean(selectedOptions.interactive) || !hasExplicitRuntimeSelection;
664
+ if (!process.stdin.isTTY || !process.stdout.isTTY || !shouldPrompt) {
665
+ validateCreateOptions(selectedOptions);
666
+ return selectedOptions;
667
+ }
668
+ const runtimeChoice = await select({
669
+ message: "Select runtime",
670
+ choices: [
671
+ {
672
+ name: "managed-worker - default Ubuntu desktop, SSH, VNC, terminal",
673
+ value: "managed-worker"
674
+ },
675
+ {
676
+ name: "custom-machine - boot a specific OCI image as the machine",
677
+ value: "custom-machine"
678
+ }
679
+ ],
680
+ default: "managed-worker"
681
+ });
682
+ if (runtimeChoice === "custom-machine") {
683
+ selectedOptions.runtimeFamily = "custom-machine";
684
+ selectedOptions.sourceKind = "oci-image";
685
+ selectedOptions.imageRef = (await textInput({ message: "OCI image ref (required):" })).trim();
686
+ selectedOptions.primaryPort = (await textInput({ message: "Primary port:", default: "3000" })).trim();
687
+ selectedOptions.primaryPath = (await textInput({ message: "Primary path:", default: "/" })).trim();
688
+ selectedOptions.healthcheckType = await select({
689
+ message: "Healthcheck type",
690
+ choices: [
691
+ { name: "tcp", value: "tcp" },
692
+ { name: "http", value: "http" }
693
+ ],
694
+ default: "tcp"
695
+ });
696
+ selectedOptions.healthcheckValue = (await textInput({ message: "Healthcheck value (optional):" })).trim();
697
+ selectedOptions.sshEnabled = await confirm({
698
+ message: "Enable SSH?",
699
+ default: false
700
+ });
701
+ selectedOptions.sshDisabled = !selectedOptions.sshEnabled;
702
+ selectedOptions.vncDisabled = true;
703
+ } else {
704
+ selectedOptions.runtimeFamily = "managed-worker";
705
+ selectedOptions.imageFamily = selectedOptions.imageFamily ?? "sandbox-agent";
706
+ }
707
+ validateCreateOptions(selectedOptions);
708
+ return selectedOptions;
709
+ }
710
+ var removeCommand = new Command2("rm").description("Delete a computer").argument("<id-or-handle>", "Computer id or handle").option("-y, --yes", "Skip confirmation prompt").action(async (identifier, options, cmd) => {
711
+ const globalYes = cmd.parent?.opts()?.yes;
712
+ const skipConfirm = Boolean(options.yes || globalYes);
713
+ const spinner = ora2("Resolving computer...").start();
714
+ try {
715
+ const computer = await resolveComputer(identifier);
716
+ spinner.stop();
717
+ if (!skipConfirm && process.stdin.isTTY) {
718
+ const confirmed = await confirm({
719
+ message: `Delete computer ${chalk3.bold(computer.handle)}?`,
720
+ default: false
721
+ });
722
+ if (!confirmed) {
723
+ console.log(chalk3.dim(" Cancelled."));
724
+ return;
725
+ }
726
+ }
727
+ const deleteSpinner = ora2("Deleting computer...").start();
728
+ await api(`/v1/computers/${computer.id}`, {
729
+ method: "DELETE"
730
+ });
731
+ deleteSpinner.succeed(chalk3.green(`Deleted ${chalk3.bold(computer.handle)}`));
732
+ } catch (error) {
733
+ spinner.fail(
734
+ error instanceof Error ? error.message : "Failed to delete computer"
735
+ );
736
+ process.exit(1);
737
+ }
738
+ });
739
+ function parseRuntimeFamilyOption(value) {
740
+ switch (value) {
741
+ case void 0:
742
+ case "managed-worker":
743
+ case "custom-machine":
744
+ return value;
745
+ default:
746
+ throw new Error("--runtime-family must be managed-worker or custom-machine");
747
+ }
748
+ }
749
+ function validateCreateOptions(options) {
750
+ const runtimeFamily = parseRuntimeFamilyOption(options.runtimeFamily);
751
+ const sourceKind = parseSourceKindOption(options.sourceKind);
752
+ if (runtimeFamily === "custom-machine" && (typeof options.imageRef !== "string" || options.imageRef.trim() === "")) {
753
+ throw new Error("--image-ref is required for --runtime-family custom-machine");
754
+ }
755
+ if (runtimeFamily === "custom-machine" && sourceKind === "none") {
756
+ throw new Error("--source-kind none is invalid for --runtime-family custom-machine");
757
+ }
758
+ if (runtimeFamily === "custom-machine" && options.vncEnabled) {
759
+ throw new Error("custom-machine does not support platform VNC");
760
+ }
761
+ }
762
+ function parseSourceKindOption(value) {
763
+ switch (value) {
764
+ case void 0:
765
+ case "none":
766
+ case "oci-image":
767
+ return value;
768
+ default:
769
+ throw new Error("--source-kind must be none or oci-image");
770
+ }
771
+ }
772
+ function parseHealthcheckTypeOption(value) {
773
+ switch (value) {
774
+ case void 0:
775
+ case "http":
776
+ case "tcp":
777
+ return value;
778
+ default:
779
+ throw new Error("--healthcheck-type must be http or tcp");
780
+ }
781
+ }
782
+ function parseOptionalPort(value) {
783
+ if (value === void 0) {
784
+ return void 0;
785
+ }
786
+ const parsed = Number.parseInt(value, 10);
787
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) {
788
+ throw new Error("--primary-port must be an integer between 1 and 65535");
789
+ }
790
+ return parsed;
791
+ }
792
+ function resolveOptionalToggle(enabled, disabled, label) {
793
+ if (enabled && disabled) {
794
+ throw new Error(`choose either --${label.toLowerCase()}-enabled or --${label.toLowerCase()}-disabled`);
795
+ }
796
+ if (enabled) {
797
+ return true;
798
+ }
799
+ if (disabled) {
800
+ return false;
801
+ }
802
+ return void 0;
803
+ }
804
+
805
+ // src/commands/completion.ts
806
+ import { Command as Command3 } from "commander";
807
+ var ZSH_SCRIPT = `#compdef computer
808
+
809
+ _computer() {
810
+ local -a commands
811
+ commands=(
812
+ 'login:Authenticate the CLI'
813
+ 'logout:Remove stored API key'
814
+ 'whoami:Show current user'
815
+ 'create:Create a computer'
816
+ 'ls:List computers'
817
+ 'get:Show computer details'
818
+ 'open:Open in browser'
819
+ 'ssh:SSH into a computer'
820
+ 'ports:Manage published ports'
821
+ 'rm:Delete a computer'
822
+ 'completion:Generate shell completions'
823
+ 'help:Display help'
824
+ )
825
+
826
+ local -a ports_commands
827
+ ports_commands=(
828
+ 'ls:List published ports'
829
+ 'publish:Publish an HTTP app port'
830
+ 'rm:Unpublish an app port'
831
+ )
832
+
833
+ _arguments -C \\
834
+ '(-h --help)'{-h,--help}'[Display help]' \\
835
+ '(-V --version)'{-V,--version}'[Show version]' \\
836
+ '1:command:->command' \\
837
+ '*::arg:->args'
838
+
839
+ case "$state" in
840
+ command)
841
+ _describe -t commands 'computer command' commands
842
+ ;;
843
+ args)
844
+ case "$words[1]" in
845
+ login)
846
+ _arguments \\
847
+ '--api-key[API key]:key:' \\
848
+ '--stdin[Read API key from stdin]' \\
849
+ '(-f --force)'{-f,--force}'[Overwrite existing key]'
850
+ ;;
851
+ whoami)
852
+ _arguments '--json[Print raw JSON]'
853
+ ;;
854
+ create)
855
+ _arguments \\
856
+ '--name[Display name]:name:' \\
857
+ '--tier[Tier override]:tier:' \\
858
+ '--interactive[Prompt for runtime choices]' \\
859
+ '--runtime-family[Runtime family]:family:(managed-worker custom-machine)' \\
860
+ '--source-kind[Source kind]:kind:(none oci-image)' \\
861
+ '--image-family[Image family]:family:' \\
862
+ '--image-ref[Image reference]:image:' \\
863
+ '--primary-port[Primary app port]:port:' \\
864
+ '--primary-path[Primary app path]:path:' \\
865
+ '--healthcheck-type[Healthcheck type]:type:(http tcp)' \\
866
+ '--healthcheck-value[Healthcheck value]:value:' \\
867
+ '--ssh-enabled[Enable SSH]' \\
868
+ '--ssh-disabled[Disable SSH]' \\
869
+ '--vnc-enabled[Enable VNC]' \\
870
+ '--vnc-disabled[Disable VNC]' \\
871
+ '1:handle:'
872
+ ;;
873
+ ls)
874
+ _arguments \\
875
+ '--json[Print raw JSON]' \\
876
+ '(-v --verbose)'{-v,--verbose}'[Show all URLs]'
877
+ ;;
878
+ get)
879
+ _arguments \\
880
+ '--json[Print raw JSON]' \\
881
+ '1:computer:_computer_handles'
882
+ ;;
883
+ open)
884
+ _arguments \\
885
+ '--vnc[Open VNC desktop]' \\
886
+ '--terminal[Open terminal]' \\
887
+ '1:computer:_computer_handles'
888
+ ;;
889
+ ssh)
890
+ _arguments '1:computer:_computer_handles'
891
+ ;;
892
+ rm)
893
+ _arguments \\
894
+ '(-y --yes)'{-y,--yes}'[Skip confirmation]' \\
895
+ '1:computer:_computer_handles'
896
+ ;;
897
+ ports)
898
+ _arguments -C \\
899
+ '1:command:->ports_command' \\
900
+ '*::arg:->ports_args'
901
+ case "$state" in
902
+ ports_command)
903
+ _describe -t commands 'ports command' ports_commands
904
+ ;;
905
+ ports_args)
906
+ case "$words[1]" in
907
+ ls)
908
+ _arguments '1:computer:_computer_handles'
909
+ ;;
910
+ publish)
911
+ _arguments \\
912
+ '--subdomain[Custom subdomain]:subdomain:' \\
913
+ '--protocol[Protocol]:protocol:(http https)' \\
914
+ '1:computer:_computer_handles' \\
915
+ '2:port:'
916
+ ;;
917
+ rm)
918
+ _arguments \\
919
+ '1:computer:_computer_handles' \\
920
+ '2:port:'
921
+ ;;
922
+ esac
923
+ ;;
924
+ esac
925
+ ;;
926
+ completion)
927
+ _arguments '1:shell:(bash zsh)'
928
+ ;;
929
+ esac
930
+ ;;
931
+ esac
932
+ }
933
+
934
+ _computer_handles() {
935
+ local -a handles
936
+ if handles=(\${(f)"$(computer ls --json 2>/dev/null | grep '"handle"' | sed 's/.*"handle": "\\([^"]*\\)".*/\\1/')"}); then
937
+ _describe -t handles 'computer handle' handles
938
+ fi
939
+ }
940
+
941
+ _computer "$@"`;
942
+ var BASH_SCRIPT = `_computer() {
943
+ local cur prev words cword
944
+ _init_completion || return
945
+
946
+ local commands="login logout whoami create ls get open ssh ports rm completion help"
947
+ local ports_commands="ls publish rm"
948
+
949
+ if [[ $cword -eq 1 ]]; then
950
+ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
951
+ return
952
+ fi
953
+
954
+ local cmd="\${words[1]}"
955
+
956
+ case "$cmd" in
957
+ login)
958
+ COMPREPLY=($(compgen -W "--api-key --stdin --force -f" -- "$cur"))
959
+ ;;
960
+ whoami)
961
+ COMPREPLY=($(compgen -W "--json" -- "$cur"))
962
+ ;;
963
+ create)
964
+ COMPREPLY=($(compgen -W "--name --tier --interactive --runtime-family --source-kind --image-family --image-ref --primary-port --primary-path --healthcheck-type --healthcheck-value --ssh-enabled --ssh-disabled --vnc-enabled --vnc-disabled" -- "$cur"))
965
+ ;;
966
+ ls)
967
+ COMPREPLY=($(compgen -W "--json --verbose -v" -- "$cur"))
968
+ ;;
969
+ get|open|ssh|rm)
970
+ if [[ $cword -eq 2 ]]; then
971
+ local handles
972
+ handles=$(computer ls --json 2>/dev/null | grep '"handle"' | sed 's/.*"handle": "\\([^"]*\\)".*/\\1/')
973
+ COMPREPLY=($(compgen -W "$handles" -- "$cur"))
974
+ else
975
+ case "$cmd" in
976
+ get) COMPREPLY=($(compgen -W "--json" -- "$cur")) ;;
977
+ open) COMPREPLY=($(compgen -W "--vnc --terminal" -- "$cur")) ;;
978
+ rm) COMPREPLY=($(compgen -W "--yes -y" -- "$cur")) ;;
979
+ esac
980
+ fi
981
+ ;;
982
+ ports)
983
+ if [[ $cword -eq 2 ]]; then
984
+ COMPREPLY=($(compgen -W "$ports_commands" -- "$cur"))
985
+ elif [[ $cword -eq 3 ]]; then
986
+ local handles
987
+ handles=$(computer ls --json 2>/dev/null | grep '"handle"' | sed 's/.*"handle": "\\([^"]*\\)".*/\\1/')
988
+ COMPREPLY=($(compgen -W "$handles" -- "$cur"))
989
+ fi
990
+ ;;
991
+ completion)
992
+ if [[ $cword -eq 2 ]]; then
993
+ COMPREPLY=($(compgen -W "bash zsh" -- "$cur"))
994
+ fi
995
+ ;;
996
+ esac
997
+ }
998
+
999
+ complete -F _computer computer`;
1000
+ var completionCommand = new Command3("completion").description("Generate shell completions").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
1001
+ switch (shell) {
1002
+ case "zsh":
1003
+ console.log(ZSH_SCRIPT);
1004
+ break;
1005
+ case "bash":
1006
+ console.log(BASH_SCRIPT);
1007
+ break;
1008
+ default:
1009
+ console.error(`Unsupported shell: ${shell}. Use "bash" or "zsh".`);
1010
+ process.exit(1);
1011
+ }
1012
+ });
1013
+
1014
+ // src/commands/login.ts
1015
+ import { Command as Command4 } from "commander";
1016
+ import chalk4 from "chalk";
1017
+ import open2 from "open";
1018
+ import ora3 from "ora";
1019
+
1020
+ // src/lib/browser-login.ts
1021
+ import { randomBytes } from "crypto";
1022
+ import { createServer } from "http";
1023
+ var CALLBACK_HOST = "127.0.0.1";
1024
+ var CALLBACK_PATH = "/callback";
1025
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
1026
+ async function createBrowserLoginAttempt() {
1027
+ const state = randomBytes(16).toString("hex");
1028
+ const deferred = createDeferred();
1029
+ let callbackURL = "";
1030
+ let closed = false;
1031
+ let settled = false;
1032
+ const server = createServer((request, response) => {
1033
+ void handleRequest({
1034
+ callbackURL,
1035
+ deferred,
1036
+ request,
1037
+ response,
1038
+ state,
1039
+ settledRef: {
1040
+ get current() {
1041
+ return settled;
1042
+ },
1043
+ set current(value) {
1044
+ settled = value;
1045
+ }
1046
+ }
1047
+ });
1048
+ });
1049
+ await listen(server);
1050
+ const address = server.address();
1051
+ if (!address || typeof address === "string") {
1052
+ await closeServer(server);
1053
+ throw new Error("Failed to allocate a loopback callback port");
1054
+ }
1055
+ callbackURL = `http://${CALLBACK_HOST}:${address.port}${CALLBACK_PATH}`;
1056
+ const loginURL = buildBrowserLoginURL(callbackURL, state);
1057
+ return {
1058
+ callbackURL,
1059
+ loginURL,
1060
+ async close() {
1061
+ if (closed) {
1062
+ return;
1063
+ }
1064
+ closed = true;
1065
+ await closeServer(server);
1066
+ },
1067
+ async waitForResult(timeoutMs = LOGIN_TIMEOUT_MS) {
1068
+ const timeout = setTimeout(() => {
1069
+ if (settled) {
1070
+ return;
1071
+ }
1072
+ settled = true;
1073
+ deferred.reject(new Error("Timed out waiting for browser login"));
1074
+ }, timeoutMs);
1075
+ try {
1076
+ return await deferred.promise;
1077
+ } finally {
1078
+ clearTimeout(timeout);
1079
+ if (!closed) {
1080
+ closed = true;
1081
+ await closeServer(server);
1082
+ }
1083
+ }
1084
+ }
1085
+ };
1086
+ }
1087
+ function buildBrowserLoginURL(callbackURL, state) {
1088
+ const url = new URL("/api/cli/auth", getWebURL());
1089
+ url.searchParams.set("callback_url", callbackURL);
1090
+ url.searchParams.set("state", state);
1091
+ return url.toString();
1092
+ }
1093
+ async function handleRequest(input) {
1094
+ const { callbackURL, deferred, request, response, state, settledRef } = input;
1095
+ if (!request.url || request.method !== "GET") {
1096
+ writeHTML(response, 404, renderErrorPage("Login page not found."));
1097
+ return;
1098
+ }
1099
+ const url = new URL(request.url, callbackURL || `http://${CALLBACK_HOST}`);
1100
+ if (url.pathname !== CALLBACK_PATH) {
1101
+ writeHTML(response, 404, renderErrorPage("Login page not found."));
1102
+ return;
1103
+ }
1104
+ if (settledRef.current) {
1105
+ writeHTML(response, 409, renderErrorPage("This login link has already been used."));
1106
+ return;
1107
+ }
1108
+ const returnedState = url.searchParams.get("state")?.trim();
1109
+ if (!returnedState || returnedState !== state) {
1110
+ settledRef.current = true;
1111
+ const error = new Error("Received an invalid browser login state");
1112
+ deferred.reject(error);
1113
+ writeHTML(response, 400, renderErrorPage(error.message));
1114
+ return;
1115
+ }
1116
+ const returnedError = url.searchParams.get("error")?.trim();
1117
+ if (returnedError) {
1118
+ settledRef.current = true;
1119
+ const error = new Error(returnedError);
1120
+ deferred.reject(error);
1121
+ writeHTML(response, 400, renderErrorPage(returnedError));
1122
+ return;
1123
+ }
1124
+ const apiKey = url.searchParams.get("api_key")?.trim();
1125
+ if (!apiKey) {
1126
+ settledRef.current = true;
1127
+ const error = new Error("Browser login did not return an API key");
1128
+ deferred.reject(error);
1129
+ writeHTML(response, 400, renderErrorPage(error.message));
1130
+ return;
1131
+ }
1132
+ try {
1133
+ const me = await apiWithKey(apiKey, "/v1/me");
1134
+ setAPIKey(apiKey);
1135
+ settledRef.current = true;
1136
+ deferred.resolve({
1137
+ apiKey,
1138
+ callbackURL,
1139
+ loginURL: buildBrowserLoginURL(callbackURL, state),
1140
+ me
1141
+ });
1142
+ writeHTML(response, 200, renderSuccessPage(me.user.email));
1143
+ } catch (error) {
1144
+ settledRef.current = true;
1145
+ const message = error instanceof Error ? error.message : "Failed to validate browser login";
1146
+ deferred.reject(new Error(message));
1147
+ writeHTML(response, 401, renderErrorPage(message));
1148
+ }
1149
+ }
1150
+ function createDeferred() {
1151
+ let resolve;
1152
+ let reject;
1153
+ const promise = new Promise((resolvePromise, rejectPromise) => {
1154
+ resolve = resolvePromise;
1155
+ reject = rejectPromise;
1156
+ });
1157
+ return {
1158
+ promise,
1159
+ reject,
1160
+ resolve
1161
+ };
1162
+ }
1163
+ function listen(server) {
1164
+ return new Promise((resolve, reject) => {
1165
+ server.once("error", reject);
1166
+ server.listen(0, CALLBACK_HOST, () => {
1167
+ server.off("error", reject);
1168
+ const address = server.address();
1169
+ if (!address || typeof address === "string") {
1170
+ reject(new Error("Failed to allocate a loopback callback port"));
1171
+ return;
1172
+ }
1173
+ resolve(address);
1174
+ });
1175
+ });
1176
+ }
1177
+ function closeServer(server) {
1178
+ return new Promise((resolve, reject) => {
1179
+ server.close((error) => {
1180
+ if (error) {
1181
+ reject(error);
1182
+ return;
1183
+ }
1184
+ resolve();
1185
+ });
1186
+ });
1187
+ }
1188
+ function writeHTML(response, statusCode, body) {
1189
+ response.statusCode = statusCode;
1190
+ response.setHeader("content-type", "text/html; charset=utf-8");
1191
+ response.end(body);
1192
+ }
1193
+ function renderSuccessPage(email) {
1194
+ return `<!doctype html>
1195
+ <html lang="en">
1196
+ <head>
1197
+ <meta charset="utf-8" />
1198
+ <title>Computer CLI login complete</title>
1199
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1200
+ <style>
1201
+ body { font-family: ui-sans-serif, system-ui, sans-serif; background: #0b1220; color: #e2e8f0; display: grid; min-height: 100vh; place-items: center; margin: 0; }
1202
+ main { width: min(32rem, calc(100vw - 2rem)); background: #111827; border: 1px solid #1f2937; border-radius: 16px; padding: 1.5rem; box-shadow: 0 20px 45px rgba(0, 0, 0, 0.35); }
1203
+ h1 { margin: 0 0 0.75rem; font-size: 1.5rem; }
1204
+ p { margin: 0; line-height: 1.5; color: #cbd5e1; }
1205
+ code { color: #f8fafc; }
1206
+ </style>
1207
+ </head>
1208
+ <body>
1209
+ <main>
1210
+ <h1>Computer CLI login complete</h1>
1211
+ <p>Signed in as <code>${escapeHTML(email)}</code>. You can close this tab.</p>
1212
+ </main>
1213
+ <script>
1214
+ window.setTimeout(() => window.close(), 150);
1215
+ </script>
1216
+ </body>
1217
+ </html>`;
1218
+ }
1219
+ function renderErrorPage(message) {
1220
+ return `<!doctype html>
1221
+ <html lang="en">
1222
+ <head>
1223
+ <meta charset="utf-8" />
1224
+ <title>Computer CLI login failed</title>
1225
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1226
+ <style>
1227
+ body { font-family: ui-sans-serif, system-ui, sans-serif; background: #180c0d; color: #fee2e2; display: grid; min-height: 100vh; place-items: center; margin: 0; }
1228
+ main { width: min(32rem, calc(100vw - 2rem)); background: #2b1114; border: 1px solid #7f1d1d; border-radius: 16px; padding: 1.5rem; box-shadow: 0 20px 45px rgba(0, 0, 0, 0.35); }
1229
+ h1 { margin: 0 0 0.75rem; font-size: 1.5rem; }
1230
+ p { margin: 0; line-height: 1.5; color: #fecaca; }
1231
+ </style>
1232
+ </head>
1233
+ <body>
1234
+ <main>
1235
+ <h1>Computer CLI login failed</h1>
1236
+ <p>${escapeHTML(message)}</p>
1237
+ </main>
1238
+ </body>
1239
+ </html>`;
1240
+ }
1241
+ function escapeHTML(value) {
1242
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1243
+ }
1244
+
1245
+ // src/commands/login.ts
1246
+ var loginCommand = new Command4("login").description("Authenticate the CLI").option("--api-key <key>", "API key starting with ac_live_").option("--stdin", "Read the API key from stdin").option("-f, --force", "Overwrite an existing stored API key").action(async (options) => {
1247
+ const existingKey = getStoredAPIKey();
1248
+ if (existingKey && !options.force) {
1249
+ console.log();
1250
+ console.log(chalk4.yellow(" Already logged in. Use --force to overwrite."));
1251
+ console.log();
1252
+ return;
1253
+ }
1254
+ const wantsManualLogin = Boolean(options.apiKey || options.stdin);
1255
+ const apiKey = await resolveAPIKeyInput(options.apiKey, options.stdin);
1256
+ if (!apiKey && wantsManualLogin) {
1257
+ console.log();
1258
+ console.log(chalk4.dim(" Usage: computer login --api-key <ac_live_...>"));
1259
+ console.log(chalk4.dim(` API: ${getBaseURL()}`));
1260
+ console.log();
1261
+ process.exit(1);
1262
+ }
1263
+ if (!apiKey) {
1264
+ await runBrowserLogin();
1265
+ return;
1266
+ }
1267
+ if (!apiKey.startsWith("ac_live_")) {
1268
+ console.log();
1269
+ console.log(chalk4.red(" API key must start with ac_live_"));
1270
+ console.log();
1271
+ process.exit(1);
1272
+ }
1273
+ const spinner = ora3("Authenticating...").start();
1274
+ try {
1275
+ const me = await apiWithKey(apiKey, "/v1/me");
1276
+ setAPIKey(apiKey);
1277
+ spinner.succeed(`Logged in as ${chalk4.bold(me.user.email)}`);
1278
+ } catch (error) {
1279
+ spinner.fail(
1280
+ error instanceof Error ? error.message : "Failed to validate API key"
1281
+ );
1282
+ process.exit(1);
1283
+ }
1284
+ });
1285
+ async function runBrowserLogin() {
1286
+ const spinner = ora3("Starting browser login...").start();
1287
+ let attempt = null;
1288
+ try {
1289
+ attempt = await createBrowserLoginAttempt();
1290
+ spinner.text = "Opening browser...";
1291
+ try {
1292
+ await open2(attempt.loginURL);
1293
+ } catch {
1294
+ spinner.stop();
1295
+ console.log();
1296
+ console.log(chalk4.yellow(" Browser auto-open failed. Open this URL to continue:"));
1297
+ console.log(chalk4.dim(` ${attempt.loginURL}`));
1298
+ console.log();
1299
+ spinner.start("Waiting for browser login...");
1300
+ }
1301
+ spinner.text = "Waiting for browser login...";
1302
+ const result = await attempt.waitForResult();
1303
+ spinner.succeed(`Logged in as ${chalk4.bold(result.me.user.email)}`);
1304
+ } catch (error) {
1305
+ spinner.fail(error instanceof Error ? error.message : "Browser login failed");
1306
+ process.exit(1);
1307
+ } finally {
1308
+ await attempt?.close();
1309
+ }
1310
+ }
1311
+ async function resolveAPIKeyInput(flagValue, readFromStdin) {
1312
+ if (flagValue?.trim()) {
1313
+ return flagValue.trim();
1314
+ }
1315
+ if (!readFromStdin) {
1316
+ return "";
1317
+ }
1318
+ if (process.stdin.isTTY) {
1319
+ return "";
1320
+ }
1321
+ const chunks = [];
1322
+ for await (const chunk of process.stdin) {
1323
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1324
+ }
1325
+ return Buffer.concat(chunks).toString("utf8").trim();
1326
+ }
1327
+
1328
+ // src/commands/logout.ts
1329
+ import { Command as Command5 } from "commander";
1330
+ import chalk5 from "chalk";
1331
+ var logoutCommand = new Command5("logout").description("Remove stored API key").action(() => {
1332
+ if (!getStoredAPIKey()) {
1333
+ console.log();
1334
+ console.log(chalk5.dim(" Not logged in."));
1335
+ if (hasEnvAPIKey()) {
1336
+ console.log(chalk5.dim(" Environment API key is still active in this shell."));
1337
+ }
1338
+ console.log();
1339
+ return;
1340
+ }
1341
+ clearAPIKey();
1342
+ console.log();
1343
+ console.log(chalk5.green(" Logged out."));
1344
+ if (hasEnvAPIKey()) {
1345
+ console.log(chalk5.dim(" Environment API key is still active in this shell."));
1346
+ }
1347
+ console.log();
1348
+ });
1349
+
1350
+ // src/commands/whoami.ts
1351
+ import { Command as Command6 } from "commander";
1352
+ import chalk6 from "chalk";
1353
+ import ora4 from "ora";
1354
+ var whoamiCommand = new Command6("whoami").description("Show current user").option("--json", "Print raw JSON").action(async (options) => {
1355
+ const spinner = options.json ? null : ora4("Loading user...").start();
1356
+ try {
1357
+ const me = await api("/v1/me");
1358
+ spinner?.stop();
1359
+ if (options.json) {
1360
+ console.log(JSON.stringify(me, null, 2));
1361
+ return;
1362
+ }
1363
+ console.log();
1364
+ console.log(` ${chalk6.bold.white(me.user.display_name || me.user.email)}`);
1365
+ if (me.user.display_name) {
1366
+ console.log(` ${chalk6.dim(me.user.email)}`);
1367
+ }
1368
+ if (me.api_key.name) {
1369
+ console.log(` ${chalk6.dim("Key:")} ${me.api_key.name}`);
1370
+ }
1371
+ console.log(` ${chalk6.dim("API:")} ${chalk6.dim(getBaseURL())}`);
1372
+ console.log();
1373
+ } catch (error) {
1374
+ if (spinner) {
1375
+ spinner.fail(error instanceof Error ? error.message : "Failed to load user");
1376
+ } else {
1377
+ console.error(error instanceof Error ? error.message : "Failed to load user");
1378
+ }
1379
+ process.exit(1);
1380
+ }
1381
+ });
1382
+
1383
+ // src/index.ts
1384
+ var require2 = createRequire(import.meta.url);
1385
+ var pkg = require2("../package.json");
1386
+ var cliName = process.argv[1] ? basename2(process.argv[1]) : "agentcomputer";
1387
+ var program = new Command7();
1388
+ program.name(cliName).description("Agent Computer CLI").version(pkg.version ?? "0.0.0").option("-y, --yes", "Skip confirmation prompts").configureHelp({
1389
+ formatHelp(cmd, helper) {
1390
+ const version = pkg.version ?? "0.0.0";
1391
+ const lines = [];
1392
+ lines.push(`${chalk7.bold(cliName)} ${chalk7.dim(`v${version}`)}`);
1393
+ lines.push("");
1394
+ if (cmd.commands.length > 0) {
1395
+ const groups = {
1396
+ Auth: [],
1397
+ Computers: [],
1398
+ Access: [],
1399
+ Other: []
1400
+ };
1401
+ for (const sub of cmd.commands) {
1402
+ const name = sub.name();
1403
+ const desc = sub.description();
1404
+ const entry = { name, desc };
1405
+ if (["login", "logout", "whoami"].includes(name)) {
1406
+ groups.Auth.push(entry);
1407
+ } else if (["create", "ls", "get", "rm"].includes(name)) {
1408
+ groups.Computers.push(entry);
1409
+ } else if (["open", "ssh", "ports"].includes(name)) {
1410
+ groups.Access.push(entry);
1411
+ } else {
1412
+ groups.Other.push(entry);
1413
+ }
1414
+ }
1415
+ for (const [groupName, entries] of Object.entries(groups)) {
1416
+ if (entries.length === 0) continue;
1417
+ lines.push(` ${chalk7.dim(groupName)}`);
1418
+ for (const entry of entries) {
1419
+ const padded = entry.name.padEnd(14);
1420
+ lines.push(` ${chalk7.white(padded)}${chalk7.dim(entry.desc)}`);
1421
+ }
1422
+ lines.push("");
1423
+ }
1424
+ }
1425
+ const globalOpts = [
1426
+ { flags: "-y, --yes", desc: "Skip confirmation prompts" },
1427
+ { flags: "-V, --version", desc: "Show version" },
1428
+ { flags: "-h, --help", desc: "Show help" }
1429
+ ];
1430
+ lines.push(` ${chalk7.dim("Options")}`);
1431
+ for (const opt of globalOpts) {
1432
+ const padded = opt.flags.padEnd(14);
1433
+ lines.push(` ${chalk7.white(padded)}${chalk7.dim(opt.desc)}`);
1434
+ }
1435
+ lines.push("");
1436
+ return lines.join("\n");
1437
+ }
1438
+ });
1439
+ program.addCommand(loginCommand);
1440
+ program.addCommand(logoutCommand);
1441
+ program.addCommand(whoamiCommand);
1442
+ program.addCommand(createCommand);
1443
+ program.addCommand(lsCommand);
1444
+ program.addCommand(getCommand);
1445
+ program.addCommand(openCommand);
1446
+ program.addCommand(sshCommand);
1447
+ program.addCommand(portsCommand);
1448
+ program.addCommand(removeCommand);
1449
+ program.addCommand(completionCommand);
1450
+ program.parse();