@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 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$1 from 'node:fs/promises';
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.2";
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 ${env$1.POCKETENV_TOKEN || token}`
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$1.mkdir(path$1.dirname(tokenPath), { recursive: true });
461
- await fs$1.writeFile(
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$1.access(tokenPath);
699
+ await fs$2.access(tokenPath);
632
700
  } catch {
633
701
  consola.log("Logged out successfully");
634
702
  return;
635
703
  }
636
- await fs$1.unlink(tokenPath);
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$1.readFile(options.privateKey, "utf8");
1103
+ privateKey = await fs$2.readFile(options.privateKey, "utf8");
1036
1104
  }
1037
1105
  if (options.publicKey && !options.generate) {
1038
- publicKey = await fs$1.readFile(options.publicKey, "utf8");
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
@@ -4,7 +4,7 @@
4
4
  "bin": {
5
5
  "pocketenv": "dist/index.js"
6
6
  },
7
- "version": "0.3.2",
7
+ "version": "0.3.4",
8
8
  "type": "module",
9
9
  "keywords": [
10
10
  "sandbox",
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 ${env.POCKETENV_TOKEN || token}`,
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
  }
@@ -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),