@pocketenv/cli 0.3.4 → 0.4.0

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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # Pocketenv CLI
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@pocketenv/cli?color=green)](https://www.npmjs.com/package/@pocketenv/cli)
4
+ ![NPM Downloads](https://img.shields.io/npm/dw/%40pocketenv%2Fcli)
4
5
  [![discord](https://img.shields.io/discord/1270021300240252979?label=discord&logo=discord&color=5865F2)](https://discord.gg/9ada4pFUFS)
5
6
  [![License: MPL-2.0](https://img.shields.io/badge/License-MPL_2.0-blue.svg)](https://opensource.org/licenses/MPL-2.0)
6
7
 
package/dist/index.js CHANGED
@@ -23,8 +23,9 @@ import { execSync } from 'child_process';
23
23
  import * as fs from 'fs';
24
24
  import { password, editor, input } from '@inquirer/prompts';
25
25
  import sodium from 'libsodium-wrappers';
26
+ import process$1 from 'node:process';
26
27
 
27
- var version = "0.3.4";
28
+ var version = "0.4.0";
28
29
 
29
30
  async function getAccessToken() {
30
31
  const tokenPath = path.join(os.homedir(), ".pocketenv", "token.json");
@@ -440,7 +441,11 @@ async function waitUntilRunning(name, authToken, timeoutMs = 6e4, intervalMs = 2
440
441
  );
441
442
  }
442
443
 
443
- async function start(name, { ssh: ssh$1, repo }) {
444
+ async function start(name, {
445
+ ssh: ssh$1,
446
+ repo,
447
+ keepAlive
448
+ }) {
444
449
  const token = await getAccessToken();
445
450
  if (repo) repo = expandRepo(repo);
446
451
  try {
@@ -448,7 +453,8 @@ async function start(name, { ssh: ssh$1, repo }) {
448
453
  await client.post(
449
454
  "/xrpc/io.pocketenv.sandbox.startSandbox",
450
455
  {
451
- repo
456
+ repo,
457
+ keepAlive
452
458
  },
453
459
  {
454
460
  params: {
@@ -536,7 +542,7 @@ function detectLightTerminal() {
536
542
  if (!savedState) return false;
537
543
  const tty = fs.openSync("/dev/tty", "r+");
538
544
  try {
539
- execSync("stty raw -echo min 0 time 2 </dev/tty 2>/dev/null");
545
+ execSync("stty -icanon -echo min 0 time 2 </dev/tty 2>/dev/null");
540
546
  fs.writeSync(tty, "\x1B]11;?\x07");
541
547
  let resp = "";
542
548
  const buf = Buffer.alloc(64);
@@ -553,8 +559,14 @@ function detectLightTerminal() {
553
559
  return 0.299 * r + 0.587 * g + 0.114 * b > 127;
554
560
  }
555
561
  } finally {
556
- fs.closeSync(tty);
557
- execSync(`stty ${savedState} </dev/tty 2>/dev/null`);
562
+ try {
563
+ fs.closeSync(tty);
564
+ } catch {
565
+ }
566
+ try {
567
+ execSync(`stty ${savedState} </dev/tty 2>/dev/null`);
568
+ } catch {
569
+ }
558
570
  }
559
571
  } catch {
560
572
  }
@@ -1639,6 +1651,160 @@ async function exec(sandbox, command) {
1639
1651
  }
1640
1652
  }
1641
1653
 
1654
+ dayjs.extend(relativeTime);
1655
+ async function createService(sandboxId, name, command, { ports, description }) {
1656
+ const token = await getAccessToken();
1657
+ try {
1658
+ await client.post(
1659
+ "/xrpc/io.pocketenv.service.addService",
1660
+ {
1661
+ service: {
1662
+ name,
1663
+ command: command.join(" "),
1664
+ description,
1665
+ ports: ports?.map((port) => parseInt(port))
1666
+ }
1667
+ },
1668
+ {
1669
+ params: {
1670
+ sandboxId
1671
+ },
1672
+ headers: {
1673
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1674
+ }
1675
+ }
1676
+ );
1677
+ consola.success(`Service ${c.highlight(name)} created successfully`);
1678
+ } catch (error) {
1679
+ consola.error("Failed to create service", error);
1680
+ process$1.exit(1);
1681
+ }
1682
+ }
1683
+ async function listServices(sandboxId) {
1684
+ const token = await getAccessToken();
1685
+ try {
1686
+ const { data } = await client.get(
1687
+ "/xrpc/io.pocketenv.service.getServices",
1688
+ {
1689
+ params: {
1690
+ sandboxId
1691
+ },
1692
+ headers: {
1693
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1694
+ }
1695
+ }
1696
+ );
1697
+ const table = new Table({
1698
+ head: [
1699
+ c.primary("ID"),
1700
+ c.primary("NAME"),
1701
+ c.primary("COMMAND"),
1702
+ c.primary("STATUS"),
1703
+ c.primary("CREATED AT")
1704
+ ],
1705
+ chars: {
1706
+ top: "",
1707
+ "top-mid": "",
1708
+ "top-left": "",
1709
+ "top-right": "",
1710
+ bottom: "",
1711
+ "bottom-mid": "",
1712
+ "bottom-left": "",
1713
+ "bottom-right": "",
1714
+ left: "",
1715
+ "left-mid": "",
1716
+ mid: "",
1717
+ "mid-mid": "",
1718
+ right: "",
1719
+ "right-mid": "",
1720
+ middle: " "
1721
+ },
1722
+ style: {
1723
+ border: [],
1724
+ head: []
1725
+ }
1726
+ });
1727
+ for (const service of data.services) {
1728
+ table.push([
1729
+ c.secondary(service.id),
1730
+ service.name,
1731
+ service.command,
1732
+ service.status === "RUNNING" ? c.highlight(service.status) : service.status,
1733
+ dayjs(service.createdAt).fromNow()
1734
+ ]);
1735
+ }
1736
+ consola.log(table.toString());
1737
+ } catch (error) {
1738
+ consola.error("Failed to list services", error);
1739
+ process$1.exit(1);
1740
+ }
1741
+ }
1742
+ async function restartService(serviceId) {
1743
+ const token = await getAccessToken();
1744
+ try {
1745
+ await client.post("/xrpc/io.pocketenv.service.restartService", void 0, {
1746
+ params: {
1747
+ serviceId
1748
+ },
1749
+ headers: {
1750
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1751
+ }
1752
+ });
1753
+ } catch (error) {
1754
+ consola.error(`Failed to restart service ${serviceId}`, error);
1755
+ process$1.exit(1);
1756
+ }
1757
+ }
1758
+ async function startService(serviceId) {
1759
+ const token = await getAccessToken();
1760
+ try {
1761
+ await client.post("/xrpc/io.pocketenv.service.startService", void 0, {
1762
+ params: {
1763
+ serviceId
1764
+ },
1765
+ headers: {
1766
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1767
+ }
1768
+ });
1769
+ } catch (error) {
1770
+ consola.error(`Failed to start service ${serviceId}`, error);
1771
+ process$1.exit(1);
1772
+ }
1773
+ }
1774
+ async function stopService(serviceId) {
1775
+ const token = await getAccessToken();
1776
+ try {
1777
+ await client.post("/xrpc/io.pocketenv.service.stopService", void 0, {
1778
+ params: {
1779
+ serviceId
1780
+ },
1781
+ headers: {
1782
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1783
+ }
1784
+ });
1785
+ } catch (error) {
1786
+ consola.error(`Failed to stop service ${serviceId}`, error);
1787
+ process$1.exit(1);
1788
+ }
1789
+ }
1790
+ async function deleteService(serviceId) {
1791
+ const token = await getAccessToken();
1792
+ try {
1793
+ await client.post("/xrpc/io.pocketenv.service.deleteService", void 0, {
1794
+ params: {
1795
+ serviceId
1796
+ },
1797
+ headers: {
1798
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1799
+ }
1800
+ });
1801
+ consola.success(`Service ${c.highlight(serviceId)} deleted successfully`);
1802
+ } catch (error) {
1803
+ consola.error(`Failed to delete service ${serviceId}`, error);
1804
+ process$1.exit(1);
1805
+ }
1806
+ }
1807
+
1642
1808
  const program = new Command();
1643
1809
  program.name("pocketenv").description(
1644
1810
  `${chalk.bold.rgb(0, 232, 198)(`pocketenv v${version}`)} ${c.muted("\u2500")} ${c.muted("Open, interoperable sandbox platform for agents and humans")}`
@@ -1668,6 +1834,9 @@ program.command("ls").description("list sandboxes").action(listSandboxes);
1668
1834
  program.command("start").argument("<sandbox>", "the sandbox to start").option("--ssh, -s", "connect to the Sandbox and automatically open a shell").option(
1669
1835
  "--repo, -r <repo>",
1670
1836
  "the repository to clone into the sandbox (e.g., github:user/repo, tangled:user/repo, or a Git URL)"
1837
+ ).option(
1838
+ "--keep-alive, -k",
1839
+ "keep the sandbox alive, ignoring inactivity timeout"
1671
1840
  ).description("start the given sandbox").action(start);
1672
1841
  program.command("stop").argument("<sandbox>", "the sandbox to stop").description("stop the given sandbox").action(stop);
1673
1842
  program.command("create").aliases(["new"]).option("--provider, -p <provider>", "the provider to use for the sandbox").option(
@@ -1720,6 +1889,13 @@ sshkeys.command("get").argument("<sandbox>", "the sandbox to get the SSH key fro
1720
1889
  const tailscale = program.command("tailscale").description("manage Tailscale");
1721
1890
  tailscale.command("put").argument("<sandbox>", "the sandbox to put the Tailscale Auth Key in").description("put a Tailscale Auth Key in the given sandbox").action(putAuthKey);
1722
1891
  tailscale.command("get").argument("<sandbox>", "the sandbox to get the Tailscale Auth Key from").description("get a Tailscale Auth Key (redacted) from the given sandbox").action(getTailscaleAuthKey);
1892
+ const service = program.command("service").description("manage services");
1893
+ service.command("create").argument("<sandbox>", "the sandbox to create the service in").argument("<name>", "the name of the service").argument("<command...>", "the command to run for the service").option("--description, -d <description>", "a description for the service").option("--ports, -p <ports...>", "a list of ports to expose for the service").description("create a new service in the given sandbox").action(createService);
1894
+ service.command("list").aliases(["ls"]).argument("<sandbox>", "the sandbox to list services for").description("list services in the given sandbox").action(listServices);
1895
+ service.command("delete").aliases(["rm", "remove"]).argument("<service_id>", "the ID of the service to delete").description("delete a service").action(deleteService);
1896
+ service.command("start").argument("<service_id>", "the ID of the service to start").description("start a service").action(startService);
1897
+ service.command("stop").argument("<service_id>", "the ID of the service to stop").description("stop a service").action(stopService);
1898
+ service.command("restart").argument("<service_id>", "the ID of the service to restart").description("restart a service").action(restartService);
1723
1899
  if (process.argv.length <= 2) {
1724
1900
  program.help();
1725
1901
  }
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "bin": {
5
5
  "pocketenv": "dist/index.js"
6
6
  },
7
- "version": "0.3.4",
7
+ "version": "0.4.0",
8
8
  "type": "module",
9
9
  "keywords": [
10
10
  "sandbox",
@@ -0,0 +1,191 @@
1
+ import consola from "consola";
2
+ import { client } from "../client";
3
+ import { env } from "../lib/env";
4
+ import getAccessToken from "../lib/getAccessToken";
5
+ import type { Service } from "../types/service";
6
+ import Table from "cli-table3";
7
+ import dayjs from "dayjs";
8
+ import relativeTime from "dayjs/plugin/relativeTime";
9
+ import { c } from "../theme";
10
+ import process from "node:process";
11
+
12
+ dayjs.extend(relativeTime);
13
+
14
+ type CreateServiceOptions = {
15
+ ports?: string[];
16
+ description?: string;
17
+ };
18
+
19
+ export async function createService(
20
+ sandboxId: string,
21
+ name: string,
22
+ command: string[],
23
+ { ports, description }: CreateServiceOptions,
24
+ ) {
25
+ const token = await getAccessToken();
26
+
27
+ try {
28
+ await client.post(
29
+ "/xrpc/io.pocketenv.service.addService",
30
+ {
31
+ service: {
32
+ name,
33
+ command: command.join(" "),
34
+ description,
35
+ ports: ports?.map((port) => parseInt(port)),
36
+ },
37
+ },
38
+ {
39
+ params: {
40
+ sandboxId,
41
+ },
42
+ headers: {
43
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
44
+ },
45
+ },
46
+ );
47
+
48
+ consola.success(`Service ${c.highlight(name)} created successfully`);
49
+ } catch (error) {
50
+ consola.error("Failed to create service", error);
51
+ process.exit(1);
52
+ }
53
+ }
54
+
55
+ export async function listServices(sandboxId: string) {
56
+ const token = await getAccessToken();
57
+
58
+ try {
59
+ const { data } = await client.get<{ services: Service[] }>(
60
+ "/xrpc/io.pocketenv.service.getServices",
61
+ {
62
+ params: {
63
+ sandboxId,
64
+ },
65
+ headers: {
66
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
67
+ },
68
+ },
69
+ );
70
+
71
+ const table = new Table({
72
+ head: [
73
+ c.primary("ID"),
74
+ c.primary("NAME"),
75
+ c.primary("COMMAND"),
76
+ c.primary("STATUS"),
77
+ c.primary("CREATED AT"),
78
+ ],
79
+ chars: {
80
+ top: "",
81
+ "top-mid": "",
82
+ "top-left": "",
83
+ "top-right": "",
84
+ bottom: "",
85
+ "bottom-mid": "",
86
+ "bottom-left": "",
87
+ "bottom-right": "",
88
+ left: "",
89
+ "left-mid": "",
90
+ mid: "",
91
+ "mid-mid": "",
92
+ right: "",
93
+ "right-mid": "",
94
+ middle: " ",
95
+ },
96
+ style: {
97
+ border: [],
98
+ head: [],
99
+ },
100
+ });
101
+
102
+ for (const service of data.services) {
103
+ table.push([
104
+ c.secondary(service.id),
105
+ service.name,
106
+ service.command,
107
+ service.status === "RUNNING"
108
+ ? c.highlight(service.status)
109
+ : service.status,
110
+ dayjs(service.createdAt).fromNow(),
111
+ ]);
112
+ }
113
+
114
+ consola.log(table.toString());
115
+ } catch (error) {
116
+ consola.error("Failed to list services", error);
117
+ process.exit(1);
118
+ }
119
+ }
120
+
121
+ export async function restartService(serviceId: string) {
122
+ const token = await getAccessToken();
123
+ try {
124
+ await client.post("/xrpc/io.pocketenv.service.restartService", undefined, {
125
+ params: {
126
+ serviceId,
127
+ },
128
+ headers: {
129
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
130
+ },
131
+ });
132
+ } catch (error) {
133
+ consola.error(`Failed to restart service ${serviceId}`, error);
134
+ process.exit(1);
135
+ }
136
+ }
137
+
138
+ export async function startService(serviceId: string) {
139
+ const token = await getAccessToken();
140
+
141
+ try {
142
+ await client.post("/xrpc/io.pocketenv.service.startService", undefined, {
143
+ params: {
144
+ serviceId,
145
+ },
146
+ headers: {
147
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
148
+ },
149
+ });
150
+ } catch (error) {
151
+ consola.error(`Failed to start service ${serviceId}`, error);
152
+ process.exit(1);
153
+ }
154
+ }
155
+
156
+ export async function stopService(serviceId: string) {
157
+ const token = await getAccessToken();
158
+
159
+ try {
160
+ await client.post("/xrpc/io.pocketenv.service.stopService", undefined, {
161
+ params: {
162
+ serviceId,
163
+ },
164
+ headers: {
165
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
166
+ },
167
+ });
168
+ } catch (error) {
169
+ consola.error(`Failed to stop service ${serviceId}`, error);
170
+ process.exit(1);
171
+ }
172
+ }
173
+
174
+ export async function deleteService(serviceId: string) {
175
+ const token = await getAccessToken();
176
+
177
+ try {
178
+ await client.post("/xrpc/io.pocketenv.service.deleteService", undefined, {
179
+ params: {
180
+ serviceId,
181
+ },
182
+ headers: {
183
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
184
+ },
185
+ });
186
+ consola.success(`Service ${c.highlight(serviceId)} deleted successfully`);
187
+ } catch (error) {
188
+ consola.error(`Failed to delete service ${serviceId}`, error);
189
+ process.exit(1);
190
+ }
191
+ }
package/src/cmd/start.ts CHANGED
@@ -9,7 +9,11 @@ import waitUntilRunning from "../lib/waitUntilRunning";
9
9
 
10
10
  async function start(
11
11
  name: string,
12
- { ssh, repo }: { ssh?: boolean; repo?: string },
12
+ {
13
+ ssh,
14
+ repo,
15
+ keepAlive,
16
+ }: { ssh?: boolean; repo?: string; keepAlive?: boolean },
13
17
  ) {
14
18
  const token = await getAccessToken();
15
19
  if (repo) repo = expandRepo(repo);
@@ -20,6 +24,7 @@ async function start(
20
24
  "/xrpc/io.pocketenv.sandbox.startSandbox",
21
25
  {
22
26
  repo,
27
+ keepAlive,
23
28
  },
24
29
  {
25
30
  params: {
package/src/index.ts CHANGED
@@ -23,6 +23,14 @@ import { listPorts } from "./cmd/ports";
23
23
  import { c } from "./theme";
24
24
  import { exposeVscode } from "./cmd/vscode";
25
25
  import { exec } from "./cmd/exec";
26
+ import {
27
+ createService,
28
+ deleteService,
29
+ listServices,
30
+ restartService,
31
+ startService,
32
+ stopService,
33
+ } from "./cmd/service";
26
34
 
27
35
  const program = new Command();
28
36
 
@@ -81,6 +89,10 @@ program
81
89
  "--repo, -r <repo>",
82
90
  "the repository to clone into the sandbox (e.g., github:user/repo, tangled:user/repo, or a Git URL)",
83
91
  )
92
+ .option(
93
+ "--keep-alive, -k",
94
+ "keep the sandbox alive, ignoring inactivity timeout",
95
+ )
84
96
  .description("start the given sandbox")
85
97
  .action(start);
86
98
 
@@ -296,6 +308,50 @@ tailscale
296
308
  .description("get a Tailscale Auth Key (redacted) from the given sandbox")
297
309
  .action(getTailscaleAuthKey);
298
310
 
311
+ const service = program.command("service").description("manage services");
312
+
313
+ service
314
+ .command("create")
315
+ .argument("<sandbox>", "the sandbox to create the service in")
316
+ .argument("<name>", "the name of the service")
317
+ .argument("<command...>", "the command to run for the service")
318
+ .option("--description, -d <description>", "a description for the service")
319
+ .option("--ports, -p <ports...>", "a list of ports to expose for the service")
320
+ .description("create a new service in the given sandbox")
321
+ .action(createService);
322
+
323
+ service
324
+ .command("list")
325
+ .aliases(["ls"])
326
+ .argument("<sandbox>", "the sandbox to list services for")
327
+ .description("list services in the given sandbox")
328
+ .action(listServices);
329
+
330
+ service
331
+ .command("delete")
332
+ .aliases(["rm", "remove"])
333
+ .argument("<service_id>", "the ID of the service to delete")
334
+ .description("delete a service")
335
+ .action(deleteService);
336
+
337
+ service
338
+ .command("start")
339
+ .argument("<service_id>", "the ID of the service to start")
340
+ .description("start a service")
341
+ .action(startService);
342
+
343
+ service
344
+ .command("stop")
345
+ .argument("<service_id>", "the ID of the service to stop")
346
+ .description("stop a service")
347
+ .action(stopService);
348
+
349
+ service
350
+ .command("restart")
351
+ .argument("<service_id>", "the ID of the service to restart")
352
+ .description("restart a service")
353
+ .action(restartService);
354
+
299
355
  if (process.argv.length <= 2) {
300
356
  program.help();
301
357
  }
package/src/theme.ts CHANGED
@@ -27,7 +27,9 @@ function detectLightTerminal(): boolean {
27
27
  if (!savedState) return false;
28
28
  const tty = fs.openSync("/dev/tty", "r+");
29
29
  try {
30
- execSync("stty raw -echo min 0 time 2 </dev/tty 2>/dev/null");
30
+ // Use -icanon -echo instead of raw: avoids disabling ISIG (Ctrl+C) so
31
+ // signal handling stays intact even if the restore below fails.
32
+ execSync("stty -icanon -echo min 0 time 2 </dev/tty 2>/dev/null");
31
33
  fs.writeSync(tty, "\x1b]11;?\x07");
32
34
  // Read in a loop until we see the response terminator (BEL or ST),
33
35
  // so leftover bytes don't leak into the terminal input buffer.
@@ -47,8 +49,8 @@ function detectLightTerminal(): boolean {
47
49
  return 0.299 * r + 0.587 * g + 0.114 * b > 127;
48
50
  }
49
51
  } finally {
50
- fs.closeSync(tty);
51
- execSync(`stty ${savedState} </dev/tty 2>/dev/null`);
52
+ try { fs.closeSync(tty); } catch {}
53
+ try { execSync(`stty ${savedState} </dev/tty 2>/dev/null`); } catch {}
52
54
  }
53
55
  } catch {}
54
56
  }
@@ -0,0 +1,9 @@
1
+ export type Service = {
2
+ id: string;
3
+ name: string;
4
+ ports?: number[];
5
+ command: string;
6
+ description?: string;
7
+ status: "RUNNING" | "STOPPED";
8
+ createdAt: string;
9
+ };