@pocketenv/cli 0.2.4 → 0.3.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/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "bin": {
5
5
  "pocketenv": "dist/index.js"
6
6
  },
7
- "version": "0.2.4",
7
+ "version": "0.3.0",
8
8
  "type": "module",
9
9
  "keywords": [
10
10
  "sandbox",
package/src/cmd/create.ts CHANGED
@@ -2,8 +2,8 @@ import consola from "consola";
2
2
  import { client } from "../client";
3
3
  import getAccessToken from "../lib/getAccessToken";
4
4
  import type { Sandbox } from "../types/sandbox";
5
- import chalk from "chalk";
6
5
  import connectToSandbox from "./ssh";
6
+ import { c } from "../theme";
7
7
 
8
8
  async function createSandbox(
9
9
  name: string,
@@ -21,7 +21,7 @@ async function createSandbox(
21
21
 
22
22
  if (["deno", "vercel", "daytona"].includes(provider || "")) {
23
23
  consola.error(
24
- `This Sandbox Runtime is temporarily disabled. ${chalk.greenBright(provider ?? "")}`,
24
+ `This Sandbox Runtime is temporarily disabled. ${c.primary(provider ?? "")}`,
25
25
  );
26
26
  process.exit(1);
27
27
  }
@@ -43,7 +43,7 @@ async function createSandbox(
43
43
  );
44
44
  if (!ssh) {
45
45
  consola.success(
46
- `Sandbox created successfully: ${chalk.greenBright(sandbox.data.name)}`,
46
+ `Sandbox created successfully: ${c.primary(sandbox.data.name)}`,
47
47
  );
48
48
  return;
49
49
  }
package/src/cmd/env.ts CHANGED
@@ -2,11 +2,11 @@ import { client } from "../client";
2
2
  import getAccessToken from "../lib/getAccessToken";
3
3
  import type { Sandbox } from "../types/sandbox";
4
4
  import type { Variable } from "../types/variable";
5
- import chalk from "chalk";
6
5
  import dayjs from "dayjs";
7
6
  import consola from "consola";
8
7
  import Table from "cli-table3";
9
8
  import { env } from "../lib/env";
9
+ import { c } from "../theme";
10
10
 
11
11
  export async function listEnvs(sandbox: string) {
12
12
  const token = await getAccessToken();
@@ -23,7 +23,7 @@ export async function listEnvs(sandbox: string) {
23
23
  );
24
24
 
25
25
  if (!data.sandbox) {
26
- consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
26
+ consola.error(`Sandbox not found: ${c.primary(sandbox)}`);
27
27
  process.exit(1);
28
28
  }
29
29
 
@@ -43,10 +43,10 @@ export async function listEnvs(sandbox: string) {
43
43
 
44
44
  const table = new Table({
45
45
  head: [
46
- chalk.cyan("ID"),
47
- chalk.cyan("NAME"),
48
- chalk.cyan("VALUE"),
49
- chalk.cyan("CREATED AT"),
46
+ c.primary("ID"),
47
+ c.primary("NAME"),
48
+ c.primary("VALUE"),
49
+ c.primary("CREATED AT"),
50
50
  ],
51
51
  chars: {
52
52
  top: "",
@@ -73,8 +73,8 @@ export async function listEnvs(sandbox: string) {
73
73
 
74
74
  for (const variable of response.data.variables) {
75
75
  table.push([
76
- chalk.greenBright(variable.id),
77
- chalk.greenBright(variable.name),
76
+ c.secondary(variable.id),
77
+ c.highlight(variable.name),
78
78
  variable.value,
79
79
  dayjs(variable.createdAt).fromNow(),
80
80
  ]);
@@ -0,0 +1,38 @@
1
+ import consola from "consola";
2
+ import getAccessToken from "../lib/getAccessToken";
3
+ import { client } from "../client";
4
+ import { env } from "../lib/env";
5
+ import { c } from "../theme";
6
+
7
+ export async function exposePort(
8
+ sandbox: string,
9
+ port: number,
10
+ description?: string,
11
+ ) {
12
+ const token = await getAccessToken();
13
+ try {
14
+ const response = await client.post<{ previewUrl?: string }>(
15
+ `/xrpc/io.pocketenv.sandbox.exposePort`,
16
+ { port, description },
17
+ {
18
+ params: {
19
+ id: sandbox,
20
+ },
21
+ headers: {
22
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
23
+ },
24
+ },
25
+ );
26
+
27
+ consola.success(
28
+ `Port ${c.primary(port)} exposed for sandbox ${c.primary(sandbox)}`,
29
+ );
30
+
31
+ if (response.data.previewUrl) {
32
+ consola.success(`Preview URL: ${c.secondary(response.data.previewUrl)}`);
33
+ }
34
+ } catch (error) {
35
+ consola.error("Failed to expose port:", error);
36
+ process.exit(1);
37
+ }
38
+ }
@@ -0,0 +1,139 @@
1
+ import consola from "consola";
2
+ import getAccessToken from "../lib/getAccessToken";
3
+ import dayjs from "dayjs";
4
+ import relativeTime from "dayjs/plugin/relativeTime";
5
+ import { client } from "../client";
6
+ import { env } from "../lib/env";
7
+ import CliTable3 from "cli-table3";
8
+ import type { File } from "../types/file";
9
+ import { c } from "../theme";
10
+ import { editor } from "@inquirer/prompts";
11
+ import fs from "fs/promises";
12
+ import path from "path";
13
+ import encrypt from "../lib/sodium";
14
+
15
+ dayjs.extend(relativeTime);
16
+
17
+ export async function putFile(
18
+ sandbox: string,
19
+ remotePath: string,
20
+ localPath?: string,
21
+ ) {
22
+ const token = await getAccessToken();
23
+
24
+ let content: string;
25
+ if (!process.stdin.isTTY) {
26
+ const chunks: Buffer[] = [];
27
+ for await (const chunk of process.stdin) chunks.push(chunk);
28
+ content = Buffer.concat(chunks).toString().trim();
29
+ } else if (localPath) {
30
+ const resolvedPath = path.resolve(localPath);
31
+ try {
32
+ await fs.access(resolvedPath);
33
+ } catch (err) {
34
+ consola.error(`No such file: ${c.error(localPath)}`);
35
+ process.exit(1);
36
+ }
37
+ content = await fs.readFile(resolvedPath, "utf-8");
38
+ } else {
39
+ content = (
40
+ await editor({
41
+ message: "File content (opens in $EDITOR):",
42
+ waitForUserInput: false,
43
+ })
44
+ ).trim();
45
+ }
46
+
47
+ try {
48
+ await client.post(
49
+ "/xrpc/io.pocketenv.file.addFile",
50
+ {
51
+ file: {
52
+ sandboxId: sandbox,
53
+ path: remotePath,
54
+ content: await encrypt(content),
55
+ },
56
+ },
57
+ {
58
+ headers: {
59
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
60
+ },
61
+ },
62
+ );
63
+
64
+ consola.success(
65
+ `File ${c.primary(remotePath)} successfully created in sandbox ${c.primary(sandbox)}`,
66
+ );
67
+ } catch (error) {
68
+ consola.error(`Failed to create file: ${error}`);
69
+ }
70
+ }
71
+
72
+ export async function listFiles(sandboxId: string) {
73
+ const token = await getAccessToken();
74
+
75
+ const response = await client.get<{ files: File[] }>(
76
+ "/xrpc/io.pocketenv.file.getFiles",
77
+ {
78
+ params: {
79
+ sandboxId,
80
+ },
81
+ headers: {
82
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
83
+ },
84
+ },
85
+ );
86
+
87
+ const table = new CliTable3({
88
+ head: [c.primary("ID"), c.primary("PATH"), c.primary("CREATED AT")],
89
+ chars: {
90
+ top: "",
91
+ "top-mid": "",
92
+ "top-left": "",
93
+ "top-right": "",
94
+ bottom: "",
95
+ "bottom-mid": "",
96
+ "bottom-left": "",
97
+ "bottom-right": "",
98
+ left: "",
99
+ "left-mid": "",
100
+ mid: "",
101
+ "mid-mid": "",
102
+ right: "",
103
+ "right-mid": "",
104
+ middle: " ",
105
+ },
106
+ style: {
107
+ border: [],
108
+ head: [],
109
+ },
110
+ });
111
+
112
+ for (const file of response.data.files) {
113
+ table.push([
114
+ c.secondary(file.id),
115
+ file.path,
116
+ dayjs(file.createdAt).fromNow(),
117
+ ]);
118
+ }
119
+
120
+ consola.log(table.toString());
121
+ }
122
+
123
+ export async function deleteFile(id: string) {
124
+ const token = await getAccessToken();
125
+
126
+ try {
127
+ await client.post(`/xrpc/io.pocketenv.file.deleteFile`, undefined, {
128
+ params: {
129
+ id,
130
+ },
131
+ headers: {
132
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
133
+ },
134
+ });
135
+ consola.success(`File ${c.primary(id)} successfully deleted from sandbox`);
136
+ } catch (error) {
137
+ consola.error(`Failed to delete file: ${error}`);
138
+ }
139
+ }
package/src/cmd/list.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { client } from "../client";
2
- import chalk from "chalk";
3
2
  import consola from "consola";
4
3
  import { env } from "../lib/env";
5
4
  import getAccessToken from "../lib/getAccessToken";
@@ -8,6 +7,7 @@ import Table from "cli-table3";
8
7
  import dayjs from "dayjs";
9
8
  import relativeTime from "dayjs/plugin/relativeTime";
10
9
  import type { Profile } from "../types/profile";
10
+ import { c } from "../theme";
11
11
  dayjs.extend(relativeTime);
12
12
 
13
13
  async function listSandboxes() {
@@ -35,10 +35,10 @@ async function listSandboxes() {
35
35
 
36
36
  const table = new Table({
37
37
  head: [
38
- chalk.cyan("NAME"),
39
- chalk.cyan("BASE"),
40
- chalk.cyan("STATUS"),
41
- chalk.cyan("CREATED AT"),
38
+ c.primary("NAME"),
39
+ c.primary("BASE"),
40
+ c.primary("STATUS"),
41
+ c.primary("CREATED AT"),
42
42
  ],
43
43
  chars: {
44
44
  top: "",
@@ -65,10 +65,10 @@ async function listSandboxes() {
65
65
 
66
66
  for (const sandbox of response.data.sandboxes) {
67
67
  table.push([
68
- chalk.greenBright(sandbox.name),
68
+ c.secondary(sandbox.name),
69
69
  sandbox.baseSandbox,
70
70
  sandbox.status === "RUNNING"
71
- ? chalk.greenBright(sandbox.status)
71
+ ? c.highlight(sandbox.status)
72
72
  : sandbox.status,
73
73
  dayjs(sandbox.createdAt).fromNow(),
74
74
  ]);
@@ -0,0 +1,63 @@
1
+ import chalk from "chalk";
2
+ import { client } from "../client";
3
+ import { env } from "../lib/env";
4
+ import getAccessToken from "../lib/getAccessToken";
5
+ import type { Port } from "../types/port";
6
+ import CliTable3 from "cli-table3";
7
+ import consola from "consola";
8
+ import { c } from "../theme";
9
+
10
+ export async function listPorts(sandbox: string) {
11
+ const token = await getAccessToken();
12
+
13
+ const response = await client.get<{ ports: Port[] }>(
14
+ "/xrpc/io.pocketenv.sandbox.getExposedPorts",
15
+ {
16
+ params: {
17
+ id: sandbox,
18
+ },
19
+ headers: {
20
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
21
+ },
22
+ },
23
+ );
24
+
25
+ const table = new CliTable3({
26
+ head: [
27
+ c.primary("PORT"),
28
+ c.primary("DESCRIPTION"),
29
+ c.primary("PREVIEW URL"),
30
+ ],
31
+ chars: {
32
+ top: "",
33
+ "top-mid": "",
34
+ "top-left": "",
35
+ "top-right": "",
36
+ bottom: "",
37
+ "bottom-mid": "",
38
+ "bottom-left": "",
39
+ "bottom-right": "",
40
+ left: "",
41
+ "left-mid": "",
42
+ mid: "",
43
+ "mid-mid": "",
44
+ right: "",
45
+ "right-mid": "",
46
+ middle: " ",
47
+ },
48
+ style: {
49
+ border: [],
50
+ head: [],
51
+ },
52
+ });
53
+
54
+ for (const port of response.data.ports) {
55
+ table.push([
56
+ c.secondary(port.port),
57
+ port.description || "-",
58
+ c.link(port.previewUrl || "-"),
59
+ ]);
60
+ }
61
+
62
+ consola.log(table.toString());
63
+ }
package/src/cmd/secret.ts CHANGED
@@ -10,6 +10,7 @@ import dayjs from "dayjs";
10
10
  import relativeTime from "dayjs/plugin/relativeTime";
11
11
  import { env } from "../lib/env";
12
12
  import encrypt from "../lib/sodium";
13
+ import { c } from "../theme";
13
14
 
14
15
  dayjs.extend(relativeTime);
15
16
 
@@ -41,7 +42,7 @@ export async function listSecrets(sandbox: string) {
41
42
  );
42
43
 
43
44
  const table = new Table({
44
- head: [chalk.cyan("ID"), chalk.cyan("NAME"), chalk.cyan("CREATED AT")],
45
+ head: [c.primary("ID"), c.primary("NAME"), c.primary("CREATED AT")],
45
46
  chars: {
46
47
  top: "",
47
48
  "top-mid": "",
@@ -67,8 +68,8 @@ export async function listSecrets(sandbox: string) {
67
68
 
68
69
  for (const secret of response.data.secrets) {
69
70
  table.push([
70
- chalk.greenBright(secret.id),
71
- chalk.greenBright(secret.name),
71
+ c.secondary(secret.id),
72
+ c.highlight(secret.name),
72
73
  dayjs(secret.createdAt).fromNow(),
73
74
  ]);
74
75
  }
@@ -78,7 +79,15 @@ export async function listSecrets(sandbox: string) {
78
79
 
79
80
  export async function putSecret(sandbox: string, key: string) {
80
81
  const token = await getAccessToken();
81
- const value = await password({ message: "Enter secret value" });
82
+ const isStdinPiped = !process.stdin.isTTY;
83
+ const value = isStdinPiped
84
+ ? await new Promise<string>((resolve) => {
85
+ let data = "";
86
+ process.stdin.setEncoding("utf8");
87
+ process.stdin.on("data", (chunk) => (data += chunk));
88
+ process.stdin.on("end", () => resolve(data.trimEnd()));
89
+ })
90
+ : await password({ message: "Enter secret value" });
82
91
 
83
92
  const { data } = await client.get("/xrpc/io.pocketenv.sandbox.getSandbox", {
84
93
  params: {
@@ -94,21 +103,27 @@ export async function putSecret(sandbox: string, key: string) {
94
103
  process.exit(1);
95
104
  }
96
105
 
97
- await client.post(
98
- "/xrpc/io.pocketenv.secret.addSecret",
99
- {
100
- secret: {
101
- sandboxId: data.sandbox.id,
102
- name: key,
103
- value: await encrypt(value),
106
+ try {
107
+ await client.post(
108
+ "/xrpc/io.pocketenv.secret.addSecret",
109
+ {
110
+ secret: {
111
+ sandboxId: data.sandbox.id,
112
+ name: key,
113
+ value: await encrypt(value),
114
+ },
104
115
  },
105
- },
106
- {
107
- headers: {
108
- Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
116
+ {
117
+ headers: {
118
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
119
+ },
109
120
  },
110
- },
111
- );
121
+ );
122
+
123
+ consola.success("Secret added successfully");
124
+ } catch (error) {
125
+ consola.error("Failed to add secret:", error);
126
+ }
112
127
  }
113
128
 
114
129
  export async function deleteSecret(id: string) {
@@ -125,7 +140,7 @@ export async function deleteSecret(id: string) {
125
140
  });
126
141
 
127
142
  consola.success("Secret deleted successfully");
128
- } catch {
129
- consola.error("Failed to delete secret");
143
+ } catch (error) {
144
+ consola.error("Failed to delete secret:", error);
130
145
  }
131
146
  }
@@ -129,7 +129,16 @@ async function ssh(sandbox: Sandbox): Promise<void> {
129
129
  process.stdin.resume();
130
130
 
131
131
  // stdin → POST /tty/:id/input
132
+ // In raw mode the OS never raises SIGINT — Ctrl+C arrives as a raw byte
133
+ // in the data stream and is forwarded to the remote shell as-is.
134
+ // We use Ctrl+K (\x0b) as a local-only escape hatch to avoid conflicting
135
+ // with Ctrl+C semantics inside the remote shell.
132
136
  process.stdin.on("data", (chunk: Buffer) => {
137
+ if (chunk.includes(0x0b)) {
138
+ // Ctrl+K pressed — tear down immediately without waiting for the server.
139
+ teardown(0);
140
+ return;
141
+ }
133
142
  sendInput(ttyUrl, sandbox.id, chunk, authToken);
134
143
  });
135
144
 
@@ -195,11 +204,18 @@ async function ssh(sandbox: Sandbox): Promise<void> {
195
204
  es.onerror = (err: ErrorEvent) => {
196
205
  // The eventsource package exposes readyState on the EventSource instance.
197
206
  if (es && es.readyState === EventSource.CLOSED) {
198
- const detail = err.message ? ` (${err.message})` : "";
199
- process.stderr.write(
200
- `\r\n${chalk.red(`Terminal connection lost${detail}`)}\r\n`,
201
- );
202
- teardown(1);
207
+ // If the shell exited cleanly the server will close the SSE stream with
208
+ // no error message. Treat a message-less close as a graceful exit (code
209
+ // 0) rather than a connection error, so the user isn't shown a red
210
+ // "connection lost" banner after a normal `exit`.
211
+ if (!err.message) {
212
+ teardown(0);
213
+ } else {
214
+ process.stderr.write(
215
+ `\r\n${chalk.red(`Terminal connection lost (${err.message})`)}\r\n`,
216
+ );
217
+ teardown(1);
218
+ }
203
219
  }
204
220
  };
205
221
 
package/src/cmd/start.ts CHANGED
@@ -3,8 +3,9 @@ import chalk from "chalk";
3
3
  import getAccessToken from "../lib/getAccessToken";
4
4
  import { client } from "../client";
5
5
  import { env } from "../lib/env";
6
+ import connectToSandbox from "./ssh";
6
7
 
7
- async function start(name: string) {
8
+ async function start(name: string, { ssh }: { ssh?: boolean }) {
8
9
  const token = await getAccessToken();
9
10
 
10
11
  try {
@@ -17,6 +18,11 @@ async function start(name: string) {
17
18
  },
18
19
  });
19
20
 
21
+ if (ssh) {
22
+ await connectToSandbox(name);
23
+ return;
24
+ }
25
+
20
26
  consola.success(`Sandbox ${chalk.greenBright(name)} started`);
21
27
  consola.log(
22
28
  `Run ${chalk.greenBright(`pocketenv console ${name}`)} to access the sandbox`,
@@ -3,10 +3,10 @@ import getAccessToken from "../lib/getAccessToken";
3
3
  import { client } from "../client";
4
4
  import type { Sandbox } from "../types/sandbox";
5
5
  import consola from "consola";
6
- import chalk from "chalk";
7
6
  import type { TailscaleAuthKey } from "../types/tailscale-auth-key";
8
7
  import { env } from "../lib/env";
9
8
  import encrypt from "../lib/sodium";
9
+ import { c } from "../theme";
10
10
 
11
11
  export async function putAuthKey(sandbox: string) {
12
12
  const token = await getAccessToken();
@@ -33,7 +33,7 @@ export async function putAuthKey(sandbox: string) {
33
33
  );
34
34
 
35
35
  if (!data.sandbox) {
36
- consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
36
+ consola.error(`Sandbox not found: ${c.primary(sandbox)}`);
37
37
  process.exit(1);
38
38
  }
39
39
 
@@ -60,7 +60,7 @@ export async function putAuthKey(sandbox: string) {
60
60
 
61
61
  consola.success(redacted);
62
62
  consola.success(
63
- `Tailscale auth key saved for sandbox: ${chalk.greenBright(sandbox)}`,
63
+ `Tailscale auth key saved for sandbox: ${c.primary(sandbox)}`,
64
64
  );
65
65
  }
66
66
 
@@ -80,7 +80,7 @@ export async function getTailscaleAuthKey(sandbox: string) {
80
80
  );
81
81
 
82
82
  if (!data.sandbox) {
83
- consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
83
+ consola.error(`Sandbox not found: ${c.primary(sandbox)}`);
84
84
  process.exit(1);
85
85
  }
86
86
 
@@ -96,10 +96,10 @@ export async function getTailscaleAuthKey(sandbox: string) {
96
96
  },
97
97
  },
98
98
  );
99
- consola.info(`Tailscale auth key: ${chalk.greenBright(tailscale.authKey)}`);
99
+ consola.info(`Tailscale auth key: ${c.primary(tailscale.authKey)}`);
100
100
  } catch {
101
101
  consola.error(
102
- `No Tailscale Auth Key found for sandbox: ${chalk.greenBright(sandbox)}`,
102
+ `No Tailscale Auth Key found for sandbox: ${c.primary(sandbox)}`,
103
103
  );
104
104
  process.exit(1);
105
105
  }
@@ -0,0 +1,31 @@
1
+ import consola from "consola";
2
+ import getAccessToken from "../lib/getAccessToken";
3
+ import { client } from "../client";
4
+ import { env } from "../lib/env";
5
+ import { c } from "../theme";
6
+
7
+ export async function unexposePort(sandbox: string, port: number) {
8
+ const token = await getAccessToken();
9
+
10
+ try {
11
+ await client.post(
12
+ `/xrpc/io.pocketenv.sandbox.unexposePort`,
13
+ { port },
14
+ {
15
+ params: {
16
+ id: sandbox,
17
+ },
18
+ headers: {
19
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
20
+ },
21
+ },
22
+ );
23
+
24
+ consola.success(
25
+ `Port ${c.primary(port)} unexposed for sandbox ${c.primary(sandbox)}`,
26
+ );
27
+ } catch (error) {
28
+ consola.error(`Failed to unexpose port: ${error}`);
29
+ process.exit(1);
30
+ }
31
+ }