@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/dist/index.js CHANGED
@@ -8,21 +8,21 @@ import os from 'os';
8
8
  import { env as env$2 } from 'process';
9
9
  import axios from 'axios';
10
10
  import { cleanEnv, str } from 'envalid';
11
+ import WebSocket from 'ws';
12
+ import { EventSource } from 'eventsource';
11
13
  import open from 'open';
12
14
  import express from 'express';
13
15
  import cors from 'cors';
14
16
  import fs$1 from 'node:fs/promises';
15
17
  import os$1 from 'node:os';
16
18
  import path$1 from 'node:path';
17
- import WebSocket from 'ws';
18
- import { EventSource } from 'eventsource';
19
19
  import Table from 'cli-table3';
20
20
  import dayjs from 'dayjs';
21
21
  import relativeTime from 'dayjs/plugin/relativeTime.js';
22
22
  import { password, editor, input } from '@inquirer/prompts';
23
23
  import sodium from 'libsodium-wrappers';
24
24
 
25
- var version = "0.2.4";
25
+ var version = "0.3.0";
26
26
 
27
27
  async function getAccessToken() {
28
28
  const tokenPath = path.join(os.homedir(), ".pocketenv", "token.json");
@@ -65,72 +65,6 @@ const client = axios.create({
65
65
  baseURL: env$1.POCKETENV_API_URL
66
66
  });
67
67
 
68
- async function start(name) {
69
- const token = await getAccessToken();
70
- try {
71
- await client.post("/xrpc/io.pocketenv.sandbox.startSandbox", void 0, {
72
- params: {
73
- id: name
74
- },
75
- headers: {
76
- Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
77
- }
78
- });
79
- consola.success(`Sandbox ${chalk.greenBright(name)} started`);
80
- consola.log(
81
- `Run ${chalk.greenBright(`pocketenv console ${name}`)} to access the sandbox`
82
- );
83
- } catch {
84
- consola.error("Failed to start sandbox");
85
- }
86
- }
87
-
88
- async function login(handle) {
89
- const app = express();
90
- app.use(cors());
91
- app.use(express.json());
92
- const server = app.listen(6997);
93
- app.post("/token", async (req, res) => {
94
- console.log(chalk.bold(chalk.greenBright("Login successful!\n")));
95
- const tokenPath = path$1.join(os$1.homedir(), ".pocketenv", "token.json");
96
- await fs$1.mkdir(path$1.dirname(tokenPath), { recursive: true });
97
- await fs$1.writeFile(
98
- tokenPath,
99
- JSON.stringify({ token: req.body.token }, null, 2)
100
- );
101
- res.json({
102
- ok: 1
103
- });
104
- server.close();
105
- });
106
- const response = await client.post(`/login`, { handle, cli: true });
107
- const redirectUrl = response.data;
108
- if (!redirectUrl.includes("authorize")) {
109
- console.error("Failed to login, please check your handle and try again.");
110
- server.close();
111
- return;
112
- }
113
- console.log("Please visit this URL to authorize the app:");
114
- console.log(chalk.cyan(redirectUrl));
115
- await open(response.data);
116
- }
117
-
118
- async function whoami() {
119
- const token = await getAccessToken();
120
- const profile = await client.get(
121
- "/xrpc/io.pocketenv.actor.getProfile",
122
- {
123
- headers: {
124
- Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
125
- }
126
- }
127
- );
128
- const handle = `@${profile.data.handle}`;
129
- consola.log(
130
- `You are logged in as ${chalk.cyan(handle)} (${profile.data.displayName}).`
131
- );
132
- }
133
-
134
68
  function sendInput$1(ws, data) {
135
69
  if (ws.readyState === WebSocket.OPEN) {
136
70
  ws.send(data);
@@ -334,6 +268,10 @@ async function ssh$1(sandbox) {
334
268
  }
335
269
  process.stdin.resume();
336
270
  process.stdin.on("data", (chunk) => {
271
+ if (chunk.includes(11)) {
272
+ teardown(0);
273
+ return;
274
+ }
337
275
  sendInput(ttyUrl, sandbox.id, chunk, authToken);
338
276
  });
339
277
  process.stdout.on("resize", () => {
@@ -380,13 +318,16 @@ ${chalk.dim("Process exited.")}\r
380
318
  });
381
319
  es.onerror = (err) => {
382
320
  if (es && es.readyState === EventSource.CLOSED) {
383
- const detail = err.message ? ` (${err.message})` : "";
384
- process.stderr.write(
385
- `\r
386
- ${chalk.red(`Terminal connection lost${detail}`)}\r
321
+ if (!err.message) {
322
+ teardown(0);
323
+ } else {
324
+ process.stderr.write(
325
+ `\r
326
+ ${chalk.red(`Terminal connection lost (${err.message})`)}\r
387
327
  `
388
- );
389
- teardown(1);
328
+ );
329
+ teardown(1);
330
+ }
390
331
  }
391
332
  };
392
333
  process.on("SIGINT", () => teardown(0));
@@ -469,6 +410,87 @@ async function ssh(sandboxName) {
469
410
  }
470
411
  }
471
412
 
413
+ async function start(name, { ssh: ssh$1 }) {
414
+ const token = await getAccessToken();
415
+ try {
416
+ await client.post("/xrpc/io.pocketenv.sandbox.startSandbox", void 0, {
417
+ params: {
418
+ id: name
419
+ },
420
+ headers: {
421
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
422
+ }
423
+ });
424
+ if (ssh$1) {
425
+ await ssh(name);
426
+ return;
427
+ }
428
+ consola.success(`Sandbox ${chalk.greenBright(name)} started`);
429
+ consola.log(
430
+ `Run ${chalk.greenBright(`pocketenv console ${name}`)} to access the sandbox`
431
+ );
432
+ } catch {
433
+ consola.error("Failed to start sandbox");
434
+ }
435
+ }
436
+
437
+ async function login(handle) {
438
+ const app = express();
439
+ app.use(cors());
440
+ app.use(express.json());
441
+ const server = app.listen(6997);
442
+ app.post("/token", async (req, res) => {
443
+ console.log(chalk.bold(chalk.greenBright("Login successful!\n")));
444
+ const tokenPath = path$1.join(os$1.homedir(), ".pocketenv", "token.json");
445
+ await fs$1.mkdir(path$1.dirname(tokenPath), { recursive: true });
446
+ await fs$1.writeFile(
447
+ tokenPath,
448
+ JSON.stringify({ token: req.body.token }, null, 2)
449
+ );
450
+ res.json({
451
+ ok: 1
452
+ });
453
+ server.close();
454
+ });
455
+ const response = await client.post(`/login`, { handle, cli: true });
456
+ const redirectUrl = response.data;
457
+ if (!redirectUrl.includes("authorize")) {
458
+ console.error("Failed to login, please check your handle and try again.");
459
+ server.close();
460
+ return;
461
+ }
462
+ console.log("Please visit this URL to authorize the app:");
463
+ console.log(chalk.cyan(redirectUrl));
464
+ await open(response.data);
465
+ }
466
+
467
+ async function whoami() {
468
+ const token = await getAccessToken();
469
+ const profile = await client.get(
470
+ "/xrpc/io.pocketenv.actor.getProfile",
471
+ {
472
+ headers: {
473
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
474
+ }
475
+ }
476
+ );
477
+ const handle = `@${profile.data.handle}`;
478
+ consola.log(
479
+ `You are logged in as ${chalk.cyan(handle)} (${profile.data.displayName}).`
480
+ );
481
+ }
482
+
483
+ const c = {
484
+ primary: (s) => chalk.rgb(0, 232, 198)(s),
485
+ secondary: (s) => chalk.rgb(0, 198, 232)(s),
486
+ accent: (s) => chalk.rgb(130, 100, 255)(s),
487
+ highlight: (s) => chalk.rgb(100, 232, 130)(s),
488
+ muted: (s) => chalk.rgb(200, 210, 220)(s),
489
+ link: (s) => chalk.rgb(255, 160, 100)(s),
490
+ sky: (s) => chalk.rgb(0, 210, 255)(s),
491
+ error: (s) => chalk.rgb(255, 100, 100)(s)
492
+ };
493
+
472
494
  dayjs.extend(relativeTime);
473
495
  async function listSandboxes() {
474
496
  const token = await getAccessToken();
@@ -492,10 +514,10 @@ async function listSandboxes() {
492
514
  );
493
515
  const table = new Table({
494
516
  head: [
495
- chalk.cyan("NAME"),
496
- chalk.cyan("BASE"),
497
- chalk.cyan("STATUS"),
498
- chalk.cyan("CREATED AT")
517
+ c.primary("NAME"),
518
+ c.primary("BASE"),
519
+ c.primary("STATUS"),
520
+ c.primary("CREATED AT")
499
521
  ],
500
522
  chars: {
501
523
  top: "",
@@ -521,9 +543,9 @@ async function listSandboxes() {
521
543
  });
522
544
  for (const sandbox of response.data.sandboxes) {
523
545
  table.push([
524
- chalk.greenBright(sandbox.name),
546
+ c.secondary(sandbox.name),
525
547
  sandbox.baseSandbox,
526
- sandbox.status === "RUNNING" ? chalk.greenBright(sandbox.status) : sandbox.status,
548
+ sandbox.status === "RUNNING" ? c.highlight(sandbox.status) : sandbox.status,
527
549
  dayjs(sandbox.createdAt).fromNow()
528
550
  ]);
529
551
  }
@@ -555,7 +577,7 @@ async function createSandbox(name, {
555
577
  const token = await getAccessToken();
556
578
  if (["deno", "vercel", "daytona"].includes(provider || "")) {
557
579
  consola.error(
558
- `This Sandbox Runtime is temporarily disabled. ${chalk.greenBright(provider ?? "")}`
580
+ `This Sandbox Runtime is temporarily disabled. ${c.primary(provider ?? "")}`
559
581
  );
560
582
  process.exit(1);
561
583
  }
@@ -575,7 +597,7 @@ async function createSandbox(name, {
575
597
  );
576
598
  if (!ssh$1) {
577
599
  consola.success(
578
- `Sandbox created successfully: ${chalk.greenBright(sandbox.data.name)}`
600
+ `Sandbox created successfully: ${c.primary(sandbox.data.name)}`
579
601
  );
580
602
  return;
581
603
  }
@@ -651,7 +673,7 @@ async function listSecrets(sandbox) {
651
673
  }
652
674
  );
653
675
  const table = new Table({
654
- head: [chalk.cyan("ID"), chalk.cyan("NAME"), chalk.cyan("CREATED AT")],
676
+ head: [c.primary("ID"), c.primary("NAME"), c.primary("CREATED AT")],
655
677
  chars: {
656
678
  top: "",
657
679
  "top-mid": "",
@@ -676,8 +698,8 @@ async function listSecrets(sandbox) {
676
698
  });
677
699
  for (const secret of response.data.secrets) {
678
700
  table.push([
679
- chalk.greenBright(secret.id),
680
- chalk.greenBright(secret.name),
701
+ c.secondary(secret.id),
702
+ c.highlight(secret.name),
681
703
  dayjs(secret.createdAt).fromNow()
682
704
  ]);
683
705
  }
@@ -685,7 +707,13 @@ async function listSecrets(sandbox) {
685
707
  }
686
708
  async function putSecret(sandbox, key) {
687
709
  const token = await getAccessToken();
688
- const value = await password({ message: "Enter secret value" });
710
+ const isStdinPiped = !process.stdin.isTTY;
711
+ const value = isStdinPiped ? await new Promise((resolve) => {
712
+ let data2 = "";
713
+ process.stdin.setEncoding("utf8");
714
+ process.stdin.on("data", (chunk) => data2 += chunk);
715
+ process.stdin.on("end", () => resolve(data2.trimEnd()));
716
+ }) : await password({ message: "Enter secret value" });
689
717
  const { data } = await client.get("/xrpc/io.pocketenv.sandbox.getSandbox", {
690
718
  params: {
691
719
  id: sandbox
@@ -698,21 +726,26 @@ async function putSecret(sandbox, key) {
698
726
  consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
699
727
  process.exit(1);
700
728
  }
701
- await client.post(
702
- "/xrpc/io.pocketenv.secret.addSecret",
703
- {
704
- secret: {
705
- sandboxId: data.sandbox.id,
706
- name: key,
707
- value: await encrypt(value)
708
- }
709
- },
710
- {
711
- headers: {
712
- Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
729
+ try {
730
+ await client.post(
731
+ "/xrpc/io.pocketenv.secret.addSecret",
732
+ {
733
+ secret: {
734
+ sandboxId: data.sandbox.id,
735
+ name: key,
736
+ value: await encrypt(value)
737
+ }
738
+ },
739
+ {
740
+ headers: {
741
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
742
+ }
713
743
  }
714
- }
715
- );
744
+ );
745
+ consola.success("Secret added successfully");
746
+ } catch (error) {
747
+ consola.error("Failed to add secret:", error);
748
+ }
716
749
  }
717
750
  async function deleteSecret(id) {
718
751
  const token = await getAccessToken();
@@ -726,8 +759,8 @@ async function deleteSecret(id) {
726
759
  }
727
760
  });
728
761
  consola.success("Secret deleted successfully");
729
- } catch {
730
- consola.error("Failed to delete secret");
762
+ } catch (error) {
763
+ consola.error("Failed to delete secret:", error);
731
764
  }
732
765
  }
733
766
 
@@ -745,7 +778,7 @@ async function listEnvs(sandbox) {
745
778
  }
746
779
  );
747
780
  if (!data.sandbox) {
748
- consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
781
+ consola.error(`Sandbox not found: ${c.primary(sandbox)}`);
749
782
  process.exit(1);
750
783
  }
751
784
  const response = await client.get(
@@ -763,10 +796,10 @@ async function listEnvs(sandbox) {
763
796
  );
764
797
  const table = new Table({
765
798
  head: [
766
- chalk.cyan("ID"),
767
- chalk.cyan("NAME"),
768
- chalk.cyan("VALUE"),
769
- chalk.cyan("CREATED AT")
799
+ c.primary("ID"),
800
+ c.primary("NAME"),
801
+ c.primary("VALUE"),
802
+ c.primary("CREATED AT")
770
803
  ],
771
804
  chars: {
772
805
  top: "",
@@ -792,8 +825,8 @@ async function listEnvs(sandbox) {
792
825
  });
793
826
  for (const variable of response.data.variables) {
794
827
  table.push([
795
- chalk.greenBright(variable.id),
796
- chalk.greenBright(variable.name),
828
+ c.secondary(variable.id),
829
+ c.highlight(variable.name),
797
830
  variable.value,
798
831
  dayjs(variable.createdAt).fromNow()
799
832
  ]);
@@ -1089,7 +1122,7 @@ async function putAuthKey(sandbox) {
1089
1122
  }
1090
1123
  );
1091
1124
  if (!data.sandbox) {
1092
- consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
1125
+ consola.error(`Sandbox not found: ${c.primary(sandbox)}`);
1093
1126
  process.exit(1);
1094
1127
  }
1095
1128
  const redacted = authKey.length > 14 ? authKey.slice(0, 11) + "*".repeat(authKey.length - 14) + authKey.slice(-3) : authKey;
@@ -1108,7 +1141,7 @@ async function putAuthKey(sandbox) {
1108
1141
  );
1109
1142
  consola.success(redacted);
1110
1143
  consola.success(
1111
- `Tailscale auth key saved for sandbox: ${chalk.greenBright(sandbox)}`
1144
+ `Tailscale auth key saved for sandbox: ${c.primary(sandbox)}`
1112
1145
  );
1113
1146
  }
1114
1147
  async function getTailscaleAuthKey(sandbox) {
@@ -1125,7 +1158,7 @@ async function getTailscaleAuthKey(sandbox) {
1125
1158
  }
1126
1159
  );
1127
1160
  if (!data.sandbox) {
1128
- consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
1161
+ consola.error(`Sandbox not found: ${c.primary(sandbox)}`);
1129
1162
  process.exit(1);
1130
1163
  }
1131
1164
  try {
@@ -1140,24 +1173,349 @@ async function getTailscaleAuthKey(sandbox) {
1140
1173
  }
1141
1174
  }
1142
1175
  );
1143
- consola.info(`Tailscale auth key: ${chalk.greenBright(tailscale.authKey)}`);
1176
+ consola.info(`Tailscale auth key: ${c.primary(tailscale.authKey)}`);
1144
1177
  } catch {
1145
1178
  consola.error(
1146
- `No Tailscale Auth Key found for sandbox: ${chalk.greenBright(sandbox)}`
1179
+ `No Tailscale Auth Key found for sandbox: ${c.primary(sandbox)}`
1147
1180
  );
1148
1181
  process.exit(1);
1149
1182
  }
1150
1183
  }
1151
1184
 
1152
- const c = {
1153
- primary: (s) => chalk.rgb(0, 232, 198)(s),
1154
- secondary: (s) => chalk.rgb(0, 198, 232)(s),
1155
- accent: (s) => chalk.rgb(130, 100, 255)(s),
1156
- highlight: (s) => chalk.rgb(100, 232, 130)(s),
1157
- muted: (s) => chalk.rgb(200, 210, 220)(s),
1158
- link: (s) => chalk.rgb(255, 160, 100)(s),
1159
- sky: (s) => chalk.rgb(0, 210, 255)(s)
1160
- };
1185
+ async function exposePort(sandbox, port, description) {
1186
+ const token = await getAccessToken();
1187
+ try {
1188
+ const response = await client.post(
1189
+ `/xrpc/io.pocketenv.sandbox.exposePort`,
1190
+ { port, description },
1191
+ {
1192
+ params: {
1193
+ id: sandbox
1194
+ },
1195
+ headers: {
1196
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1197
+ }
1198
+ }
1199
+ );
1200
+ consola.success(
1201
+ `Port ${c.primary(port)} exposed for sandbox ${c.primary(sandbox)}`
1202
+ );
1203
+ if (response.data.previewUrl) {
1204
+ consola.success(`Preview URL: ${c.secondary(response.data.previewUrl)}`);
1205
+ }
1206
+ } catch (error) {
1207
+ consola.error("Failed to expose port:", error);
1208
+ process.exit(1);
1209
+ }
1210
+ }
1211
+
1212
+ async function unexposePort(sandbox, port) {
1213
+ const token = await getAccessToken();
1214
+ try {
1215
+ await client.post(
1216
+ `/xrpc/io.pocketenv.sandbox.unexposePort`,
1217
+ { port },
1218
+ {
1219
+ params: {
1220
+ id: sandbox
1221
+ },
1222
+ headers: {
1223
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1224
+ }
1225
+ }
1226
+ );
1227
+ consola.success(
1228
+ `Port ${c.primary(port)} unexposed for sandbox ${c.primary(sandbox)}`
1229
+ );
1230
+ } catch (error) {
1231
+ consola.error(`Failed to unexpose port: ${error}`);
1232
+ process.exit(1);
1233
+ }
1234
+ }
1235
+
1236
+ dayjs.extend(relativeTime);
1237
+ async function listVolumes(sandboxId) {
1238
+ const token = await getAccessToken();
1239
+ const response = await client.get(
1240
+ "/xrpc/io.pocketenv.volume.getVolumes",
1241
+ {
1242
+ params: {
1243
+ sandboxId
1244
+ },
1245
+ headers: {
1246
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1247
+ }
1248
+ }
1249
+ );
1250
+ const table = new Table({
1251
+ head: [
1252
+ c.primary("ID"),
1253
+ c.primary("NAME"),
1254
+ c.primary("PATH"),
1255
+ c.primary("CREATED AT")
1256
+ ],
1257
+ chars: {
1258
+ top: "",
1259
+ "top-mid": "",
1260
+ "top-left": "",
1261
+ "top-right": "",
1262
+ bottom: "",
1263
+ "bottom-mid": "",
1264
+ "bottom-left": "",
1265
+ "bottom-right": "",
1266
+ left: "",
1267
+ "left-mid": "",
1268
+ mid: "",
1269
+ "mid-mid": "",
1270
+ right: "",
1271
+ "right-mid": "",
1272
+ middle: " "
1273
+ },
1274
+ style: {
1275
+ border: [],
1276
+ head: []
1277
+ }
1278
+ });
1279
+ for (const volume of response.data.volumes) {
1280
+ table.push([
1281
+ c.secondary(volume.id),
1282
+ volume.name,
1283
+ volume.path,
1284
+ dayjs(volume.createdAt).fromNow()
1285
+ ]);
1286
+ }
1287
+ consola.log(table.toString());
1288
+ }
1289
+ async function createVolume(sandbox, name, path) {
1290
+ const token = await getAccessToken();
1291
+ try {
1292
+ await client.post(
1293
+ "/xrpc/io.pocketenv.volume.addVolume",
1294
+ {
1295
+ volume: {
1296
+ sandboxId: sandbox,
1297
+ name,
1298
+ path
1299
+ }
1300
+ },
1301
+ {
1302
+ headers: {
1303
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1304
+ }
1305
+ }
1306
+ );
1307
+ consola.success(
1308
+ `Volume ${chalk.rgb(0, 232, 198)(name)} successfully mounted in sandbox ${chalk.rgb(0, 232, 198)(sandbox)} at path ${chalk.rgb(0, 232, 198)(path)}`
1309
+ );
1310
+ } catch (error) {
1311
+ consola.error("Failed to create volume:", error);
1312
+ }
1313
+ }
1314
+ async function deleteVolume(id) {
1315
+ const token = await getAccessToken();
1316
+ try {
1317
+ await client.post(`/xrpc/io.pocketenv.volume.deleteVolume`, void 0, {
1318
+ params: {
1319
+ id
1320
+ },
1321
+ headers: {
1322
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1323
+ }
1324
+ });
1325
+ } catch (error) {
1326
+ consola.error(`Failed to delete volume: ${error}`);
1327
+ return;
1328
+ }
1329
+ consola.success(
1330
+ `Volume ${chalk.rgb(0, 232, 198)(id)} successfully deleted from sandbox`
1331
+ );
1332
+ }
1333
+
1334
+ dayjs.extend(relativeTime);
1335
+ async function putFile(sandbox, remotePath, localPath) {
1336
+ const token = await getAccessToken();
1337
+ let content;
1338
+ if (!process.stdin.isTTY) {
1339
+ const chunks = [];
1340
+ for await (const chunk of process.stdin) chunks.push(chunk);
1341
+ content = Buffer.concat(chunks).toString().trim();
1342
+ } else if (localPath) {
1343
+ const resolvedPath = path.resolve(localPath);
1344
+ try {
1345
+ await fs.access(resolvedPath);
1346
+ } catch (err) {
1347
+ consola.error(`No such file: ${c.error(localPath)}`);
1348
+ process.exit(1);
1349
+ }
1350
+ content = await fs.readFile(resolvedPath, "utf-8");
1351
+ } else {
1352
+ content = (await editor({
1353
+ message: "File content (opens in $EDITOR):",
1354
+ waitForUserInput: false
1355
+ })).trim();
1356
+ }
1357
+ try {
1358
+ await client.post(
1359
+ "/xrpc/io.pocketenv.file.addFile",
1360
+ {
1361
+ file: {
1362
+ sandboxId: sandbox,
1363
+ path: remotePath,
1364
+ content: await encrypt(content)
1365
+ }
1366
+ },
1367
+ {
1368
+ headers: {
1369
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1370
+ }
1371
+ }
1372
+ );
1373
+ consola.success(
1374
+ `File ${c.primary(remotePath)} successfully created in sandbox ${c.primary(sandbox)}`
1375
+ );
1376
+ } catch (error) {
1377
+ consola.error(`Failed to create file: ${error}`);
1378
+ }
1379
+ }
1380
+ async function listFiles(sandboxId) {
1381
+ const token = await getAccessToken();
1382
+ const response = await client.get(
1383
+ "/xrpc/io.pocketenv.file.getFiles",
1384
+ {
1385
+ params: {
1386
+ sandboxId
1387
+ },
1388
+ headers: {
1389
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1390
+ }
1391
+ }
1392
+ );
1393
+ const table = new Table({
1394
+ head: [c.primary("ID"), c.primary("PATH"), c.primary("CREATED AT")],
1395
+ chars: {
1396
+ top: "",
1397
+ "top-mid": "",
1398
+ "top-left": "",
1399
+ "top-right": "",
1400
+ bottom: "",
1401
+ "bottom-mid": "",
1402
+ "bottom-left": "",
1403
+ "bottom-right": "",
1404
+ left: "",
1405
+ "left-mid": "",
1406
+ mid: "",
1407
+ "mid-mid": "",
1408
+ right: "",
1409
+ "right-mid": "",
1410
+ middle: " "
1411
+ },
1412
+ style: {
1413
+ border: [],
1414
+ head: []
1415
+ }
1416
+ });
1417
+ for (const file of response.data.files) {
1418
+ table.push([
1419
+ c.secondary(file.id),
1420
+ file.path,
1421
+ dayjs(file.createdAt).fromNow()
1422
+ ]);
1423
+ }
1424
+ consola.log(table.toString());
1425
+ }
1426
+ async function deleteFile(id) {
1427
+ const token = await getAccessToken();
1428
+ try {
1429
+ await client.post(`/xrpc/io.pocketenv.file.deleteFile`, void 0, {
1430
+ params: {
1431
+ id
1432
+ },
1433
+ headers: {
1434
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1435
+ }
1436
+ });
1437
+ consola.success(`File ${c.primary(id)} successfully deleted from sandbox`);
1438
+ } catch (error) {
1439
+ consola.error(`Failed to delete file: ${error}`);
1440
+ }
1441
+ }
1442
+
1443
+ async function listPorts(sandbox) {
1444
+ const token = await getAccessToken();
1445
+ const response = await client.get(
1446
+ "/xrpc/io.pocketenv.sandbox.getExposedPorts",
1447
+ {
1448
+ params: {
1449
+ id: sandbox
1450
+ },
1451
+ headers: {
1452
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1453
+ }
1454
+ }
1455
+ );
1456
+ const table = new Table({
1457
+ head: [
1458
+ c.primary("PORT"),
1459
+ c.primary("DESCRIPTION"),
1460
+ c.primary("PREVIEW URL")
1461
+ ],
1462
+ chars: {
1463
+ top: "",
1464
+ "top-mid": "",
1465
+ "top-left": "",
1466
+ "top-right": "",
1467
+ bottom: "",
1468
+ "bottom-mid": "",
1469
+ "bottom-left": "",
1470
+ "bottom-right": "",
1471
+ left: "",
1472
+ "left-mid": "",
1473
+ mid: "",
1474
+ "mid-mid": "",
1475
+ right: "",
1476
+ "right-mid": "",
1477
+ middle: " "
1478
+ },
1479
+ style: {
1480
+ border: [],
1481
+ head: []
1482
+ }
1483
+ });
1484
+ for (const port of response.data.ports) {
1485
+ table.push([
1486
+ c.secondary(port.port),
1487
+ port.description || "-",
1488
+ c.link(port.previewUrl || "-")
1489
+ ]);
1490
+ }
1491
+ consola.log(table.toString());
1492
+ }
1493
+
1494
+ async function exposeVscode(sandbox) {
1495
+ const token = await getAccessToken();
1496
+ try {
1497
+ const response = await client.post(
1498
+ `/xrpc/io.pocketenv.sandbox.exposeVscode`,
1499
+ void 0,
1500
+ {
1501
+ params: {
1502
+ id: sandbox
1503
+ },
1504
+ headers: {
1505
+ Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
1506
+ }
1507
+ }
1508
+ );
1509
+ consola.success(`VS Code Server exposed for sandbox ${c.primary(sandbox)}`);
1510
+ if (response.data.previewUrl) {
1511
+ consola.success(`Preview URL: ${c.secondary(response.data.previewUrl)}`);
1512
+ }
1513
+ } catch (error) {
1514
+ consola.error("Failed to expose VS Code:", error);
1515
+ process.exit(1);
1516
+ }
1517
+ }
1518
+
1161
1519
  const program = new Command();
1162
1520
  program.name("pocketenv").description(
1163
1521
  `${chalk.bold.rgb(0, 232, 198)(`pocketenv v${version}`)} ${c.muted("\u2500")} ${c.muted("Open, interoperable sandbox platform for agents and humans")}`
@@ -1184,7 +1542,7 @@ program.command("login").argument("<handle>", "your AT Proto handle (e.g., <user
1184
1542
  program.command("whoami").description("get the current logged-in user").action(whoami);
1185
1543
  program.command("console").aliases(["shell", "ssh", "s"]).argument("[sandbox]", "the sandbox to connect to").description("open an interactive shell for the given sandbox").action(ssh);
1186
1544
  program.command("ls").description("list sandboxes").action(listSandboxes);
1187
- program.command("start").argument("<sandbox>", "the sandbox to start").description("start the given sandbox").action(start);
1545
+ program.command("start").argument("<sandbox>", "the sandbox to start").option("--ssh, -s", "connect to the Sandbox and automatically open a shell").description("start the given sandbox").action(start);
1188
1546
  program.command("stop").argument("<sandbox>", "the sandbox to stop").description("stop the given sandbox").action(stop);
1189
1547
  program.command("create").aliases(["new"]).option("--provider, -p <provider>", "the provider to use for the sandbox").option(
1190
1548
  "--base, -b <base>",
@@ -1192,6 +1550,34 @@ program.command("create").aliases(["new"]).option("--provider, -p <provider>", "
1192
1550
  ).option("--ssh, -s", "connect to the Sandbox and automatically open a shell").argument("[name]", "the name of the sandbox to create").description("create a new sandbox").action(createSandbox);
1193
1551
  program.command("logout").description("logout (removes session token)").action(logout);
1194
1552
  program.command("rm").aliases(["delete", "remove"]).argument("<sandbox>", "the sandbox to delete").description("delete the given sandbox").action(deleteSandbox);
1553
+ program.command("vscode").argument("<sandbox>", "the sandbox to expose VS Code for").description(
1554
+ "expose a VS Code Server instance running in the given sandbox to the internet"
1555
+ ).action(exposeVscode);
1556
+ program.command("expose").argument("<sandbox>", "the sandbox to expose a port for").argument("<port>", "the port to expose", (val) => {
1557
+ const port = parseInt(val, 10);
1558
+ if (isNaN(port)) {
1559
+ consola.error(`port must be a number, got: ${val}`);
1560
+ process.exit(1);
1561
+ }
1562
+ return port;
1563
+ }).argument("[description]", "an optional description for the exposed port").description("expose a port from the given sandbox to the internet").action(exposePort);
1564
+ program.command("unexpose").argument("<sandbox>", "the sandbox to unexpose a port for").argument("<port>", "the port to unexpose", (val) => {
1565
+ const port = parseInt(val, 10);
1566
+ if (isNaN(port)) {
1567
+ consola.error(`port must be a number, got: ${val}`);
1568
+ process.exit(1);
1569
+ }
1570
+ return port;
1571
+ }).description("unexpose a port from the given sandbox").action(unexposePort);
1572
+ const volume = program.command("volume").description("manage volumes");
1573
+ volume.command("put").argument("<sandbox>", "the sandbox to put the volume in").argument("<name>", "the name of the volume").argument("<path>", "the path to mount the volume at").description("put a volume in the given sandbox").action(createVolume);
1574
+ volume.command("list").aliases(["ls"]).argument("<sandbox>", "the sandbox to list volumes for").description("list volumes in the given sandbox").action(listVolumes);
1575
+ volume.command("delete").aliases(["rm", "remove"]).argument("<id>", "the ID of the volume to delete").description("delete a volume").action(deleteVolume);
1576
+ const file = program.command("file").description("manage files");
1577
+ file.command("put").argument("<sandbox>", "the sandbox to put the file in").argument("<path>", "the remote path to upload the file to").argument("[localPath]", "the local path of the file to upload").description("upload a file to the given sandbox").action(putFile);
1578
+ file.command("list").aliases(["ls"]).argument("<sandbox>", "the sandbox to list files for").description("list files in the given sandbox").action(listFiles);
1579
+ file.command("delete").aliases(["rm", "remove"]).argument("<id>", "the ID of the file to delete").description("delete a file").action(deleteFile);
1580
+ program.command("ports").argument("<sandbox>", "the sandbox to list exposed ports for").description("list exposed ports for a sandbox").action(listPorts);
1195
1581
  const secret = program.command("secret").description("manage secrets");
1196
1582
  secret.command("put").argument("<sandbox>", "the sandbox to put the secret in").argument("<key>", "the key of the secret").description("put a secret in the given sandbox").action(putSecret);
1197
1583
  secret.command("list").aliases(["ls"]).argument("<sandbox>", "the sandbox to list secrets for").description("list secrets in the given sandbox").action(listSecrets);