aicomputer 0.1.3 → 0.1.4

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 (3) hide show
  1. package/README.md +13 -8
  2. package/dist/index.js +151 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,14 +10,19 @@ npm install -g aicomputer
10
10
 
11
11
  ## Usage
12
12
 
13
+ After installing, use the `computer` command:
14
+
13
15
  ```bash
14
- agentcomputer login
15
- agentcomputer login --api-key <ac_live_...>
16
- agentcomputer whoami
17
- agentcomputer create my-box
18
- agentcomputer open my-box
19
- agentcomputer open my-box --terminal
20
- agentcomputer ssh my-box
16
+ computer login
17
+ computer login --api-key <ac_live_...>
18
+ computer whoami
19
+ computer create my-box
20
+ computer open my-box
21
+ computer open my-box --terminal
22
+ computer ssh
23
+ computer ssh my-box
21
24
  ```
22
25
 
23
- The package installs the `aicomputer`, `agentcomputer`, and `computer` commands. You can also run it without a global install via `npx aicomputer <command>`.
26
+ Run `computer ssh` without a handle in an interactive terminal to pick from your available machines.
27
+
28
+ You can also run without a global install via `npx aicomputer <command>`.
package/dist/index.js CHANGED
@@ -8,9 +8,9 @@ import { basename as basename2 } from "path";
8
8
 
9
9
  // src/commands/access.ts
10
10
  import { spawn } from "child_process";
11
+ import { select } from "@inquirer/prompts";
11
12
  import { Command } from "commander";
12
13
  import chalk2 from "chalk";
13
- import open from "open";
14
14
  import ora from "ora";
15
15
 
16
16
  // src/lib/config.ts
@@ -216,7 +216,7 @@ function vncURL(computer) {
216
216
  return null;
217
217
  }
218
218
  const domain = computer.primary_web_host.replace(/^[^.]+\./, "");
219
- return `https://6080--${computer.handle}.${domain}/vnc_lite.html`;
219
+ return `https://6080--${computer.handle}.${domain}/vnc.html?autoconnect=1&resize=remote`;
220
220
  }
221
221
  function terminalURL(computer) {
222
222
  if (computer.runtime_family !== "managed-worker") {
@@ -236,7 +236,9 @@ function normalizePrimaryPath(primaryPath) {
236
236
  // src/lib/ssh-keys.ts
237
237
  import { basename } from "path";
238
238
  import { homedir as homedir2 } from "os";
239
- import { readFile } from "fs/promises";
239
+ import { readFile, mkdir } from "fs/promises";
240
+ import { execFileSync } from "child_process";
241
+ import { existsSync as existsSync2 } from "fs";
240
242
  var DEFAULT_PUBLIC_KEY_PATHS = [
241
243
  `${homedir2()}/.ssh/id_ed25519.pub`,
242
244
  `${homedir2()}/.ssh/id_ecdsa.pub`,
@@ -245,19 +247,19 @@ var DEFAULT_PUBLIC_KEY_PATHS = [
245
247
  async function ensureDefaultSSHKeyRegistered() {
246
248
  for (const path of DEFAULT_PUBLIC_KEY_PATHS) {
247
249
  try {
248
- const publicKey = (await readFile(path, "utf8")).trim();
249
- if (!publicKey) {
250
+ const publicKey2 = (await readFile(path, "utf8")).trim();
251
+ if (!publicKey2) {
250
252
  continue;
251
253
  }
252
- const key = await api("/v1/ssh-keys", {
254
+ const key2 = await api("/v1/ssh-keys", {
253
255
  method: "POST",
254
256
  body: JSON.stringify({
255
257
  name: basename(path),
256
- public_key: publicKey
258
+ public_key: publicKey2
257
259
  })
258
260
  });
259
261
  return {
260
- key,
262
+ key: key2,
261
263
  publicKeyPath: path,
262
264
  privateKeyPath: path.replace(/\.pub$/, "")
263
265
  };
@@ -268,7 +270,33 @@ async function ensureDefaultSSHKeyRegistered() {
268
270
  throw error;
269
271
  }
270
272
  }
271
- throw new Error("no SSH public key found in ~/.ssh (looked for id_ed25519.pub, id_ecdsa.pub, id_rsa.pub)");
273
+ const generated = await generateSSHKey();
274
+ const publicKey = (await readFile(generated.publicKeyPath, "utf8")).trim();
275
+ const key = await api("/v1/ssh-keys", {
276
+ method: "POST",
277
+ body: JSON.stringify({
278
+ name: basename(generated.publicKeyPath),
279
+ public_key: publicKey
280
+ })
281
+ });
282
+ return {
283
+ key,
284
+ publicKeyPath: generated.publicKeyPath,
285
+ privateKeyPath: generated.privateKeyPath
286
+ };
287
+ }
288
+ async function generateSSHKey() {
289
+ const sshDir = `${homedir2()}/.ssh`;
290
+ if (!existsSync2(sshDir)) {
291
+ await mkdir(sshDir, { mode: 448 });
292
+ }
293
+ const privateKeyPath = `${sshDir}/id_ed25519`;
294
+ const publicKeyPath = `${privateKeyPath}.pub`;
295
+ console.log("No SSH key found \u2014 generating one at", publicKeyPath);
296
+ execFileSync("ssh-keygen", ["-t", "ed25519", "-f", privateKeyPath, "-N", ""], {
297
+ stdio: "inherit"
298
+ });
299
+ return { publicKeyPath, privateKeyPath };
272
300
  }
273
301
 
274
302
  // src/lib/format.ts
@@ -306,6 +334,53 @@ function formatStatus(status) {
306
334
  }
307
335
  }
308
336
 
337
+ // src/lib/open-browser.ts
338
+ import { constants } from "fs";
339
+ import { access } from "fs/promises";
340
+ import open from "open";
341
+ var IMAGE_BROWSER_LAUNCHER = "/usr/local/bin/browser-launcher";
342
+ async function isExecutable(path) {
343
+ try {
344
+ await access(path, constants.X_OK);
345
+ return true;
346
+ } catch {
347
+ return false;
348
+ }
349
+ }
350
+ function hasBrokenChromeBrowserEnv() {
351
+ const configuredBrowser = process.env.BROWSER?.trim().toLowerCase();
352
+ switch (configuredBrowser) {
353
+ case "chrome":
354
+ case "google-chrome":
355
+ case "google chrome":
356
+ return true;
357
+ default:
358
+ return false;
359
+ }
360
+ }
361
+ async function openBrowserURL(url) {
362
+ if (process.platform !== "linux") {
363
+ await open(url);
364
+ return;
365
+ }
366
+ if (hasBrokenChromeBrowserEnv()) {
367
+ delete process.env.BROWSER;
368
+ }
369
+ if (await isExecutable(IMAGE_BROWSER_LAUNCHER)) {
370
+ try {
371
+ await open(url, { app: { name: IMAGE_BROWSER_LAUNCHER } });
372
+ return;
373
+ } catch {
374
+ }
375
+ }
376
+ try {
377
+ await open(url, { app: { name: "xdg-open" } });
378
+ return;
379
+ } catch {
380
+ }
381
+ await open(url);
382
+ }
383
+
309
384
  // src/commands/access.ts
310
385
  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
386
  const spinner = ora("Preparing access...").start();
@@ -321,7 +396,7 @@ var openCommand = new Command("open").description("Open a computer in your brows
321
396
  }
322
397
  const url = info.connection.vnc_url;
323
398
  spinner.succeed(`Opening VNC for ${chalk2.bold(computer.handle)}`);
324
- await open(url);
399
+ await openBrowserURL(url);
325
400
  console.log(chalk2.dim(` ${url}`));
326
401
  return;
327
402
  }
@@ -329,25 +404,27 @@ var openCommand = new Command("open").description("Open a computer in your brows
329
404
  if (!info.connection.terminal_available) {
330
405
  throw new Error("Terminal access is not available for this computer");
331
406
  }
332
- const access2 = await createTerminalAccess(computer.id);
407
+ const access3 = await createTerminalAccess(computer.id);
333
408
  spinner.succeed(`Opening terminal for ${chalk2.bold(computer.handle)}`);
334
- await open(access2.access_url);
335
- console.log(chalk2.dim(` ${access2.access_url}`));
409
+ await openBrowserURL(access3.access_url);
410
+ console.log(chalk2.dim(` ${access3.access_url}`));
336
411
  return;
337
412
  }
338
- const access = await createBrowserAccess(computer.id);
413
+ const access2 = await createBrowserAccess(computer.id);
339
414
  spinner.succeed(`Opening ${chalk2.bold(computer.handle)}`);
340
- await open(access.access_url);
341
- console.log(chalk2.dim(` ${access.access_url}`));
415
+ await openBrowserURL(access2.access_url);
416
+ console.log(chalk2.dim(` ${access2.access_url}`));
342
417
  } catch (error) {
343
418
  spinner.fail(error instanceof Error ? error.message : "Failed to open computer");
344
419
  process.exit(1);
345
420
  }
346
421
  });
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();
422
+ var sshCommand = new Command("ssh").description("Open an SSH session to a computer").argument("[id-or-handle]", "Computer id or handle").action(async (identifier) => {
423
+ const spinner = ora(
424
+ identifier ? "Preparing SSH access..." : "Fetching computers..."
425
+ ).start();
349
426
  try {
350
- const computer = await resolveComputer(identifier);
427
+ const computer = await resolveSSHComputer(identifier, spinner);
351
428
  const info = await getConnectionInfo(computer.id);
352
429
  if (!info.connection.ssh_available) {
353
430
  throw new Error("SSH is not available for this computer");
@@ -457,12 +534,55 @@ async function runSSH(args) {
457
534
  });
458
535
  });
459
536
  }
537
+ async function resolveSSHComputer(identifier, spinner) {
538
+ const trimmed = identifier?.trim();
539
+ if (trimmed) {
540
+ return resolveComputer(trimmed);
541
+ }
542
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
543
+ throw new Error("computer id or handle is required when not running interactively");
544
+ }
545
+ const computers = await listComputers();
546
+ const available = computers.filter(isSSHSelectable);
547
+ if (available.length === 0) {
548
+ if (computers.length === 0) {
549
+ throw new Error("no computers found");
550
+ }
551
+ throw new Error("no running computers with SSH enabled");
552
+ }
553
+ const handleWidth = Math.max(6, ...available.map((computer) => computer.handle.length));
554
+ spinner.stop();
555
+ let selectedID;
556
+ try {
557
+ selectedID = await select({
558
+ message: "Select a computer to SSH into",
559
+ pageSize: Math.min(available.length, 10),
560
+ choices: available.map((computer) => ({
561
+ name: `${padEnd(chalk2.white(computer.handle), handleWidth + 2)}${padEnd(formatStatus(computer.status), 12)}${chalk2.dim(describeSSHChoice(computer))}`,
562
+ value: computer.id
563
+ }))
564
+ });
565
+ } finally {
566
+ spinner.start("Preparing SSH access...");
567
+ }
568
+ return available.find((computer) => computer.id === selectedID) ?? available[0];
569
+ }
570
+ function isSSHSelectable(computer) {
571
+ return computer.ssh_enabled && computer.status === "running";
572
+ }
573
+ function describeSSHChoice(computer) {
574
+ const displayName = computer.display_name.trim();
575
+ if (displayName && displayName !== computer.handle) {
576
+ return `${displayName} ${timeAgo(computer.updated_at)}`;
577
+ }
578
+ return `${computer.runtime_family} ${timeAgo(computer.updated_at)}`;
579
+ }
460
580
 
461
581
  // src/commands/computers.ts
462
582
  import { Command as Command2 } from "commander";
463
583
  import chalk3 from "chalk";
464
584
  import ora2 from "ora";
465
- import { select, input as textInput, confirm } from "@inquirer/prompts";
585
+ import { select as select2, input as textInput, confirm } from "@inquirer/prompts";
466
586
  function isInternalCondition(value) {
467
587
  return /^[A-Z][a-zA-Z]+$/.test(value);
468
588
  }
@@ -665,7 +785,7 @@ async function resolveCreateOptions(options) {
665
785
  validateCreateOptions(selectedOptions);
666
786
  return selectedOptions;
667
787
  }
668
- const runtimeChoice = await select({
788
+ const runtimeChoice = await select2({
669
789
  message: "Select runtime",
670
790
  choices: [
671
791
  {
@@ -685,7 +805,7 @@ async function resolveCreateOptions(options) {
685
805
  selectedOptions.imageRef = (await textInput({ message: "OCI image ref (required):" })).trim();
686
806
  selectedOptions.primaryPort = (await textInput({ message: "Primary port:", default: "3000" })).trim();
687
807
  selectedOptions.primaryPath = (await textInput({ message: "Primary path:", default: "/" })).trim();
688
- selectedOptions.healthcheckType = await select({
808
+ selectedOptions.healthcheckType = await select2({
689
809
  message: "Healthcheck type",
690
810
  choices: [
691
811
  { name: "tcp", value: "tcp" },
@@ -804,7 +924,7 @@ function resolveOptionalToggle(enabled, disabled, label) {
804
924
 
805
925
  // src/commands/completion.ts
806
926
  import { Command as Command3 } from "commander";
807
- var ZSH_SCRIPT = `#compdef computer
927
+ var ZSH_SCRIPT = `#compdef computer agentcomputer aicomputer
808
928
 
809
929
  _computer() {
810
930
  local -a commands
@@ -933,7 +1053,8 @@ _computer() {
933
1053
 
934
1054
  _computer_handles() {
935
1055
  local -a handles
936
- if handles=(\${(f)"$(computer ls --json 2>/dev/null | grep '"handle"' | sed 's/.*"handle": "\\([^"]*\\)".*/\\1/')"}); then
1056
+ local cli="\${words[1]:-computer}"
1057
+ if handles=(\${(f)"$(\${cli} ls --json 2>/dev/null | grep '"handle"' | sed 's/.*"handle": "\\([^"]*\\)".*/\\1/')"}); then
937
1058
  _describe -t handles 'computer handle' handles
938
1059
  fi
939
1060
  }
@@ -968,8 +1089,8 @@ var BASH_SCRIPT = `_computer() {
968
1089
  ;;
969
1090
  get|open|ssh|rm)
970
1091
  if [[ $cword -eq 2 ]]; then
971
- local handles
972
- handles=$(computer ls --json 2>/dev/null | grep '"handle"' | sed 's/.*"handle": "\\([^"]*\\)".*/\\1/')
1092
+ local handles cli="\${words[0]:-computer}"
1093
+ handles=$(\${cli} ls --json 2>/dev/null | grep '"handle"' | sed 's/.*"handle": "\\([^"]*\\)".*/\\1/')
973
1094
  COMPREPLY=($(compgen -W "$handles" -- "$cur"))
974
1095
  else
975
1096
  case "$cmd" in
@@ -983,8 +1104,8 @@ var BASH_SCRIPT = `_computer() {
983
1104
  if [[ $cword -eq 2 ]]; then
984
1105
  COMPREPLY=($(compgen -W "$ports_commands" -- "$cur"))
985
1106
  elif [[ $cword -eq 3 ]]; then
986
- local handles
987
- handles=$(computer ls --json 2>/dev/null | grep '"handle"' | sed 's/.*"handle": "\\([^"]*\\)".*/\\1/')
1107
+ local handles cli="\${words[0]:-computer}"
1108
+ handles=$(\${cli} ls --json 2>/dev/null | grep '"handle"' | sed 's/.*"handle": "\\([^"]*\\)".*/\\1/')
988
1109
  COMPREPLY=($(compgen -W "$handles" -- "$cur"))
989
1110
  fi
990
1111
  ;;
@@ -996,7 +1117,7 @@ var BASH_SCRIPT = `_computer() {
996
1117
  esac
997
1118
  }
998
1119
 
999
- complete -F _computer computer`;
1120
+ complete -F _computer computer agentcomputer aicomputer`;
1000
1121
  var completionCommand = new Command3("completion").description("Generate shell completions").argument("<shell>", "Shell type (bash or zsh)").action((shell) => {
1001
1122
  switch (shell) {
1002
1123
  case "zsh":
@@ -1014,7 +1135,6 @@ var completionCommand = new Command3("completion").description("Generate shell c
1014
1135
  // src/commands/login.ts
1015
1136
  import { Command as Command4 } from "commander";
1016
1137
  import chalk4 from "chalk";
1017
- import open2 from "open";
1018
1138
  import ora3 from "ora";
1019
1139
 
1020
1140
  // src/lib/browser-login.ts
@@ -1289,7 +1409,7 @@ async function runBrowserLogin() {
1289
1409
  attempt = await createBrowserLoginAttempt();
1290
1410
  spinner.text = "Opening browser...";
1291
1411
  try {
1292
- await open2(attempt.loginURL);
1412
+ await openBrowserURL(attempt.loginURL);
1293
1413
  } catch {
1294
1414
  spinner.stop();
1295
1415
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicomputer",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Computer CLI - manage your Agent Computer fleet from the terminal",
5
5
  "homepage": "https://agentcomputer.ai",
6
6
  "repository": {