@pocketenv/cli 0.2.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 +341 -0
- package/bun.lock +601 -0
- package/dist/index.js +1181 -0
- package/package.json +57 -0
- package/src/client.ts +6 -0
- package/src/cmd/create.ts +46 -0
- package/src/cmd/env.ts +126 -0
- package/src/cmd/list.ts +80 -0
- package/src/cmd/login.ts +49 -0
- package/src/cmd/logout.ts +20 -0
- package/src/cmd/rm.ts +24 -0
- package/src/cmd/secret.ts +127 -0
- package/src/cmd/ssh/cloudflare.ts +212 -0
- package/src/cmd/ssh/index.ts +96 -0
- package/src/cmd/ssh/terminal.ts +0 -0
- package/src/cmd/ssh/tty.ts +221 -0
- package/src/cmd/sshkeys.ts +176 -0
- package/src/cmd/start.ts +25 -0
- package/src/cmd/stop.ts +22 -0
- package/src/cmd/tailscale.ts +106 -0
- package/src/cmd/whoami.ts +24 -0
- package/src/index.ts +187 -0
- package/src/lib/env.ts +11 -0
- package/src/lib/getAccessToken.ts +36 -0
- package/src/lib/sodium.ts +15 -0
- package/src/lib/sshKeys.ts +122 -0
- package/src/types/file.ts +5 -0
- package/src/types/profile.ts +9 -0
- package/src/types/providers.ts +1 -0
- package/src/types/sandbox.ts +23 -0
- package/src/types/secret.ts +5 -0
- package/src/types/sshkeys.ts +6 -0
- package/src/types/tailscale-auth-key.ts +5 -0
- package/src/types/variable.ts +6 -0
- package/src/types/volume.ts +6 -0
- package/tsconfig.json +29 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import consola from 'consola';
|
|
5
|
+
import fs from 'fs/promises';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { env as env$2 } from 'process';
|
|
9
|
+
import axios from 'axios';
|
|
10
|
+
import { cleanEnv, str } from 'envalid';
|
|
11
|
+
import open from 'open';
|
|
12
|
+
import express from 'express';
|
|
13
|
+
import cors from 'cors';
|
|
14
|
+
import fs$1 from 'node:fs/promises';
|
|
15
|
+
import os$1 from 'node:os';
|
|
16
|
+
import path$1 from 'node:path';
|
|
17
|
+
import WebSocket from 'ws';
|
|
18
|
+
import { EventSource } from 'eventsource';
|
|
19
|
+
import Table from 'cli-table3';
|
|
20
|
+
import dayjs from 'dayjs';
|
|
21
|
+
import relativeTime from 'dayjs/plugin/relativeTime.js';
|
|
22
|
+
import { password, editor, input } from '@inquirer/prompts';
|
|
23
|
+
import sodium from 'libsodium-wrappers';
|
|
24
|
+
|
|
25
|
+
var version = "0.2.0";
|
|
26
|
+
|
|
27
|
+
async function getAccessToken() {
|
|
28
|
+
const tokenPath = path.join(os.homedir(), ".pocketenv", "token.json");
|
|
29
|
+
try {
|
|
30
|
+
await fs.access(tokenPath);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (!env$2.POCKETENV_TOKEN) {
|
|
33
|
+
consola.error(
|
|
34
|
+
`You are not logged in. Please run ${chalk.greenBright(
|
|
35
|
+
"`pocketenv login <username>.bsky.social`"
|
|
36
|
+
)} first.`
|
|
37
|
+
);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const tokenData = await fs.readFile(tokenPath, "utf-8");
|
|
42
|
+
const { token } = JSON.parse(tokenData);
|
|
43
|
+
if (!token) {
|
|
44
|
+
consola.error(
|
|
45
|
+
`You are not logged in. Please run ${chalk.greenBright(
|
|
46
|
+
"`rocksky login <username>.bsky.social`"
|
|
47
|
+
)} first.`
|
|
48
|
+
);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
return token;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const env$1 = cleanEnv(process.env, {
|
|
55
|
+
POCKETENV_TOKEN: str({ default: "" }),
|
|
56
|
+
POCKETENV_API_URL: str({ default: "https://api.pocketenv.io" }),
|
|
57
|
+
POCKETENV_CF_URL: str({ default: "https://sbx.pocketenv.io" }),
|
|
58
|
+
POCKETENV_TTY_URL: str({ default: "https://api.pocketenv.io" }),
|
|
59
|
+
POCKETENV_PUBLIC_KEY: str({
|
|
60
|
+
default: "2bf96e12d109e6948046a7803ef1696e12c11f04f20a6ce64dbd4bcd93db9341"
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const client = axios.create({
|
|
65
|
+
baseURL: env$1.POCKETENV_API_URL
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
async function start(name) {
|
|
69
|
+
const token = await getAccessToken();
|
|
70
|
+
await client.post("/xrpc/io.pocketenv.sandbox.startSandbox", void 0, {
|
|
71
|
+
params: {
|
|
72
|
+
id: name
|
|
73
|
+
},
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
consola.success(`Sandbox ${chalk.greenBright(name)} started`);
|
|
79
|
+
consola.log(
|
|
80
|
+
`Run ${chalk.greenBright(`pocketenv console ${name}`)} to access the sandbox`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function login(handle) {
|
|
85
|
+
const app = express();
|
|
86
|
+
app.use(cors());
|
|
87
|
+
app.use(express.json());
|
|
88
|
+
const server = app.listen(6997);
|
|
89
|
+
app.post("/token", async (req, res) => {
|
|
90
|
+
console.log(chalk.bold(chalk.greenBright("Login successful!\n")));
|
|
91
|
+
const tokenPath = path$1.join(os$1.homedir(), ".pocketenv", "token.json");
|
|
92
|
+
await fs$1.mkdir(path$1.dirname(tokenPath), { recursive: true });
|
|
93
|
+
await fs$1.writeFile(
|
|
94
|
+
tokenPath,
|
|
95
|
+
JSON.stringify({ token: req.body.token }, null, 2)
|
|
96
|
+
);
|
|
97
|
+
res.json({
|
|
98
|
+
ok: 1
|
|
99
|
+
});
|
|
100
|
+
server.close();
|
|
101
|
+
});
|
|
102
|
+
const response = await client.post(`/login`, { handle, cli: true });
|
|
103
|
+
const redirectUrl = response.data;
|
|
104
|
+
if (!redirectUrl.includes("authorize")) {
|
|
105
|
+
console.error("Failed to login, please check your handle and try again.");
|
|
106
|
+
server.close();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
console.log("Please visit this URL to authorize the app:");
|
|
110
|
+
console.log(chalk.cyan(redirectUrl));
|
|
111
|
+
await open(response.data);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function whoami() {
|
|
115
|
+
const token = await getAccessToken();
|
|
116
|
+
const profile = await client.get(
|
|
117
|
+
"/xrpc/io.pocketenv.actor.getProfile",
|
|
118
|
+
{
|
|
119
|
+
headers: {
|
|
120
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
const handle = `@${profile.data.handle}`;
|
|
125
|
+
consola.log(
|
|
126
|
+
`You are logged in as ${chalk.cyan(handle)} (${profile.data.displayName}).`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sendInput$1(ws, data) {
|
|
131
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
132
|
+
ws.send(data);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function sendResize$1(ws, cols, rows) {
|
|
136
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
137
|
+
ws.send(JSON.stringify({ type: "resize", cols, rows }));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function resolveWorkerUrl(baseSandbox, cfUrl) {
|
|
141
|
+
return cfUrl.replace("sbx", baseSandbox).replace("claude-code", "claudecode");
|
|
142
|
+
}
|
|
143
|
+
async function ssh$2(sandbox) {
|
|
144
|
+
const token = await getAccessToken();
|
|
145
|
+
const authToken = env$1.POCKETENV_TOKEN || token;
|
|
146
|
+
const tokenResponse = await client.get(
|
|
147
|
+
"/xrpc/io.pocketenv.actor.getTerminalToken",
|
|
148
|
+
{ headers: { Authorization: `Bearer ${authToken}` } }
|
|
149
|
+
);
|
|
150
|
+
const terminalToken = tokenResponse.data.token;
|
|
151
|
+
if (!terminalToken) {
|
|
152
|
+
consola.error("Failed to obtain a terminal token.");
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
const cfBaseUrl = env$1.POCKETENV_CF_URL;
|
|
156
|
+
const workerUrl = resolveWorkerUrl(sandbox.baseSandbox, cfBaseUrl);
|
|
157
|
+
const wsBase = workerUrl.replace(/^http/, "ws");
|
|
158
|
+
const wsUrl = new URL(`${wsBase}/v1/sandboxes/${sandbox.id}/ws/terminal`);
|
|
159
|
+
wsUrl.searchParams.set("t", terminalToken);
|
|
160
|
+
wsUrl.searchParams.set("session", crypto.randomUUID());
|
|
161
|
+
let cols = process.stdout.columns ?? 220;
|
|
162
|
+
let rows = process.stdout.rows ?? 50;
|
|
163
|
+
consola.info(
|
|
164
|
+
`Connecting to ${chalk.cyanBright(sandbox.name)} via Cloudflare sandbox\u2026`
|
|
165
|
+
);
|
|
166
|
+
const ws = new WebSocket(wsUrl.toString(), {
|
|
167
|
+
headers: { "User-Agent": "pocketenv-cli" }
|
|
168
|
+
});
|
|
169
|
+
let exiting = false;
|
|
170
|
+
let stdinAttached = false;
|
|
171
|
+
function teardown(code = 0) {
|
|
172
|
+
if (exiting) return;
|
|
173
|
+
exiting = true;
|
|
174
|
+
if (process.stdin.isTTY) {
|
|
175
|
+
try {
|
|
176
|
+
process.stdin.setRawMode(false);
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
process.stdin.pause();
|
|
181
|
+
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
182
|
+
ws.close(1e3, "client disconnect");
|
|
183
|
+
}
|
|
184
|
+
process.exit(code);
|
|
185
|
+
}
|
|
186
|
+
ws.on("open", () => {
|
|
187
|
+
});
|
|
188
|
+
ws.on("message", (raw, isBinary) => {
|
|
189
|
+
if (isBinary) {
|
|
190
|
+
process.stdout.write(raw);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let msg;
|
|
194
|
+
try {
|
|
195
|
+
msg = JSON.parse(raw.toString());
|
|
196
|
+
} catch {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
switch (msg.type) {
|
|
200
|
+
case "ready": {
|
|
201
|
+
sendResize$1(ws, cols, rows);
|
|
202
|
+
if (stdinAttached) break;
|
|
203
|
+
stdinAttached = true;
|
|
204
|
+
if (process.stdin.isTTY) {
|
|
205
|
+
process.stdin.setRawMode(true);
|
|
206
|
+
}
|
|
207
|
+
process.stdin.resume();
|
|
208
|
+
process.stdin.on("data", (chunk) => {
|
|
209
|
+
sendInput$1(ws, chunk);
|
|
210
|
+
});
|
|
211
|
+
process.stdout.on("resize", () => {
|
|
212
|
+
cols = process.stdout.columns ?? cols;
|
|
213
|
+
rows = process.stdout.rows ?? rows;
|
|
214
|
+
sendResize$1(ws, cols, rows);
|
|
215
|
+
});
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case "error":
|
|
219
|
+
process.stderr.write(
|
|
220
|
+
`\r
|
|
221
|
+
${chalk.red("Terminal error:")} ${msg.message}\r
|
|
222
|
+
`
|
|
223
|
+
);
|
|
224
|
+
teardown(1);
|
|
225
|
+
break;
|
|
226
|
+
case "exit":
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
`\r
|
|
229
|
+
${chalk.dim(
|
|
230
|
+
`Session exited with code ${msg.code}${msg.signal ? ` (${msg.signal})` : ""}`
|
|
231
|
+
)}\r
|
|
232
|
+
`
|
|
233
|
+
);
|
|
234
|
+
teardown(msg.code ?? 0);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
ws.on("close", (code, reason) => {
|
|
239
|
+
if (!exiting) {
|
|
240
|
+
process.stderr.write(
|
|
241
|
+
`\r
|
|
242
|
+
${chalk.yellow("Connection closed")} (${code}${reason.length ? ` \u2013 ${reason}` : ""})\r
|
|
243
|
+
`
|
|
244
|
+
);
|
|
245
|
+
teardown(0);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
ws.on("error", (err) => {
|
|
249
|
+
consola.error("WebSocket error:", err.message);
|
|
250
|
+
teardown(1);
|
|
251
|
+
});
|
|
252
|
+
process.on("SIGINT", () => teardown(0));
|
|
253
|
+
process.on("SIGTERM", () => teardown(0));
|
|
254
|
+
await new Promise((resolve) => {
|
|
255
|
+
ws.on("close", resolve);
|
|
256
|
+
ws.on("error", () => resolve());
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function sendInput(ttyUrl, sandboxId, data, token) {
|
|
261
|
+
try {
|
|
262
|
+
await axios.post(
|
|
263
|
+
`${ttyUrl}/tty/${sandboxId}/input`,
|
|
264
|
+
data instanceof Buffer ? data.toString("utf-8") : data,
|
|
265
|
+
{
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "text/plain",
|
|
268
|
+
Authorization: `Bearer ${token}`
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
);
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function sendResize(ttyUrl, sandboxId, cols, rows, token) {
|
|
276
|
+
try {
|
|
277
|
+
await axios.post(
|
|
278
|
+
`${ttyUrl}/tty/${sandboxId}/resize`,
|
|
279
|
+
{ cols, rows },
|
|
280
|
+
{
|
|
281
|
+
headers: {
|
|
282
|
+
"Content-Type": "application/json",
|
|
283
|
+
Authorization: `Bearer ${token}`
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
} catch {
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function makeAuthFetch(token) {
|
|
291
|
+
return (url, init) => {
|
|
292
|
+
const headers = new Headers(init.headers ?? {});
|
|
293
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
294
|
+
return fetch(url, { ...init, headers });
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
async function ssh$1(sandbox) {
|
|
298
|
+
const token = await getAccessToken();
|
|
299
|
+
const authToken = env$1.POCKETENV_TOKEN || token;
|
|
300
|
+
const ttyUrl = env$1.POCKETENV_TTY_URL;
|
|
301
|
+
let cols = process.stdout.columns ?? 220;
|
|
302
|
+
let rows = process.stdout.rows ?? 50;
|
|
303
|
+
consola.info(
|
|
304
|
+
`Connecting to ${chalk.cyanBright(sandbox.name)} via TTY stream\u2026`
|
|
305
|
+
);
|
|
306
|
+
let exiting = false;
|
|
307
|
+
let es = null;
|
|
308
|
+
let stdinAttached = false;
|
|
309
|
+
function teardown(code = 0) {
|
|
310
|
+
if (exiting) return;
|
|
311
|
+
exiting = true;
|
|
312
|
+
if (process.stdin.isTTY) {
|
|
313
|
+
try {
|
|
314
|
+
process.stdin.setRawMode(false);
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
process.stdin.pause();
|
|
319
|
+
if (es) {
|
|
320
|
+
es.close();
|
|
321
|
+
es = null;
|
|
322
|
+
}
|
|
323
|
+
process.exit(code);
|
|
324
|
+
}
|
|
325
|
+
function attachStdin() {
|
|
326
|
+
if (stdinAttached) return;
|
|
327
|
+
stdinAttached = true;
|
|
328
|
+
if (process.stdin.isTTY) {
|
|
329
|
+
process.stdin.setRawMode(true);
|
|
330
|
+
}
|
|
331
|
+
process.stdin.resume();
|
|
332
|
+
process.stdin.on("data", (chunk) => {
|
|
333
|
+
sendInput(ttyUrl, sandbox.id, chunk, authToken);
|
|
334
|
+
});
|
|
335
|
+
process.stdout.on("resize", () => {
|
|
336
|
+
cols = process.stdout.columns ?? cols;
|
|
337
|
+
rows = process.stdout.rows ?? rows;
|
|
338
|
+
sendResize(ttyUrl, sandbox.id, cols, rows, authToken);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
process.stdout.write(`\x1B[35mConnecting to terminal...\x1B[0m\r
|
|
342
|
+
`);
|
|
343
|
+
es = new EventSource(`${ttyUrl}/tty/${sandbox.id}/stream`, {
|
|
344
|
+
fetch: makeAuthFetch(authToken)
|
|
345
|
+
});
|
|
346
|
+
es.addEventListener("open", () => {
|
|
347
|
+
process.stdout.write("\r\x1B[K");
|
|
348
|
+
sendResize(ttyUrl, sandbox.id, cols, rows, authToken).then(() => {
|
|
349
|
+
attachStdin();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
es.addEventListener("output", (event) => {
|
|
353
|
+
try {
|
|
354
|
+
const { data } = JSON.parse(event.data);
|
|
355
|
+
process.stdout.write(data);
|
|
356
|
+
} catch {
|
|
357
|
+
process.stdout.write(event.data);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
es.addEventListener("exit", (event) => {
|
|
361
|
+
let code = 0;
|
|
362
|
+
try {
|
|
363
|
+
const parsed = JSON.parse(event.data);
|
|
364
|
+
code = parsed.code ?? 0;
|
|
365
|
+
process.stderr.write(
|
|
366
|
+
`\r
|
|
367
|
+
${chalk.dim(`Process exited with code ${code}`)}\r
|
|
368
|
+
`
|
|
369
|
+
);
|
|
370
|
+
} catch {
|
|
371
|
+
process.stderr.write(`\r
|
|
372
|
+
${chalk.dim("Process exited.")}\r
|
|
373
|
+
`);
|
|
374
|
+
}
|
|
375
|
+
teardown(code);
|
|
376
|
+
});
|
|
377
|
+
es.onerror = (err) => {
|
|
378
|
+
if (es && es.readyState === EventSource.CLOSED) {
|
|
379
|
+
const detail = err.message ? ` (${err.message})` : "";
|
|
380
|
+
process.stderr.write(
|
|
381
|
+
`\r
|
|
382
|
+
${chalk.red(`Terminal connection lost${detail}`)}\r
|
|
383
|
+
`
|
|
384
|
+
);
|
|
385
|
+
teardown(1);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
process.on("SIGINT", () => teardown(0));
|
|
389
|
+
process.on("SIGTERM", () => teardown(0));
|
|
390
|
+
await new Promise((resolve) => {
|
|
391
|
+
const poll = setInterval(() => {
|
|
392
|
+
if (exiting) {
|
|
393
|
+
clearInterval(poll);
|
|
394
|
+
resolve();
|
|
395
|
+
}
|
|
396
|
+
}, 200);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function ssh(sandboxName) {
|
|
401
|
+
const token = await getAccessToken();
|
|
402
|
+
const authToken = env$1.POCKETENV_TOKEN || token;
|
|
403
|
+
let sandbox;
|
|
404
|
+
if (!sandboxName) {
|
|
405
|
+
const profile = await client.get(
|
|
406
|
+
"/xrpc/io.pocketenv.actor.getProfile",
|
|
407
|
+
{ headers: { Authorization: `Bearer ${authToken}` } }
|
|
408
|
+
);
|
|
409
|
+
const response = await client.get(
|
|
410
|
+
"/xrpc/io.pocketenv.actor.getActorSandboxes",
|
|
411
|
+
{
|
|
412
|
+
params: { did: profile.data.did, offset: 0, limit: 100 },
|
|
413
|
+
headers: { Authorization: `Bearer ${authToken}` }
|
|
414
|
+
}
|
|
415
|
+
);
|
|
416
|
+
const runningSandboxes = response.data.sandboxes.filter(
|
|
417
|
+
(s) => s.status === "RUNNING"
|
|
418
|
+
);
|
|
419
|
+
if (runningSandboxes.length === 0) {
|
|
420
|
+
consola.error(
|
|
421
|
+
`No running sandboxes found. Start one with ${chalk.greenBright("pocketenv start <sandbox>")} first.`
|
|
422
|
+
);
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
sandbox = runningSandboxes[0];
|
|
426
|
+
consola.info(`Connecting to sandbox ${chalk.greenBright(sandbox.name)}\u2026`);
|
|
427
|
+
} else {
|
|
428
|
+
const response = await client.get(
|
|
429
|
+
"/xrpc/io.pocketenv.sandbox.getSandbox",
|
|
430
|
+
{
|
|
431
|
+
params: { id: sandboxName },
|
|
432
|
+
headers: { Authorization: `Bearer ${authToken}` }
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
if (!response.data.sandbox) {
|
|
436
|
+
consola.error(`Sandbox ${chalk.yellowBright(sandboxName)} not found.`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
sandbox = response.data.sandbox;
|
|
440
|
+
}
|
|
441
|
+
if (sandbox.status !== "RUNNING") {
|
|
442
|
+
consola.error(
|
|
443
|
+
`Sandbox ${chalk.yellowBright(sandbox.name)} is not running. Start it with ${chalk.greenBright(`pocketenv start ${sandbox.name}`)}.`
|
|
444
|
+
);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
switch (sandbox.provider) {
|
|
448
|
+
case "cloudflare":
|
|
449
|
+
await ssh$2(sandbox);
|
|
450
|
+
break;
|
|
451
|
+
case "daytona":
|
|
452
|
+
break;
|
|
453
|
+
case "deno":
|
|
454
|
+
break;
|
|
455
|
+
case "vercel":
|
|
456
|
+
break;
|
|
457
|
+
case "sprites":
|
|
458
|
+
await ssh$1(sandbox);
|
|
459
|
+
break;
|
|
460
|
+
default:
|
|
461
|
+
consola.error(
|
|
462
|
+
`Sandbox ${chalk.yellowBright(sandbox.name)} uses provider ${chalk.cyan(sandbox.provider)}, but this command only supports ${chalk.cyan("cloudflare")}, ${chalk.cyan("daytona")}, ${chalk.cyan("deno")}, ${chalk.cyan("vercel")}, or ${chalk.cyan("sprites")} sandboxes.`
|
|
463
|
+
);
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
dayjs.extend(relativeTime);
|
|
469
|
+
async function listSandboxes() {
|
|
470
|
+
const token = await getAccessToken();
|
|
471
|
+
const profile = await client.get(
|
|
472
|
+
"/xrpc/io.pocketenv.actor.getProfile",
|
|
473
|
+
{
|
|
474
|
+
headers: {
|
|
475
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
const response = await client.get(
|
|
480
|
+
"/xrpc/io.pocketenv.actor.getActorSandboxes",
|
|
481
|
+
{
|
|
482
|
+
params: {
|
|
483
|
+
did: profile.data.did,
|
|
484
|
+
offset: 0,
|
|
485
|
+
limit: 100
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
);
|
|
489
|
+
const table = new Table({
|
|
490
|
+
head: [
|
|
491
|
+
chalk.cyan("NAME"),
|
|
492
|
+
chalk.cyan("BASE"),
|
|
493
|
+
chalk.cyan("STATUS"),
|
|
494
|
+
chalk.cyan("CREATED AT")
|
|
495
|
+
],
|
|
496
|
+
chars: {
|
|
497
|
+
top: "",
|
|
498
|
+
"top-mid": "",
|
|
499
|
+
"top-left": "",
|
|
500
|
+
"top-right": "",
|
|
501
|
+
bottom: "",
|
|
502
|
+
"bottom-mid": "",
|
|
503
|
+
"bottom-left": "",
|
|
504
|
+
"bottom-right": "",
|
|
505
|
+
left: "",
|
|
506
|
+
"left-mid": "",
|
|
507
|
+
mid: "",
|
|
508
|
+
"mid-mid": "",
|
|
509
|
+
right: "",
|
|
510
|
+
"right-mid": "",
|
|
511
|
+
middle: " "
|
|
512
|
+
},
|
|
513
|
+
style: {
|
|
514
|
+
border: [],
|
|
515
|
+
head: []
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
for (const sandbox of response.data.sandboxes) {
|
|
519
|
+
table.push([
|
|
520
|
+
chalk.greenBright(sandbox.name),
|
|
521
|
+
sandbox.baseSandbox,
|
|
522
|
+
sandbox.status === "RUNNING" ? chalk.greenBright(sandbox.status) : sandbox.status,
|
|
523
|
+
dayjs(sandbox.createdAt).fromNow()
|
|
524
|
+
]);
|
|
525
|
+
}
|
|
526
|
+
consola.log(table.toString());
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function stop(name) {
|
|
530
|
+
const token = await getAccessToken();
|
|
531
|
+
await client.post("/xrpc/io.pocketenv.sandbox.stopSandbox", void 0, {
|
|
532
|
+
params: {
|
|
533
|
+
id: name
|
|
534
|
+
},
|
|
535
|
+
headers: {
|
|
536
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
consola.success(`Sandbox ${chalk.greenBright(name)} stopped`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function createSandbox(name, { provider, ssh: ssh$1 }) {
|
|
543
|
+
const token = await getAccessToken();
|
|
544
|
+
if (["deno", "vercel", "daytona"].includes(provider || "")) {
|
|
545
|
+
consola.error(
|
|
546
|
+
`This Sandbox Runtime is temporarily disabled. ${chalk.greenBright(provider ?? "")}`
|
|
547
|
+
);
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
const sandbox = await client.post(
|
|
552
|
+
"/xrpc/io.pocketenv.sandbox.createSandbox",
|
|
553
|
+
{
|
|
554
|
+
name,
|
|
555
|
+
base: "at://did:plc:aturpi2ls3yvsmhc6wybomun/io.pocketenv.sandbox/openclaw",
|
|
556
|
+
provider: provider ?? "cloudflare"
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
headers: {
|
|
560
|
+
Authorization: `Bearer ${token}`
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
);
|
|
564
|
+
if (!ssh$1) {
|
|
565
|
+
consola.success(
|
|
566
|
+
`Sandbox created successfully: ${chalk.greenBright(sandbox.data.name)}`
|
|
567
|
+
);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
await ssh(sandbox.data.name);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
consola.error(`Failed to create sandbox: ${error}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function logout() {
|
|
577
|
+
const tokenPath = path$1.join(os$1.homedir(), ".pocketenv", "token.json");
|
|
578
|
+
try {
|
|
579
|
+
await fs$1.access(tokenPath);
|
|
580
|
+
} catch {
|
|
581
|
+
consola.log("Logged out successfully");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
await fs$1.unlink(tokenPath);
|
|
585
|
+
consola.log("Logged out successfully");
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function deleteSandbox(id) {
|
|
589
|
+
const token = await getAccessToken();
|
|
590
|
+
try {
|
|
591
|
+
await client.post("/xrpc/io.pocketenv.sandbox.deleteSandbox", void 0, {
|
|
592
|
+
params: {
|
|
593
|
+
id
|
|
594
|
+
},
|
|
595
|
+
headers: {
|
|
596
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
consola.success("Sandbox deleted successfully");
|
|
600
|
+
} catch {
|
|
601
|
+
consola.error("Failed to delete sandbox");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function encrypt(message) {
|
|
606
|
+
await sodium.ready;
|
|
607
|
+
const sealed = sodium.crypto_box_seal(
|
|
608
|
+
sodium.from_string(message),
|
|
609
|
+
sodium.from_hex(env$1.POCKETENV_PUBLIC_KEY)
|
|
610
|
+
);
|
|
611
|
+
return sodium.to_base64(sealed, sodium.base64_variants.URLSAFE_NO_PADDING);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
dayjs.extend(relativeTime);
|
|
615
|
+
async function listSecrets(sandbox) {
|
|
616
|
+
const token = await getAccessToken();
|
|
617
|
+
const { data } = await client.get(
|
|
618
|
+
"/xrpc/io.pocketenv.sandbox.getSandbox",
|
|
619
|
+
{
|
|
620
|
+
params: {
|
|
621
|
+
id: sandbox
|
|
622
|
+
},
|
|
623
|
+
headers: {
|
|
624
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
const response = await client.get(
|
|
629
|
+
"/xrpc/io.pocketenv.secret.getSecrets",
|
|
630
|
+
{
|
|
631
|
+
params: {
|
|
632
|
+
sandboxId: data.sandbox.id,
|
|
633
|
+
offset: 0,
|
|
634
|
+
limit: 100
|
|
635
|
+
},
|
|
636
|
+
headers: {
|
|
637
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
const table = new Table({
|
|
642
|
+
head: [chalk.cyan("ID"), chalk.cyan("NAME"), chalk.cyan("CREATED AT")],
|
|
643
|
+
chars: {
|
|
644
|
+
top: "",
|
|
645
|
+
"top-mid": "",
|
|
646
|
+
"top-left": "",
|
|
647
|
+
"top-right": "",
|
|
648
|
+
bottom: "",
|
|
649
|
+
"bottom-mid": "",
|
|
650
|
+
"bottom-left": "",
|
|
651
|
+
"bottom-right": "",
|
|
652
|
+
left: "",
|
|
653
|
+
"left-mid": "",
|
|
654
|
+
mid: "",
|
|
655
|
+
"mid-mid": "",
|
|
656
|
+
right: "",
|
|
657
|
+
"right-mid": "",
|
|
658
|
+
middle: " "
|
|
659
|
+
},
|
|
660
|
+
style: {
|
|
661
|
+
border: [],
|
|
662
|
+
head: []
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
for (const secret of response.data.secrets) {
|
|
666
|
+
table.push([
|
|
667
|
+
chalk.greenBright(secret.id),
|
|
668
|
+
chalk.greenBright(secret.name),
|
|
669
|
+
dayjs(secret.createdAt).fromNow()
|
|
670
|
+
]);
|
|
671
|
+
}
|
|
672
|
+
consola.log(table.toString());
|
|
673
|
+
}
|
|
674
|
+
async function putSecret(sandbox, key) {
|
|
675
|
+
const token = await getAccessToken();
|
|
676
|
+
const value = await password({ message: "Enter secret value" });
|
|
677
|
+
const { data } = await client.get("/xrpc/io.pocketenv.sandbox.getSandbox", {
|
|
678
|
+
params: {
|
|
679
|
+
id: sandbox
|
|
680
|
+
},
|
|
681
|
+
headers: {
|
|
682
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
if (!data.sandbox) {
|
|
686
|
+
consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
await client.post(
|
|
690
|
+
"/xrpc/io.pocketenv.secret.addSecret",
|
|
691
|
+
{
|
|
692
|
+
secret: {
|
|
693
|
+
sandboxId: data.sandbox.id,
|
|
694
|
+
name: key,
|
|
695
|
+
value: await encrypt(value)
|
|
696
|
+
}
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
headers: {
|
|
700
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
async function deleteSecret(id) {
|
|
706
|
+
const token = await getAccessToken();
|
|
707
|
+
await client.post("/xrpc/io.pocketenv.secret.deleteSecret", void 0, {
|
|
708
|
+
params: {
|
|
709
|
+
id
|
|
710
|
+
},
|
|
711
|
+
headers: {
|
|
712
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
consola.success("Secret deleted successfully");
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
async function listEnvs(sandbox) {
|
|
719
|
+
const token = await getAccessToken();
|
|
720
|
+
const { data } = await client.get(
|
|
721
|
+
"/xrpc/io.pocketenv.sandbox.getSandbox",
|
|
722
|
+
{
|
|
723
|
+
params: {
|
|
724
|
+
id: sandbox
|
|
725
|
+
},
|
|
726
|
+
headers: {
|
|
727
|
+
Authorization: `Bearer ${token}`
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
);
|
|
731
|
+
if (!data.sandbox) {
|
|
732
|
+
consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
|
|
733
|
+
process.exit(1);
|
|
734
|
+
}
|
|
735
|
+
const response = await client.get(
|
|
736
|
+
"/xrpc/io.pocketenv.variable.getVariables",
|
|
737
|
+
{
|
|
738
|
+
params: {
|
|
739
|
+
sandboxId: data.sandbox.id,
|
|
740
|
+
offset: 0,
|
|
741
|
+
limit: 100
|
|
742
|
+
},
|
|
743
|
+
headers: {
|
|
744
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
);
|
|
748
|
+
const table = new Table({
|
|
749
|
+
head: [
|
|
750
|
+
chalk.cyan("ID"),
|
|
751
|
+
chalk.cyan("NAME"),
|
|
752
|
+
chalk.cyan("VALUE"),
|
|
753
|
+
chalk.cyan("CREATED AT")
|
|
754
|
+
],
|
|
755
|
+
chars: {
|
|
756
|
+
top: "",
|
|
757
|
+
"top-mid": "",
|
|
758
|
+
"top-left": "",
|
|
759
|
+
"top-right": "",
|
|
760
|
+
bottom: "",
|
|
761
|
+
"bottom-mid": "",
|
|
762
|
+
"bottom-left": "",
|
|
763
|
+
"bottom-right": "",
|
|
764
|
+
left: "",
|
|
765
|
+
"left-mid": "",
|
|
766
|
+
mid: "",
|
|
767
|
+
"mid-mid": "",
|
|
768
|
+
right: "",
|
|
769
|
+
"right-mid": "",
|
|
770
|
+
middle: " "
|
|
771
|
+
},
|
|
772
|
+
style: {
|
|
773
|
+
border: [],
|
|
774
|
+
head: []
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
for (const variable of response.data.variables) {
|
|
778
|
+
table.push([
|
|
779
|
+
chalk.greenBright(variable.id),
|
|
780
|
+
chalk.greenBright(variable.name),
|
|
781
|
+
variable.value,
|
|
782
|
+
dayjs(variable.createdAt).fromNow()
|
|
783
|
+
]);
|
|
784
|
+
}
|
|
785
|
+
consola.log(table.toString());
|
|
786
|
+
}
|
|
787
|
+
async function putEnv(sandbox, key, value) {
|
|
788
|
+
const token = await getAccessToken();
|
|
789
|
+
const { data } = await client.get(
|
|
790
|
+
"/xrpc/io.pocketenv.sandbox.getSandbox",
|
|
791
|
+
{
|
|
792
|
+
params: {
|
|
793
|
+
id: sandbox
|
|
794
|
+
},
|
|
795
|
+
headers: {
|
|
796
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
);
|
|
800
|
+
await client.post(
|
|
801
|
+
"/xrpc/io.pocketenv.variable.addVariable",
|
|
802
|
+
{
|
|
803
|
+
variable: { sandboxId: data.sandbox.id, name: key, value }
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
headers: {
|
|
807
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
);
|
|
811
|
+
consola.success("Variable updated successfully");
|
|
812
|
+
}
|
|
813
|
+
async function deleteEnv(id) {
|
|
814
|
+
const token = await getAccessToken();
|
|
815
|
+
await client.post("/xrpc/io.pocketenv.variable.deleteVariable", void 0, {
|
|
816
|
+
params: { id },
|
|
817
|
+
headers: {
|
|
818
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
consola.success("Variable deleted successfully");
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function u32(n) {
|
|
825
|
+
return new Uint8Array([
|
|
826
|
+
n >>> 24 & 255,
|
|
827
|
+
n >>> 16 & 255,
|
|
828
|
+
n >>> 8 & 255,
|
|
829
|
+
n & 255
|
|
830
|
+
]);
|
|
831
|
+
}
|
|
832
|
+
function concatBytes(...arrays) {
|
|
833
|
+
const total = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
834
|
+
const out = new Uint8Array(total);
|
|
835
|
+
let offset = 0;
|
|
836
|
+
for (const arr of arrays) {
|
|
837
|
+
out.set(arr, offset);
|
|
838
|
+
offset += arr.length;
|
|
839
|
+
}
|
|
840
|
+
return out;
|
|
841
|
+
}
|
|
842
|
+
function sshString(bytes) {
|
|
843
|
+
return concatBytes(u32(bytes.length), bytes);
|
|
844
|
+
}
|
|
845
|
+
function text(value) {
|
|
846
|
+
return new TextEncoder().encode(value);
|
|
847
|
+
}
|
|
848
|
+
function wrapPem(label, bytes) {
|
|
849
|
+
const base64 = sodium.to_base64(bytes, sodium.base64_variants.ORIGINAL);
|
|
850
|
+
const lines = base64.match(/.{1,70}/g)?.join("\n") ?? base64;
|
|
851
|
+
return `-----BEGIN ${label}-----
|
|
852
|
+
${lines}
|
|
853
|
+
-----END ${label}-----
|
|
854
|
+
`;
|
|
855
|
+
}
|
|
856
|
+
function buildEd25519PublicKeyBlob(publicKey) {
|
|
857
|
+
return concatBytes(sshString(text("ssh-ed25519")), sshString(publicKey));
|
|
858
|
+
}
|
|
859
|
+
function publicLineFromPublicKey(publicKey, comment) {
|
|
860
|
+
const blob = buildEd25519PublicKeyBlob(publicKey);
|
|
861
|
+
return `ssh-ed25519 ${sodium.to_base64(blob, sodium.base64_variants.ORIGINAL)} ${comment}`;
|
|
862
|
+
}
|
|
863
|
+
function buildOpenSSHEd25519PrivateKey(publicKey, seed, comment) {
|
|
864
|
+
if (publicKey.length !== 32) throw new Error("Invalid public key length");
|
|
865
|
+
if (seed.length !== 32) throw new Error("Invalid seed length");
|
|
866
|
+
const privateKey64 = concatBytes(seed, publicKey);
|
|
867
|
+
const publicBlob = buildEd25519PublicKeyBlob(publicKey);
|
|
868
|
+
const checkint = crypto.getRandomValues(new Uint32Array(1))[0];
|
|
869
|
+
const commentBytes = text(comment);
|
|
870
|
+
const privateSectionWithoutPadding = concatBytes(
|
|
871
|
+
u32(checkint),
|
|
872
|
+
u32(checkint),
|
|
873
|
+
sshString(text("ssh-ed25519")),
|
|
874
|
+
sshString(publicKey),
|
|
875
|
+
sshString(privateKey64),
|
|
876
|
+
sshString(commentBytes)
|
|
877
|
+
);
|
|
878
|
+
const blockSize = 8;
|
|
879
|
+
const remainder = privateSectionWithoutPadding.length % blockSize;
|
|
880
|
+
const padLen = remainder === 0 ? 0 : blockSize - remainder;
|
|
881
|
+
const padding = new Uint8Array(padLen);
|
|
882
|
+
for (let i = 0; i < padLen; i++) padding[i] = i + 1;
|
|
883
|
+
const privateSection = concatBytes(privateSectionWithoutPadding, padding);
|
|
884
|
+
const opensshKey = concatBytes(
|
|
885
|
+
text("openssh-key-v1\0"),
|
|
886
|
+
sshString(text("none")),
|
|
887
|
+
sshString(text("none")),
|
|
888
|
+
sshString(new Uint8Array()),
|
|
889
|
+
u32(1),
|
|
890
|
+
sshString(publicBlob),
|
|
891
|
+
sshString(privateSection)
|
|
892
|
+
);
|
|
893
|
+
return wrapPem("OPENSSH PRIVATE KEY", opensshKey);
|
|
894
|
+
}
|
|
895
|
+
async function generateEd25519SSHKeyPair(comment = "user@browser") {
|
|
896
|
+
await sodium.ready;
|
|
897
|
+
const seed = new Uint8Array(32);
|
|
898
|
+
crypto.getRandomValues(seed);
|
|
899
|
+
const kp = sodium.crypto_sign_seed_keypair(seed);
|
|
900
|
+
const publicKey = new Uint8Array(kp.publicKey);
|
|
901
|
+
const publicKeyOpenSSH = publicLineFromPublicKey(publicKey, comment);
|
|
902
|
+
const privateKeyOpenSSH = buildOpenSSHEd25519PrivateKey(
|
|
903
|
+
publicKey,
|
|
904
|
+
seed,
|
|
905
|
+
comment
|
|
906
|
+
);
|
|
907
|
+
return {
|
|
908
|
+
publicKey: publicKeyOpenSSH,
|
|
909
|
+
privateKey: privateKeyOpenSSH,
|
|
910
|
+
seedBase64: sodium.to_base64(seed, sodium.base64_variants.ORIGINAL)
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
async function getSshKey(sandbox) {
|
|
915
|
+
const token = await getAccessToken();
|
|
916
|
+
const { data } = await client.get(
|
|
917
|
+
"/xrpc/io.pocketenv.sandbox.getSandbox",
|
|
918
|
+
{
|
|
919
|
+
params: {
|
|
920
|
+
id: sandbox
|
|
921
|
+
},
|
|
922
|
+
headers: {
|
|
923
|
+
Authorization: `Bearer ${token}`
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
);
|
|
927
|
+
if (!data.sandbox) {
|
|
928
|
+
consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
|
|
929
|
+
process.exit(1);
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
const { data: sshKeys } = await client.get(
|
|
933
|
+
"/xrpc/io.pocketenv.sandbox.getSshKeys",
|
|
934
|
+
{
|
|
935
|
+
params: {
|
|
936
|
+
id: data.sandbox.id
|
|
937
|
+
},
|
|
938
|
+
headers: {
|
|
939
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
);
|
|
943
|
+
consola.log("\nPrivate Key:");
|
|
944
|
+
consola.log(sshKeys.privateKey.replace(/\\n/g, "\n"));
|
|
945
|
+
consola.log("\nPublic Key:");
|
|
946
|
+
consola.log(sshKeys.publicKey, "\n");
|
|
947
|
+
} catch (error) {
|
|
948
|
+
consola.info(
|
|
949
|
+
`No SSH keys found for this sandbox.
|
|
950
|
+
Create one with ${chalk.greenBright(`pocketenv sshkeys put ${sandbox} --generate`)}.`
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async function putKeys(sandbox, options) {
|
|
955
|
+
const token = await getAccessToken();
|
|
956
|
+
let privateKey;
|
|
957
|
+
let publicKey;
|
|
958
|
+
if (options.generate) {
|
|
959
|
+
const generated = await generateEd25519SSHKeyPair("");
|
|
960
|
+
privateKey = generated.privateKey;
|
|
961
|
+
publicKey = generated.publicKey;
|
|
962
|
+
}
|
|
963
|
+
if (options.privateKey && !options.generate) {
|
|
964
|
+
privateKey = await fs$1.readFile(options.privateKey, "utf8");
|
|
965
|
+
}
|
|
966
|
+
if (options.publicKey && !options.generate) {
|
|
967
|
+
publicKey = await fs$1.readFile(options.publicKey, "utf8");
|
|
968
|
+
}
|
|
969
|
+
const validatePrivateKey = (value) => {
|
|
970
|
+
const trimmed = value.trim();
|
|
971
|
+
if (!trimmed.startsWith("-----BEGIN")) {
|
|
972
|
+
return "Private key must start with a PEM header (e.g. -----BEGIN OPENSSH PRIVATE KEY-----)";
|
|
973
|
+
}
|
|
974
|
+
if (!trimmed.endsWith("-----")) {
|
|
975
|
+
return "Private key must end with a PEM footer (e.g. -----END OPENSSH PRIVATE KEY-----)";
|
|
976
|
+
}
|
|
977
|
+
return true;
|
|
978
|
+
};
|
|
979
|
+
if (!privateKey) {
|
|
980
|
+
privateKey = (await editor({
|
|
981
|
+
message: "Enter your SSH private key (opens in $EDITOR):",
|
|
982
|
+
postfix: ".pem",
|
|
983
|
+
waitForUserInput: false,
|
|
984
|
+
validate: validatePrivateKey
|
|
985
|
+
})).trim();
|
|
986
|
+
}
|
|
987
|
+
if (!publicKey) {
|
|
988
|
+
publicKey = (await input({
|
|
989
|
+
message: "Enter your SSH public key:",
|
|
990
|
+
validate: (value) => value.trim().length > 0 ? true : "Public key cannot be empty."
|
|
991
|
+
})).trim();
|
|
992
|
+
}
|
|
993
|
+
const { data } = await client.get(
|
|
994
|
+
"/xrpc/io.pocketenv.sandbox.getSandbox",
|
|
995
|
+
{
|
|
996
|
+
params: {
|
|
997
|
+
id: sandbox
|
|
998
|
+
},
|
|
999
|
+
headers: {
|
|
1000
|
+
Authorization: `Bearer ${token}`
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
);
|
|
1004
|
+
if (!data.sandbox) {
|
|
1005
|
+
consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
|
|
1006
|
+
process.exit(1);
|
|
1007
|
+
}
|
|
1008
|
+
const encryptedPrivateKey = await encrypt(privateKey);
|
|
1009
|
+
const redacted = (() => {
|
|
1010
|
+
const header = "-----BEGIN OPENSSH PRIVATE KEY-----";
|
|
1011
|
+
const footer = "-----END OPENSSH PRIVATE KEY-----";
|
|
1012
|
+
const headerIndex = privateKey.indexOf(header);
|
|
1013
|
+
const footerIndex = privateKey.indexOf(footer);
|
|
1014
|
+
if (headerIndex === -1 || footerIndex === -1)
|
|
1015
|
+
return privateKey.replace(/\n/g, "\\n");
|
|
1016
|
+
const body = privateKey.slice(headerIndex + header.length, footerIndex);
|
|
1017
|
+
const chars = body.split("");
|
|
1018
|
+
const nonNewlineIndices = chars.map((c, i) => c !== "\n" ? i : -1).filter((i) => i !== -1);
|
|
1019
|
+
const maskedBody = nonNewlineIndices.length > 15 ? (() => {
|
|
1020
|
+
const middleIndices = nonNewlineIndices.slice(10, -5);
|
|
1021
|
+
middleIndices.forEach((i) => {
|
|
1022
|
+
chars[i] = "*";
|
|
1023
|
+
});
|
|
1024
|
+
return chars.join("");
|
|
1025
|
+
})() : body;
|
|
1026
|
+
return `${header}${maskedBody}${footer}`.replace(/\n/g, "\\n");
|
|
1027
|
+
})();
|
|
1028
|
+
await client.post(
|
|
1029
|
+
"/xrpc/io.pocketenv.sandbox.putSshKeys",
|
|
1030
|
+
{
|
|
1031
|
+
id: data.sandbox.id,
|
|
1032
|
+
privateKey: encryptedPrivateKey,
|
|
1033
|
+
publicKey,
|
|
1034
|
+
redacted
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
headers: {
|
|
1038
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
);
|
|
1042
|
+
consola.log("\nPrivate Key:");
|
|
1043
|
+
consola.log(redacted.replace(/\\n/g, "\n"));
|
|
1044
|
+
consola.log("\nPublic Key:");
|
|
1045
|
+
consola.log(publicKey, "\n");
|
|
1046
|
+
consola.success("SSH keys saved successfully!");
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async function putAuthKey(sandbox) {
|
|
1050
|
+
const token = await getAccessToken();
|
|
1051
|
+
const authKey = (await password({ message: "Enter Tailscale Auth Key" })).trim();
|
|
1052
|
+
if (!authKey.startsWith("tskey-auth-")) {
|
|
1053
|
+
consola.error("Invalid Tailscale Auth Key");
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
const { data } = await client.get(
|
|
1057
|
+
"/xrpc/io.pocketenv.sandbox.getSandbox",
|
|
1058
|
+
{
|
|
1059
|
+
params: {
|
|
1060
|
+
id: sandbox
|
|
1061
|
+
},
|
|
1062
|
+
headers: {
|
|
1063
|
+
Authorization: `Bearer ${token}`
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
);
|
|
1067
|
+
if (!data.sandbox) {
|
|
1068
|
+
consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
const redacted = authKey.length > 14 ? authKey.slice(0, 11) + "*".repeat(authKey.length - 14) + authKey.slice(-3) : authKey;
|
|
1072
|
+
await client.post(
|
|
1073
|
+
"/xrpc/io.pocketenv.sandbox.putTailscaleAuthKey",
|
|
1074
|
+
{
|
|
1075
|
+
id: data.sandbox.id,
|
|
1076
|
+
authKey: await encrypt(authKey),
|
|
1077
|
+
redacted
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
headers: {
|
|
1081
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
);
|
|
1085
|
+
consola.success(redacted);
|
|
1086
|
+
consola.success(
|
|
1087
|
+
`Tailscale auth key saved for sandbox: ${chalk.greenBright(sandbox)}`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
async function getTailscaleAuthKey(sandbox) {
|
|
1091
|
+
const token = await getAccessToken();
|
|
1092
|
+
const { data } = await client.get(
|
|
1093
|
+
"/xrpc/io.pocketenv.sandbox.getSandbox",
|
|
1094
|
+
{
|
|
1095
|
+
params: {
|
|
1096
|
+
id: sandbox
|
|
1097
|
+
},
|
|
1098
|
+
headers: {
|
|
1099
|
+
Authorization: `Bearer ${token}`
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
);
|
|
1103
|
+
if (!data.sandbox) {
|
|
1104
|
+
consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
const { data: tailscale } = await client.get(
|
|
1109
|
+
"/xrpc/io.pocketenv.sandbox.getTailscaleAuthKey",
|
|
1110
|
+
{
|
|
1111
|
+
params: {
|
|
1112
|
+
id: data.sandbox.id
|
|
1113
|
+
},
|
|
1114
|
+
headers: {
|
|
1115
|
+
Authorization: `Bearer ${env$1.POCKETENV_TOKEN || token}`
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
);
|
|
1119
|
+
consola.info(`Tailscale auth key: ${chalk.greenBright(tailscale.authKey)}`);
|
|
1120
|
+
} catch {
|
|
1121
|
+
consola.error(
|
|
1122
|
+
`No Tailscale Auth Key found for sandbox: ${chalk.greenBright(sandbox)}`
|
|
1123
|
+
);
|
|
1124
|
+
process.exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const program = new Command();
|
|
1129
|
+
program.name("pocketenv").description(
|
|
1130
|
+
`
|
|
1131
|
+
___ __ __
|
|
1132
|
+
/ _ \\___ ____/ /_____ / /____ ___ _ __
|
|
1133
|
+
/ ___/ _ \\/ __/ '_/ -_) __/ -_) _ \\ |/ /
|
|
1134
|
+
/_/ \\___/\\__/_/\\_\\__/\\__/\\__/_/ /_/___/
|
|
1135
|
+
|
|
1136
|
+
Open, interoperable sandbox platform for agents and humans \u{1F4E6} \u2728
|
|
1137
|
+
`
|
|
1138
|
+
).version(version);
|
|
1139
|
+
program.configureHelp({
|
|
1140
|
+
styleTitle: (str) => chalk.bold.cyan(str),
|
|
1141
|
+
styleCommandText: (str) => chalk.yellow(str),
|
|
1142
|
+
styleDescriptionText: (str) => chalk.white(str),
|
|
1143
|
+
styleOptionText: (str) => chalk.green(str),
|
|
1144
|
+
styleArgumentText: (str) => chalk.magenta(str),
|
|
1145
|
+
styleSubcommandText: (str) => chalk.blue(str)
|
|
1146
|
+
});
|
|
1147
|
+
program.addHelpText(
|
|
1148
|
+
"after",
|
|
1149
|
+
`
|
|
1150
|
+
${chalk.bold("\nLearn more about Pocketenv:")} ${chalk.magentaBright("https://docs.pocketenv.io")}
|
|
1151
|
+
${chalk.bold("Join our Discord community:")} ${chalk.blueBright("https://discord.gg/9ada4pFUFS")}
|
|
1152
|
+
${chalk.bold("Report bugs:")} ${chalk.greenBright("https://github.com/pocketenv-io/pocketenv/issues")}
|
|
1153
|
+
`
|
|
1154
|
+
);
|
|
1155
|
+
program.command("login").argument("<handle>", "your AT Proto handle (e.g., <username>.bsky.social)").description("login with your AT Proto account and get a session token").action(login);
|
|
1156
|
+
program.command("whoami").description("get the current logged-in user").action(whoami);
|
|
1157
|
+
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);
|
|
1158
|
+
program.command("ls").description("list sandboxes").action(listSandboxes);
|
|
1159
|
+
program.command("start").argument("<sandbox>", "the sandbox to start").description("start the given sandbox").action(start);
|
|
1160
|
+
program.command("stop").argument("<sandbox>", "the sandbox to stop").description("stop the given sandbox").action(stop);
|
|
1161
|
+
program.command("create").aliases(["new"]).option("--provider, -p <provider>", "the provider to use for the sandbox").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);
|
|
1162
|
+
program.command("logout").description("logout (removes session token)").action(logout);
|
|
1163
|
+
program.command("rm").aliases(["delete", "remove"]).argument("<sandbox>", "the sandbox to delete").description("delete the given sandbox").action(deleteSandbox);
|
|
1164
|
+
const secret = program.command("secret").description("manage secrets");
|
|
1165
|
+
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);
|
|
1166
|
+
secret.command("list").aliases(["ls"]).argument("<sandbox>", "the sandbox to list secrets for").description("list secrets in the given sandbox").action(listSecrets);
|
|
1167
|
+
secret.command("delete").aliases(["rm", "remove"]).argument("<secret_id>", "the ID of the secret to delete").description("delete a secret").action(deleteSecret);
|
|
1168
|
+
const env = program.command("env").description("manage environment variables");
|
|
1169
|
+
env.command("put").argument("<sandbox>", "the sandbox to put the environment variable in").argument("<key>", "the key of the environment variable").argument("<value>", "the value of the environment variable").description("put an environment variable in the given sandbox").action(putEnv);
|
|
1170
|
+
env.command("list").aliases(["ls"]).argument("<sandbox>", "the sandbox to list environment variables for").description("list environment variables in the given sandbox").action(listEnvs);
|
|
1171
|
+
env.command("delete").aliases(["rm", "remove"]).argument("<variable_id>", "the ID of the environment variable to delete").description("delete an environment variable").action(deleteEnv);
|
|
1172
|
+
const sshkeys = program.command("sshkeys").description("manage SSH keys");
|
|
1173
|
+
sshkeys.command("put").argument("<sandbox>", "the sandbox to put the SSH key in").option("--private-key <path>", "the path to the SSH private key").option("--public-key <path>", "the path to the SSH public key").option("--generate, -g", "generate a new SSH key pair").description("put an SSH key in the given sandbox").action(putKeys);
|
|
1174
|
+
sshkeys.command("get").argument("<sandbox>", "the sandbox to get the SSH key from").description("get an SSH key (public key only) from the given sandbox").action(getSshKey);
|
|
1175
|
+
const tailscale = program.command("tailscale").description("manage Tailscale");
|
|
1176
|
+
tailscale.command("put").argument("<sandbox>", "the sandbox to put the Tailscale Auth Key in").description("put a Tailscale key in the given sandbox").action(putAuthKey);
|
|
1177
|
+
tailscale.command("get").argument("<sandbox>", "the sandbox to get the Tailscale key from").description("get a Tailscale key (redacted) from the given sandbox").action(getTailscaleAuthKey);
|
|
1178
|
+
if (process.argv.length <= 2) {
|
|
1179
|
+
program.help();
|
|
1180
|
+
}
|
|
1181
|
+
program.parse(process.argv);
|