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.
- package/README.md +13 -8
- package/dist/index.js +151 -31
- 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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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}/
|
|
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
|
|
249
|
-
if (!
|
|
250
|
+
const publicKey2 = (await readFile(path, "utf8")).trim();
|
|
251
|
+
if (!publicKey2) {
|
|
250
252
|
continue;
|
|
251
253
|
}
|
|
252
|
-
const
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
407
|
+
const access3 = await createTerminalAccess(computer.id);
|
|
333
408
|
spinner.succeed(`Opening terminal for ${chalk2.bold(computer.handle)}`);
|
|
334
|
-
await
|
|
335
|
-
console.log(chalk2.dim(` ${
|
|
409
|
+
await openBrowserURL(access3.access_url);
|
|
410
|
+
console.log(chalk2.dim(` ${access3.access_url}`));
|
|
336
411
|
return;
|
|
337
412
|
}
|
|
338
|
-
const
|
|
413
|
+
const access2 = await createBrowserAccess(computer.id);
|
|
339
414
|
spinner.succeed(`Opening ${chalk2.bold(computer.handle)}`);
|
|
340
|
-
await
|
|
341
|
-
console.log(chalk2.dim(` ${
|
|
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("
|
|
348
|
-
const spinner = ora(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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=$(
|
|
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=$(
|
|
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
|
|
1412
|
+
await openBrowserURL(attempt.loginURL);
|
|
1293
1413
|
} catch {
|
|
1294
1414
|
spinner.stop();
|
|
1295
1415
|
console.log();
|