@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.
@@ -0,0 +1,25 @@
1
+ import consola from "consola";
2
+ import chalk from "chalk";
3
+ import getAccessToken from "../lib/getAccessToken";
4
+ import { client } from "../client";
5
+ import { env } from "../lib/env";
6
+
7
+ async function start(name: string) {
8
+ const token = await getAccessToken();
9
+
10
+ await client.post("/xrpc/io.pocketenv.sandbox.startSandbox", undefined, {
11
+ params: {
12
+ id: name,
13
+ },
14
+ headers: {
15
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
16
+ },
17
+ });
18
+
19
+ consola.success(`Sandbox ${chalk.greenBright(name)} started`);
20
+ consola.log(
21
+ `Run ${chalk.greenBright(`pocketenv console ${name}`)} to access the sandbox`,
22
+ );
23
+ }
24
+
25
+ export default start;
@@ -0,0 +1,22 @@
1
+ import chalk from "chalk";
2
+ import consola from "consola";
3
+ import getAccessToken from "../lib/getAccessToken";
4
+ import { client } from "../client";
5
+ import { env } from "../lib/env";
6
+
7
+ async function stop(name: string) {
8
+ const token = await getAccessToken();
9
+
10
+ await client.post("/xrpc/io.pocketenv.sandbox.stopSandbox", undefined, {
11
+ params: {
12
+ id: name,
13
+ },
14
+ headers: {
15
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
16
+ },
17
+ });
18
+
19
+ consola.success(`Sandbox ${chalk.greenBright(name)} stopped`);
20
+ }
21
+
22
+ export default stop;
@@ -0,0 +1,106 @@
1
+ import { password } from "@inquirer/prompts";
2
+ import getAccessToken from "../lib/getAccessToken";
3
+ import { client } from "../client";
4
+ import type { Sandbox } from "../types/sandbox";
5
+ import consola from "consola";
6
+ import chalk from "chalk";
7
+ import type { TailscaleAuthKey } from "../types/tailscale-auth-key";
8
+ import { env } from "../lib/env";
9
+ import encrypt from "../lib/sodium";
10
+
11
+ export async function putAuthKey(sandbox: string) {
12
+ const token = await getAccessToken();
13
+
14
+ const authKey = (
15
+ await password({ message: "Enter Tailscale Auth Key" })
16
+ ).trim();
17
+
18
+ if (!authKey.startsWith("tskey-auth-")) {
19
+ consola.error("Invalid Tailscale Auth Key");
20
+ process.exit(1);
21
+ }
22
+
23
+ const { data } = await client.get<{ sandbox: Sandbox }>(
24
+ "/xrpc/io.pocketenv.sandbox.getSandbox",
25
+ {
26
+ params: {
27
+ id: sandbox,
28
+ },
29
+ headers: {
30
+ Authorization: `Bearer ${token}`,
31
+ },
32
+ },
33
+ );
34
+
35
+ if (!data.sandbox) {
36
+ consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
37
+ process.exit(1);
38
+ }
39
+
40
+ const redacted =
41
+ authKey.length > 14
42
+ ? authKey.slice(0, 11) +
43
+ "*".repeat(authKey.length - 14) +
44
+ authKey.slice(-3)
45
+ : authKey;
46
+
47
+ await client.post(
48
+ "/xrpc/io.pocketenv.sandbox.putTailscaleAuthKey",
49
+ {
50
+ id: data.sandbox.id,
51
+ authKey: await encrypt(authKey),
52
+ redacted,
53
+ },
54
+ {
55
+ headers: {
56
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
57
+ },
58
+ },
59
+ );
60
+
61
+ consola.success(redacted);
62
+ consola.success(
63
+ `Tailscale auth key saved for sandbox: ${chalk.greenBright(sandbox)}`,
64
+ );
65
+ }
66
+
67
+ export async function getTailscaleAuthKey(sandbox: string) {
68
+ const token = await getAccessToken();
69
+
70
+ const { data } = await client.get<{ sandbox: Sandbox }>(
71
+ "/xrpc/io.pocketenv.sandbox.getSandbox",
72
+ {
73
+ params: {
74
+ id: sandbox,
75
+ },
76
+ headers: {
77
+ Authorization: `Bearer ${token}`,
78
+ },
79
+ },
80
+ );
81
+
82
+ if (!data.sandbox) {
83
+ consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`);
84
+ process.exit(1);
85
+ }
86
+
87
+ try {
88
+ const { data: tailscale } = await client.get<TailscaleAuthKey>(
89
+ "/xrpc/io.pocketenv.sandbox.getTailscaleAuthKey",
90
+ {
91
+ params: {
92
+ id: data.sandbox.id,
93
+ },
94
+ headers: {
95
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
96
+ },
97
+ },
98
+ );
99
+ consola.info(`Tailscale auth key: ${chalk.greenBright(tailscale.authKey)}`);
100
+ } catch {
101
+ consola.error(
102
+ `No Tailscale Auth Key found for sandbox: ${chalk.greenBright(sandbox)}`,
103
+ );
104
+ process.exit(1);
105
+ }
106
+ }
@@ -0,0 +1,24 @@
1
+ import chalk from "chalk";
2
+ import { client } from "../client";
3
+ import { env } from "../lib/env";
4
+ import consola from "consola";
5
+ import getAccessToken from "../lib/getAccessToken";
6
+ import type { Profile } from "../types/profile";
7
+
8
+ async function whoami() {
9
+ const token = await getAccessToken();
10
+ const profile = await client.get<Profile>(
11
+ "/xrpc/io.pocketenv.actor.getProfile",
12
+ {
13
+ headers: {
14
+ Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`,
15
+ },
16
+ },
17
+ );
18
+ const handle = `@${profile.data.handle}`;
19
+ consola.log(
20
+ `You are logged in as ${chalk.cyan(handle)} (${profile.data.displayName}).`,
21
+ );
22
+ }
23
+
24
+ export default whoami;
package/src/index.ts ADDED
@@ -0,0 +1,187 @@
1
+ import chalk from "chalk";
2
+ import { version } from "../package.json" assert { type: "json" };
3
+ import { Command } from "commander";
4
+ import start from "./cmd/start";
5
+ import login from "./cmd/login";
6
+ import whoami from "./cmd/whoami";
7
+ import ssh from "./cmd/ssh";
8
+ import listSandboxes from "./cmd/list";
9
+ import stop from "./cmd/stop";
10
+ import createSandbox from "./cmd/create";
11
+ import logout from "./cmd/logout";
12
+ import deleteSandbox from "./cmd/rm";
13
+ import { deleteSecret, listSecrets, putSecret } from "./cmd/secret";
14
+ import { deleteEnv, listEnvs, putEnv } from "./cmd/env";
15
+ import { getSshKey, putKeys } from "./cmd/sshkeys";
16
+ import { getTailscaleAuthKey, putAuthKey } from "./cmd/tailscale";
17
+
18
+ const program = new Command();
19
+
20
+ program
21
+ .name("pocketenv")
22
+ .description(
23
+ `
24
+ ___ __ __
25
+ / _ \\___ ____/ /_____ / /____ ___ _ __
26
+ / ___/ _ \\/ __/ '_/ -_) __/ -_) _ \\ |/ /
27
+ /_/ \\___/\\__/_/\\_\\__/\\__/\\__/_/ /_/___/
28
+
29
+ Open, interoperable sandbox platform for agents and humans 📦 ✨
30
+ `,
31
+ )
32
+ .version(version);
33
+
34
+ program.configureHelp({
35
+ styleTitle: (str) => chalk.bold.cyan(str),
36
+ styleCommandText: (str) => chalk.yellow(str),
37
+ styleDescriptionText: (str) => chalk.white(str),
38
+ styleOptionText: (str) => chalk.green(str),
39
+ styleArgumentText: (str) => chalk.magenta(str),
40
+ styleSubcommandText: (str) => chalk.blue(str),
41
+ });
42
+
43
+ program.addHelpText(
44
+ "after",
45
+ `
46
+ ${chalk.bold("\nLearn more about Pocketenv:")} ${chalk.magentaBright("https://docs.pocketenv.io")}
47
+ ${chalk.bold("Join our Discord community:")} ${chalk.blueBright("https://discord.gg/9ada4pFUFS")}
48
+ ${chalk.bold("Report bugs:")} ${chalk.greenBright("https://github.com/pocketenv-io/pocketenv/issues")}
49
+ `,
50
+ );
51
+
52
+ program
53
+ .command("login")
54
+ .argument("<handle>", "your AT Proto handle (e.g., <username>.bsky.social)")
55
+ .description("login with your AT Proto account and get a session token")
56
+ .action(login);
57
+
58
+ program
59
+ .command("whoami")
60
+ .description("get the current logged-in user")
61
+ .action(whoami);
62
+
63
+ program
64
+ .command("console")
65
+ .aliases(["shell", "ssh", "s"])
66
+ .argument("[sandbox]", "the sandbox to connect to")
67
+ .description("open an interactive shell for the given sandbox")
68
+ .action(ssh);
69
+
70
+ program.command("ls").description("list sandboxes").action(listSandboxes);
71
+
72
+ program
73
+ .command("start")
74
+ .argument("<sandbox>", "the sandbox to start")
75
+ .description("start the given sandbox")
76
+ .action(start);
77
+
78
+ program
79
+ .command("stop")
80
+ .argument("<sandbox>", "the sandbox to stop")
81
+ .description("stop the given sandbox")
82
+ .action(stop);
83
+
84
+ program
85
+ .command("create")
86
+ .aliases(["new"])
87
+ .option("--provider, -p <provider>", "the provider to use for the sandbox")
88
+ .option("--ssh, -s", "connect to the Sandbox and automatically open a shell")
89
+ .argument("[name]", "the name of the sandbox to create")
90
+ .description("create a new sandbox")
91
+ .action(createSandbox);
92
+
93
+ program
94
+ .command("logout")
95
+ .description("logout (removes session token)")
96
+ .action(logout);
97
+
98
+ program
99
+ .command("rm")
100
+ .aliases(["delete", "remove"])
101
+ .argument("<sandbox>", "the sandbox to delete")
102
+ .description("delete the given sandbox")
103
+ .action(deleteSandbox);
104
+
105
+ const secret = program.command("secret").description("manage secrets");
106
+
107
+ secret
108
+ .command("put")
109
+ .argument("<sandbox>", "the sandbox to put the secret in")
110
+ .argument("<key>", "the key of the secret")
111
+ .description("put a secret in the given sandbox")
112
+ .action(putSecret);
113
+
114
+ secret
115
+ .command("list")
116
+ .aliases(["ls"])
117
+ .argument("<sandbox>", "the sandbox to list secrets for")
118
+ .description("list secrets in the given sandbox")
119
+ .action(listSecrets);
120
+
121
+ secret
122
+ .command("delete")
123
+ .aliases(["rm", "remove"])
124
+ .argument("<secret_id>", "the ID of the secret to delete")
125
+ .description("delete a secret")
126
+ .action(deleteSecret);
127
+
128
+ const env = program.command("env").description("manage environment variables");
129
+
130
+ env
131
+ .command("put")
132
+ .argument("<sandbox>", "the sandbox to put the environment variable in")
133
+ .argument("<key>", "the key of the environment variable")
134
+ .argument("<value>", "the value of the environment variable")
135
+ .description("put an environment variable in the given sandbox")
136
+ .action(putEnv);
137
+
138
+ env
139
+ .command("list")
140
+ .aliases(["ls"])
141
+ .argument("<sandbox>", "the sandbox to list environment variables for")
142
+ .description("list environment variables in the given sandbox")
143
+ .action(listEnvs);
144
+
145
+ env
146
+ .command("delete")
147
+ .aliases(["rm", "remove"])
148
+ .argument("<variable_id>", "the ID of the environment variable to delete")
149
+ .description("delete an environment variable")
150
+ .action(deleteEnv);
151
+
152
+ const sshkeys = program.command("sshkeys").description("manage SSH keys");
153
+
154
+ sshkeys
155
+ .command("put")
156
+ .argument("<sandbox>", "the sandbox to put the SSH key in")
157
+ .option("--private-key <path>", "the path to the SSH private key")
158
+ .option("--public-key <path>", "the path to the SSH public key")
159
+ .option("--generate, -g", "generate a new SSH key pair")
160
+ .description("put an SSH key in the given sandbox")
161
+ .action(putKeys);
162
+
163
+ sshkeys
164
+ .command("get")
165
+ .argument("<sandbox>", "the sandbox to get the SSH key from")
166
+ .description("get an SSH key (public key only) from the given sandbox")
167
+ .action(getSshKey);
168
+
169
+ const tailscale = program.command("tailscale").description("manage Tailscale");
170
+
171
+ tailscale
172
+ .command("put")
173
+ .argument("<sandbox>", "the sandbox to put the Tailscale Auth Key in")
174
+ .description("put a Tailscale Auth Key in the given sandbox")
175
+ .action(putAuthKey);
176
+
177
+ tailscale
178
+ .command("get")
179
+ .argument("<sandbox>", "the sandbox to get the Tailscale Auth Key from")
180
+ .description("get a Tailscale Auth Key (redacted) from the given sandbox")
181
+ .action(getTailscaleAuthKey);
182
+
183
+ if (process.argv.length <= 2) {
184
+ program.help();
185
+ }
186
+
187
+ program.parse(process.argv);
package/src/lib/env.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { cleanEnv, str } from "envalid";
2
+
3
+ export const env = cleanEnv(process.env, {
4
+ POCKETENV_TOKEN: str({ default: "" }),
5
+ POCKETENV_API_URL: str({ default: "https://api.pocketenv.io" }),
6
+ POCKETENV_CF_URL: str({ default: "https://sbx.pocketenv.io" }),
7
+ POCKETENV_TTY_URL: str({ default: "https://api.pocketenv.io" }),
8
+ POCKETENV_PUBLIC_KEY: str({
9
+ default: "2bf96e12d109e6948046a7803ef1696e12c11f04f20a6ce64dbd4bcd93db9341",
10
+ }),
11
+ });
@@ -0,0 +1,36 @@
1
+ import consola from "consola";
2
+ import chalk from "chalk";
3
+ import fs from "fs/promises";
4
+ import path from "path";
5
+ import os from "os";
6
+ import { env } from "process";
7
+
8
+ async function getAccessToken(): Promise<string> {
9
+ const tokenPath = path.join(os.homedir(), ".pocketenv", "token.json");
10
+ try {
11
+ await fs.access(tokenPath);
12
+ } catch (err) {
13
+ if (!env.POCKETENV_TOKEN) {
14
+ consola.error(
15
+ `You are not logged in. Please run ${chalk.greenBright(
16
+ "`pocketenv login <username>.bsky.social`",
17
+ )} first.`,
18
+ );
19
+ process.exit(1);
20
+ }
21
+ }
22
+
23
+ const tokenData = await fs.readFile(tokenPath, "utf-8");
24
+ const { token } = JSON.parse(tokenData);
25
+ if (!token) {
26
+ consola.error(
27
+ `You are not logged in. Please run ${chalk.greenBright(
28
+ "`rocksky login <username>.bsky.social`",
29
+ )} first.`,
30
+ );
31
+ process.exit(1);
32
+ }
33
+ return token;
34
+ }
35
+
36
+ export default getAccessToken;
@@ -0,0 +1,15 @@
1
+ import sodium from "libsodium-wrappers";
2
+ import { env } from "./env";
3
+
4
+ async function encrypt(message: string): Promise<string> {
5
+ await sodium.ready;
6
+
7
+ const sealed = sodium.crypto_box_seal(
8
+ sodium.from_string(message),
9
+ sodium.from_hex(env.POCKETENV_PUBLIC_KEY),
10
+ );
11
+
12
+ return sodium.to_base64(sealed, sodium.base64_variants.URLSAFE_NO_PADDING);
13
+ }
14
+
15
+ export default encrypt;
@@ -0,0 +1,122 @@
1
+ import sodium from "libsodium-wrappers";
2
+
3
+ export type SSHKeyPair = {
4
+ publicKey: string;
5
+ privateKey: string;
6
+ seedBase64: string;
7
+ };
8
+
9
+ function u32(n: number): Uint8Array {
10
+ return new Uint8Array([
11
+ (n >>> 24) & 0xff,
12
+ (n >>> 16) & 0xff,
13
+ (n >>> 8) & 0xff,
14
+ n & 0xff,
15
+ ]);
16
+ }
17
+
18
+ function concatBytes(...arrays: Uint8Array[]): Uint8Array {
19
+ const total = arrays.reduce((sum, arr) => sum + arr.length, 0);
20
+ const out = new Uint8Array(total);
21
+ let offset = 0;
22
+ for (const arr of arrays) {
23
+ out.set(arr, offset);
24
+ offset += arr.length;
25
+ }
26
+ return out;
27
+ }
28
+
29
+ function sshString(bytes: Uint8Array): Uint8Array {
30
+ return concatBytes(u32(bytes.length), bytes);
31
+ }
32
+
33
+ function text(value: string): Uint8Array {
34
+ return new TextEncoder().encode(value);
35
+ }
36
+
37
+ function wrapPem(label: string, bytes: Uint8Array): string {
38
+ const base64 = sodium.to_base64(bytes, sodium.base64_variants.ORIGINAL);
39
+ const lines = base64.match(/.{1,70}/g)?.join("\n") ?? base64;
40
+ return `-----BEGIN ${label}-----\n${lines}\n-----END ${label}-----\n`;
41
+ }
42
+
43
+ function buildEd25519PublicKeyBlob(publicKey: Uint8Array): Uint8Array {
44
+ return concatBytes(sshString(text("ssh-ed25519")), sshString(publicKey));
45
+ }
46
+
47
+ function publicLineFromPublicKey(
48
+ publicKey: Uint8Array,
49
+ comment: string,
50
+ ): string {
51
+ const blob = buildEd25519PublicKeyBlob(publicKey);
52
+ return `ssh-ed25519 ${sodium.to_base64(blob, sodium.base64_variants.ORIGINAL)} ${comment}`;
53
+ }
54
+
55
+ function buildOpenSSHEd25519PrivateKey(
56
+ publicKey: Uint8Array,
57
+ seed: Uint8Array,
58
+ comment: string,
59
+ ): string {
60
+ if (publicKey.length !== 32) throw new Error("Invalid public key length");
61
+ if (seed.length !== 32) throw new Error("Invalid seed length");
62
+
63
+ const privateKey64 = concatBytes(seed, publicKey);
64
+ const publicBlob = buildEd25519PublicKeyBlob(publicKey);
65
+ const checkint = crypto.getRandomValues(new Uint32Array(1))[0]!;
66
+ const commentBytes = text(comment);
67
+
68
+ const privateSectionWithoutPadding = concatBytes(
69
+ u32(checkint),
70
+ u32(checkint),
71
+ sshString(text("ssh-ed25519")),
72
+ sshString(publicKey),
73
+ sshString(privateKey64),
74
+ sshString(commentBytes),
75
+ );
76
+
77
+ const blockSize = 8;
78
+ const remainder = privateSectionWithoutPadding.length % blockSize;
79
+ const padLen = remainder === 0 ? 0 : blockSize - remainder;
80
+
81
+ const padding = new Uint8Array(padLen);
82
+ for (let i = 0; i < padLen; i++) padding[i] = i + 1;
83
+
84
+ const privateSection = concatBytes(privateSectionWithoutPadding, padding);
85
+
86
+ const opensshKey = concatBytes(
87
+ text("openssh-key-v1\0"),
88
+ sshString(text("none")),
89
+ sshString(text("none")),
90
+ sshString(new Uint8Array()),
91
+ u32(1),
92
+ sshString(publicBlob),
93
+ sshString(privateSection),
94
+ );
95
+
96
+ return wrapPem("OPENSSH PRIVATE KEY", opensshKey);
97
+ }
98
+
99
+ export async function generateEd25519SSHKeyPair(
100
+ comment = "user@browser",
101
+ ): Promise<SSHKeyPair> {
102
+ await sodium.ready;
103
+
104
+ const seed = new Uint8Array(32);
105
+ crypto.getRandomValues(seed);
106
+
107
+ const kp = sodium.crypto_sign_seed_keypair(seed);
108
+ const publicKey = new Uint8Array(kp.publicKey);
109
+
110
+ const publicKeyOpenSSH = publicLineFromPublicKey(publicKey, comment);
111
+ const privateKeyOpenSSH = buildOpenSSHEd25519PrivateKey(
112
+ publicKey,
113
+ seed,
114
+ comment,
115
+ );
116
+
117
+ return {
118
+ publicKey: publicKeyOpenSSH,
119
+ privateKey: privateKeyOpenSSH,
120
+ seedBase64: sodium.to_base64(seed, sodium.base64_variants.ORIGINAL),
121
+ };
122
+ }
@@ -0,0 +1,5 @@
1
+ export type File = {
2
+ id: string;
3
+ path: string;
4
+ createdAt: string;
5
+ };
@@ -0,0 +1,9 @@
1
+ export type Profile = {
2
+ id: string;
3
+ did: string;
4
+ handle: string;
5
+ displayName: string;
6
+ avatar: string;
7
+ createdAt: string;
8
+ updatedAt: string;
9
+ };
@@ -0,0 +1 @@
1
+ export type Provider = "daytona" | "deno" | "cloudflare" | "vercel" | "sprites";
@@ -0,0 +1,23 @@
1
+ import type { Profile } from "./profile";
2
+ import type { Provider } from "./providers";
3
+
4
+ export type Sandbox = {
5
+ id: string;
6
+ name: string;
7
+ provider: Provider;
8
+ baseSandbox: string;
9
+ displayName: string;
10
+ uri: string;
11
+ description?: string;
12
+ topics?: string[];
13
+ logo?: string;
14
+ readme?: string;
15
+ repo?: string;
16
+ vcpus?: number;
17
+ memory?: number;
18
+ installs: number;
19
+ status: "RUNNING" | "STOPPED";
20
+ startedAt?: string;
21
+ createdAt: string;
22
+ owner: Profile | null;
23
+ };
@@ -0,0 +1,5 @@
1
+ export type Secret = {
2
+ id: string;
3
+ name: string;
4
+ createdAt: string;
5
+ };
@@ -0,0 +1,6 @@
1
+ export type SshKeys = {
2
+ id: string;
3
+ privateKey: string;
4
+ publicKey: string;
5
+ createdAt: string;
6
+ };
@@ -0,0 +1,5 @@
1
+ export type TailscaleAuthKey = {
2
+ id: string;
3
+ authKey: string;
4
+ createdAt: string;
5
+ };
@@ -0,0 +1,6 @@
1
+ export type Variable = {
2
+ id: string;
3
+ name: string;
4
+ value: string;
5
+ createdAt: string;
6
+ };
@@ -0,0 +1,6 @@
1
+ export type Volume = {
2
+ id: string;
3
+ name: string;
4
+ path: string;
5
+ createdAt: string;
6
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }