@pocketenv/cli 0.3.2 → 0.3.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/dist/index.js +83 -15
- package/package.json +1 -1
- package/src/cmd/create.ts +2 -0
- package/src/cmd/start.ts +4 -1
- package/src/lib/expandRepo.ts +3 -0
- package/src/lib/waitUntilRunning.ts +27 -0
- package/src/theme.ts +59 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import consola from 'consola';
|
|
5
|
-
import fs from 'fs/promises';
|
|
5
|
+
import fs$1 from 'fs/promises';
|
|
6
6
|
import path from 'path';
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import { env as env$2 } from 'process';
|
|
@@ -13,21 +13,23 @@ import { EventSource } from 'eventsource';
|
|
|
13
13
|
import open from 'open';
|
|
14
14
|
import express from 'express';
|
|
15
15
|
import cors from 'cors';
|
|
16
|
-
import fs$
|
|
16
|
+
import fs$2 from 'node:fs/promises';
|
|
17
17
|
import os$1 from 'node:os';
|
|
18
18
|
import path$1 from 'node:path';
|
|
19
19
|
import Table from 'cli-table3';
|
|
20
20
|
import dayjs from 'dayjs';
|
|
21
21
|
import relativeTime from 'dayjs/plugin/relativeTime.js';
|
|
22
|
+
import { execSync } from 'child_process';
|
|
23
|
+
import * as fs from 'fs';
|
|
22
24
|
import { password, editor, input } from '@inquirer/prompts';
|
|
23
25
|
import sodium from 'libsodium-wrappers';
|
|
24
26
|
|
|
25
|
-
var version = "0.3.
|
|
27
|
+
var version = "0.3.4";
|
|
26
28
|
|
|
27
29
|
async function getAccessToken() {
|
|
28
30
|
const tokenPath = path.join(os.homedir(), ".pocketenv", "token.json");
|
|
29
31
|
try {
|
|
30
|
-
await fs.access(tokenPath);
|
|
32
|
+
await fs$1.access(tokenPath);
|
|
31
33
|
} catch (err) {
|
|
32
34
|
if (!env$2.POCKETENV_TOKEN) {
|
|
33
35
|
consola.error(
|
|
@@ -38,7 +40,7 @@ async function getAccessToken() {
|
|
|
38
40
|
process.exit(1);
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
|
-
const tokenData = await fs.readFile(tokenPath, "utf-8");
|
|
43
|
+
const tokenData = await fs$1.readFile(tokenPath, "utf-8");
|
|
42
44
|
const { token } = JSON.parse(tokenData);
|
|
43
45
|
if (!token) {
|
|
44
46
|
consola.error(
|
|
@@ -415,13 +417,34 @@ function expandRepo(repo) {
|
|
|
415
417
|
if (githubMatch) return `https://github.com/${githubMatch[1]}`;
|
|
416
418
|
const tangledMatch = repo.match(/^tangled:([^/]+\/[^/]+)$/);
|
|
417
419
|
if (tangledMatch) return `https://tangled.org/${tangledMatch[1]}`;
|
|
420
|
+
const gitlabMatch = repo.match(/^gitlab:([^/]+\/[^/]+)$/);
|
|
421
|
+
if (gitlabMatch) return `https://gitlab.com/${gitlabMatch[1]}`;
|
|
418
422
|
return repo;
|
|
419
423
|
}
|
|
420
424
|
|
|
425
|
+
async function waitUntilRunning(name, authToken, timeoutMs = 6e4, intervalMs = 2e3) {
|
|
426
|
+
const deadline = Date.now() + timeoutMs;
|
|
427
|
+
while (Date.now() < deadline) {
|
|
428
|
+
const response = await client.get(
|
|
429
|
+
"/xrpc/io.pocketenv.sandbox.getSandbox",
|
|
430
|
+
{
|
|
431
|
+
params: { id: name },
|
|
432
|
+
headers: { Authorization: `Bearer ${authToken}` }
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
if (response.data.sandbox?.status === "RUNNING") return;
|
|
436
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
437
|
+
}
|
|
438
|
+
throw new Error(
|
|
439
|
+
`Sandbox ${name} did not reach RUNNING state within ${timeoutMs / 1e3}s`
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
421
443
|
async function start(name, { ssh: ssh$1, repo }) {
|
|
422
444
|
const token = await getAccessToken();
|
|
423
445
|
if (repo) repo = expandRepo(repo);
|
|
424
446
|
try {
|
|
447
|
+
const authToken = env$1.POCKETENV_TOKEN || token;
|
|
425
448
|
await client.post(
|
|
426
449
|
"/xrpc/io.pocketenv.sandbox.startSandbox",
|
|
427
450
|
{
|
|
@@ -432,11 +455,12 @@ async function start(name, { ssh: ssh$1, repo }) {
|
|
|
432
455
|
id: name
|
|
433
456
|
},
|
|
434
457
|
headers: {
|
|
435
|
-
Authorization: `Bearer ${
|
|
458
|
+
Authorization: `Bearer ${authToken}`
|
|
436
459
|
}
|
|
437
460
|
}
|
|
438
461
|
);
|
|
439
462
|
if (ssh$1) {
|
|
463
|
+
await waitUntilRunning(name, authToken);
|
|
440
464
|
await ssh(name);
|
|
441
465
|
return;
|
|
442
466
|
}
|
|
@@ -457,8 +481,8 @@ async function login(handle) {
|
|
|
457
481
|
app.post("/token", async (req, res) => {
|
|
458
482
|
console.log(chalk.bold(chalk.greenBright("Login successful!\n")));
|
|
459
483
|
const tokenPath = path$1.join(os$1.homedir(), ".pocketenv", "token.json");
|
|
460
|
-
await fs$
|
|
461
|
-
await fs$
|
|
484
|
+
await fs$2.mkdir(path$1.dirname(tokenPath), { recursive: true });
|
|
485
|
+
await fs$2.writeFile(
|
|
462
486
|
tokenPath,
|
|
463
487
|
JSON.stringify({ token: req.body.token }, null, 2)
|
|
464
488
|
);
|
|
@@ -495,12 +519,55 @@ async function whoami() {
|
|
|
495
519
|
);
|
|
496
520
|
}
|
|
497
521
|
|
|
522
|
+
function detectLightTerminal() {
|
|
523
|
+
const vscodeTheme = process.env.VSCODE_THEME_KIND;
|
|
524
|
+
if (vscodeTheme) {
|
|
525
|
+
return vscodeTheme === "vscode-light" || vscodeTheme === "vscode-high-contrast-light";
|
|
526
|
+
}
|
|
527
|
+
const colorfgbg = process.env.COLORFGBG;
|
|
528
|
+
if (colorfgbg) {
|
|
529
|
+
const parts = colorfgbg.split(";");
|
|
530
|
+
const bg = parseInt(parts[parts.length - 1] ?? "", 10);
|
|
531
|
+
if (!isNaN(bg)) return bg >= 8;
|
|
532
|
+
}
|
|
533
|
+
if (process.stdout.isTTY) {
|
|
534
|
+
try {
|
|
535
|
+
const savedState = execSync("stty -g </dev/tty 2>/dev/null", { encoding: "utf8" }).trim();
|
|
536
|
+
if (!savedState) return false;
|
|
537
|
+
const tty = fs.openSync("/dev/tty", "r+");
|
|
538
|
+
try {
|
|
539
|
+
execSync("stty raw -echo min 0 time 2 </dev/tty 2>/dev/null");
|
|
540
|
+
fs.writeSync(tty, "\x1B]11;?\x07");
|
|
541
|
+
let resp = "";
|
|
542
|
+
const buf = Buffer.alloc(64);
|
|
543
|
+
for (let i = 0; i < 16; i++) {
|
|
544
|
+
const n = fs.readSync(tty, buf, 0, 64, null);
|
|
545
|
+
if (n === 0) break;
|
|
546
|
+
resp += buf.slice(0, n).toString();
|
|
547
|
+
if (resp.includes("\x07") || resp.includes("\x1B\\")) break;
|
|
548
|
+
}
|
|
549
|
+
const m = resp.match(/rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i);
|
|
550
|
+
if (m?.[1] && m[2] && m[3]) {
|
|
551
|
+
const norm = (h) => parseInt(h.slice(0, 2), 16);
|
|
552
|
+
const r = norm(m[1]), g = norm(m[2]), b = norm(m[3]);
|
|
553
|
+
return 0.299 * r + 0.587 * g + 0.114 * b > 127;
|
|
554
|
+
}
|
|
555
|
+
} finally {
|
|
556
|
+
fs.closeSync(tty);
|
|
557
|
+
execSync(`stty ${savedState} </dev/tty 2>/dev/null`);
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
const isLightTerminal = detectLightTerminal();
|
|
498
565
|
const c = {
|
|
499
566
|
primary: (s) => chalk.rgb(0, 232, 198)(s),
|
|
500
567
|
secondary: (s) => chalk.rgb(0, 198, 232)(s),
|
|
501
568
|
accent: (s) => chalk.rgb(130, 100, 255)(s),
|
|
502
569
|
highlight: (s) => chalk.rgb(100, 232, 130)(s),
|
|
503
|
-
muted: (s) => chalk.rgb(200, 210, 220)(s),
|
|
570
|
+
muted: (s) => isLightTerminal ? chalk.black(s) : chalk.rgb(200, 210, 220)(s),
|
|
504
571
|
link: (s) => chalk.rgb(255, 160, 100)(s),
|
|
505
572
|
sky: (s) => chalk.rgb(0, 210, 255)(s),
|
|
506
573
|
error: (s) => chalk.rgb(255, 100, 100)(s)
|
|
@@ -619,6 +686,7 @@ async function createSandbox(name, {
|
|
|
619
686
|
);
|
|
620
687
|
return;
|
|
621
688
|
}
|
|
689
|
+
await waitUntilRunning(sandbox.data.name, token);
|
|
622
690
|
await ssh(sandbox.data.name);
|
|
623
691
|
} catch (error) {
|
|
624
692
|
consola.error(`Failed to create sandbox: ${error}`);
|
|
@@ -628,12 +696,12 @@ async function createSandbox(name, {
|
|
|
628
696
|
async function logout() {
|
|
629
697
|
const tokenPath = path$1.join(os$1.homedir(), ".pocketenv", "token.json");
|
|
630
698
|
try {
|
|
631
|
-
await fs$
|
|
699
|
+
await fs$2.access(tokenPath);
|
|
632
700
|
} catch {
|
|
633
701
|
consola.log("Logged out successfully");
|
|
634
702
|
return;
|
|
635
703
|
}
|
|
636
|
-
await fs$
|
|
704
|
+
await fs$2.unlink(tokenPath);
|
|
637
705
|
consola.log("Logged out successfully");
|
|
638
706
|
}
|
|
639
707
|
|
|
@@ -1032,10 +1100,10 @@ async function putKeys(sandbox, options) {
|
|
|
1032
1100
|
publicKey = generated.publicKey;
|
|
1033
1101
|
}
|
|
1034
1102
|
if (options.privateKey && !options.generate) {
|
|
1035
|
-
privateKey = await fs$
|
|
1103
|
+
privateKey = await fs$2.readFile(options.privateKey, "utf8");
|
|
1036
1104
|
}
|
|
1037
1105
|
if (options.publicKey && !options.generate) {
|
|
1038
|
-
publicKey = await fs$
|
|
1106
|
+
publicKey = await fs$2.readFile(options.publicKey, "utf8");
|
|
1039
1107
|
}
|
|
1040
1108
|
const validatePrivateKey = (value) => {
|
|
1041
1109
|
const trimmed = value.trim();
|
|
@@ -1360,12 +1428,12 @@ async function putFile(sandbox, remotePath, localPath) {
|
|
|
1360
1428
|
} else if (localPath) {
|
|
1361
1429
|
const resolvedPath = path.resolve(localPath);
|
|
1362
1430
|
try {
|
|
1363
|
-
await fs.access(resolvedPath);
|
|
1431
|
+
await fs$1.access(resolvedPath);
|
|
1364
1432
|
} catch (err) {
|
|
1365
1433
|
consola.error(`No such file: ${c.error(localPath)}`);
|
|
1366
1434
|
process.exit(1);
|
|
1367
1435
|
}
|
|
1368
|
-
content = await fs.readFile(resolvedPath, "utf-8");
|
|
1436
|
+
content = await fs$1.readFile(resolvedPath, "utf-8");
|
|
1369
1437
|
} else {
|
|
1370
1438
|
content = (await editor({
|
|
1371
1439
|
message: "File content (opens in $EDITOR):",
|
package/package.json
CHANGED
package/src/cmd/create.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { Sandbox } from "../types/sandbox";
|
|
|
5
5
|
import connectToSandbox from "./ssh";
|
|
6
6
|
import { c } from "../theme";
|
|
7
7
|
import { expandRepo } from "../lib/expandRepo";
|
|
8
|
+
import waitUntilRunning from "../lib/waitUntilRunning";
|
|
8
9
|
|
|
9
10
|
async function createSandbox(
|
|
10
11
|
name: string,
|
|
@@ -52,6 +53,7 @@ async function createSandbox(
|
|
|
52
53
|
);
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
56
|
+
await waitUntilRunning(sandbox.data.name, token);
|
|
55
57
|
await connectToSandbox(sandbox.data.name);
|
|
56
58
|
} catch (error) {
|
|
57
59
|
consola.error(`Failed to create sandbox: ${error}`);
|
package/src/cmd/start.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { client } from "../client";
|
|
|
5
5
|
import { env } from "../lib/env";
|
|
6
6
|
import connectToSandbox from "./ssh";
|
|
7
7
|
import { expandRepo } from "../lib/expandRepo";
|
|
8
|
+
import waitUntilRunning from "../lib/waitUntilRunning";
|
|
8
9
|
|
|
9
10
|
async function start(
|
|
10
11
|
name: string,
|
|
@@ -14,6 +15,7 @@ async function start(
|
|
|
14
15
|
if (repo) repo = expandRepo(repo);
|
|
15
16
|
|
|
16
17
|
try {
|
|
18
|
+
const authToken = env.POCKETENV_TOKEN || token;
|
|
17
19
|
await client.post(
|
|
18
20
|
"/xrpc/io.pocketenv.sandbox.startSandbox",
|
|
19
21
|
{
|
|
@@ -24,12 +26,13 @@ async function start(
|
|
|
24
26
|
id: name,
|
|
25
27
|
},
|
|
26
28
|
headers: {
|
|
27
|
-
Authorization: `Bearer ${
|
|
29
|
+
Authorization: `Bearer ${authToken}`,
|
|
28
30
|
},
|
|
29
31
|
},
|
|
30
32
|
);
|
|
31
33
|
|
|
32
34
|
if (ssh) {
|
|
35
|
+
await waitUntilRunning(name, authToken);
|
|
33
36
|
await connectToSandbox(name);
|
|
34
37
|
return;
|
|
35
38
|
}
|
package/src/lib/expandRepo.ts
CHANGED
|
@@ -5,5 +5,8 @@ export function expandRepo(repo: string): string {
|
|
|
5
5
|
const tangledMatch = repo.match(/^tangled:([^/]+\/[^/]+)$/);
|
|
6
6
|
if (tangledMatch) return `https://tangled.org/${tangledMatch[1]}`;
|
|
7
7
|
|
|
8
|
+
const gitlabMatch = repo.match(/^gitlab:([^/]+\/[^/]+)$/);
|
|
9
|
+
if (gitlabMatch) return `https://gitlab.com/${gitlabMatch[1]}`;
|
|
10
|
+
|
|
8
11
|
return repo;
|
|
9
12
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { client } from "../client";
|
|
2
|
+
import type { Sandbox } from "../types/sandbox";
|
|
3
|
+
|
|
4
|
+
async function waitUntilRunning(
|
|
5
|
+
name: string,
|
|
6
|
+
authToken: string,
|
|
7
|
+
timeoutMs = 60_000,
|
|
8
|
+
intervalMs = 2_000,
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const deadline = Date.now() + timeoutMs;
|
|
11
|
+
while (Date.now() < deadline) {
|
|
12
|
+
const response = await client.get<{ sandbox: Sandbox | null }>(
|
|
13
|
+
"/xrpc/io.pocketenv.sandbox.getSandbox",
|
|
14
|
+
{
|
|
15
|
+
params: { id: name },
|
|
16
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
if (response.data.sandbox?.status === "RUNNING") return;
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
21
|
+
}
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Sandbox ${name} did not reach RUNNING state within ${timeoutMs / 1000}s`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default waitUntilRunning;
|
package/src/theme.ts
CHANGED
|
@@ -1,11 +1,69 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
|
|
5
|
+
function detectLightTerminal(): boolean {
|
|
6
|
+
// VS Code terminal
|
|
7
|
+
const vscodeTheme = process.env.VSCODE_THEME_KIND;
|
|
8
|
+
if (vscodeTheme) {
|
|
9
|
+
return vscodeTheme === "vscode-light" || vscodeTheme === "vscode-high-contrast-light";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// COLORFGBG — set by xterm, iTerm2, etc. ("fg;bg", bg >= 8 = light)
|
|
13
|
+
const colorfgbg = process.env.COLORFGBG;
|
|
14
|
+
if (colorfgbg) {
|
|
15
|
+
const parts = colorfgbg.split(";");
|
|
16
|
+
const bg = parseInt(parts[parts.length - 1] ?? "", 10);
|
|
17
|
+
if (!isNaN(bg)) return bg >= 8;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// OSC 11 background color query — works with Apple Terminal, iTerm2, etc.
|
|
21
|
+
// stty is redirected from /dev/tty explicitly because execSync pipes stdio,
|
|
22
|
+
// which means stty would otherwise fail to find the terminal.
|
|
23
|
+
if (process.stdout.isTTY) {
|
|
24
|
+
try {
|
|
25
|
+
const savedState = execSync("stty -g </dev/tty 2>/dev/null", { encoding: "utf8" }).trim();
|
|
26
|
+
// If we couldn't save the state, skip to avoid leaving the terminal in raw mode.
|
|
27
|
+
if (!savedState) return false;
|
|
28
|
+
const tty = fs.openSync("/dev/tty", "r+");
|
|
29
|
+
try {
|
|
30
|
+
execSync("stty raw -echo min 0 time 2 </dev/tty 2>/dev/null");
|
|
31
|
+
fs.writeSync(tty, "\x1b]11;?\x07");
|
|
32
|
+
// Read in a loop until we see the response terminator (BEL or ST),
|
|
33
|
+
// so leftover bytes don't leak into the terminal input buffer.
|
|
34
|
+
let resp = "";
|
|
35
|
+
const buf = Buffer.alloc(64);
|
|
36
|
+
for (let i = 0; i < 16; i++) {
|
|
37
|
+
const n = fs.readSync(tty, buf, 0, 64, null);
|
|
38
|
+
if (n === 0) break;
|
|
39
|
+
resp += buf.slice(0, n).toString();
|
|
40
|
+
if (resp.includes("\x07") || resp.includes("\x1b\\")) break;
|
|
41
|
+
}
|
|
42
|
+
const m = resp.match(/rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i);
|
|
43
|
+
if (m?.[1] && m[2] && m[3]) {
|
|
44
|
+
// Components can be 2 or 4 hex digits; normalize to 0-255
|
|
45
|
+
const norm = (h: string) => parseInt(h.slice(0, 2), 16);
|
|
46
|
+
const r = norm(m[1]), g = norm(m[2]), b = norm(m[3]);
|
|
47
|
+
return 0.299 * r + 0.587 * g + 0.114 * b > 127;
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
fs.closeSync(tty);
|
|
51
|
+
execSync(`stty ${savedState} </dev/tty 2>/dev/null`);
|
|
52
|
+
}
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isLightTerminal = detectLightTerminal();
|
|
2
60
|
|
|
3
61
|
export const c = {
|
|
4
62
|
primary: (s: string | number) => chalk.rgb(0, 232, 198)(s),
|
|
5
63
|
secondary: (s: string | number) => chalk.rgb(0, 198, 232)(s),
|
|
6
64
|
accent: (s: string | number) => chalk.rgb(130, 100, 255)(s),
|
|
7
65
|
highlight: (s: string | number) => chalk.rgb(100, 232, 130)(s),
|
|
8
|
-
muted: (s: string | number) => chalk.rgb(200, 210, 220)(s),
|
|
66
|
+
muted: (s: string | number) => isLightTerminal ? chalk.black(s) : chalk.rgb(200, 210, 220)(s),
|
|
9
67
|
link: (s: string | number) => chalk.rgb(255, 160, 100)(s),
|
|
10
68
|
sky: (s: string | number) => chalk.rgb(0, 210, 255)(s),
|
|
11
69
|
error: (s: string | number) => chalk.rgb(255, 100, 100)(s),
|