the universal sandbox runtime for agents and humans. pocketenv.io
sandbox openclaw agent claude-code vercel-sandbox deno-sandbox cloudflare-sandbox atproto sprites daytona
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

Migrate CLI to @pocketenv/sdk

+267 -1021
+8
CHANGELOG.md
··· 2 2 3 3 All notable changes to this project will be documented in this file. 4 4 5 + ## [0.6.3] - 2026-04-05 6 + 7 + ### Changed 8 + 9 + - **Migrate CLI to `@pocketenv/sdk`**: All direct API/axios calls have been replaced with the official `@pocketenv/sdk`. A new `configureSdk()` helper initialises the SDK once per command using the resolved auth token. Encryption, redaction, and SSH key generation for secrets, files, tailscale, and SSH keys are now handled by the SDK internally. The `waitUntilRunning` local helper has been removed in favour of `sandbox.waitUntilRunning()`. 10 + 11 + --- 12 + 5 13 ## [0.6.2] - 2026-04-05 6 14 7 15 ### Added
+3
apps/cli/bun.lock
··· 6 6 "name": "cli", 7 7 "dependencies": { 8 8 "@inquirer/prompts": "^8.3.0", 9 + "@pocketenv/sdk": "^0.2.1", 9 10 "axios": "^1.13.6", 10 11 "chalk": "^5.6.2", 11 12 "cli-table3": "^0.6.5", ··· 145 146 "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], 146 147 147 148 "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="], 149 + 150 + "@pocketenv/sdk": ["@pocketenv/sdk@0.2.1", "", { "dependencies": { "ignore": "^7.0.5", "libsodium-wrappers": "^0.8.2", "tar": "^7.5.13", "zod": "^4.3.6" } }, "sha512-YNIpg0ggESU8pcKF5seaxxJs5bUcIYNVZ/dWpx/0rC+IlVjaQg5TGFL+vTYxmES1rDSoQDAdMH/VuGLoHZtbpw=="], 148 151 149 152 "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="], 150 153
+2 -1
apps/cli/package.json
··· 4 4 "bin": { 5 5 "pocketenv": "dist/index.js" 6 6 }, 7 - "version": "0.6.2", 7 + "version": "0.6.3", 8 8 "type": "module", 9 9 "keywords": [ 10 10 "sandbox", ··· 42 42 }, 43 43 "dependencies": { 44 44 "@inquirer/prompts": "^8.3.0", 45 + "@pocketenv/sdk": "^0.2.1", 45 46 "axios": "^1.13.6", 46 47 "chalk": "^5.6.2", 47 48 "cli-table3": "^0.6.5",
+26 -284
apps/cli/src/cmd/copy.ts
··· 1 1 import ora from "ora"; 2 2 import { c } from "../theme"; 3 - import { 4 - readdir, 5 - unlink, 6 - lstat, 7 - writeFile, 8 - mkdir, 9 - readFile, 10 - } from "node:fs/promises"; 11 - import { loadIgnoreFiles, makeIsIgnored } from "../lib/ignore"; 12 - import { join } from "node:path"; 13 - import * as tar from "tar"; 14 - import crypto from "node:crypto"; 15 3 import consola from "consola"; 16 - import getAccessToken from "../lib/getAccessToken"; 17 - import { client } from "../client"; 18 - import type { Sandbox } from "../types/sandbox"; 4 + import { Sandbox } from "@pocketenv/sdk"; 5 + import { configureSdk } from "../lib/sdk"; 19 6 20 7 async function copy(source: string, destination: string) { 21 8 const spinner = ora( ··· 29 16 30 17 const controller = new AbortController(); 31 18 const { signal } = controller; 32 - const tempFiles: string[] = []; 33 19 34 20 const onInterrupt = async () => { 35 21 controller.abort(); 36 22 spinner.stop(); 37 - await Promise.allSettled(tempFiles.map((f) => unlink(f))); 38 23 process.exit(130); 39 24 }; 40 25 41 26 process.once("SIGINT", onInterrupt); 42 27 28 + await configureSdk(); 29 + 43 30 try { 44 31 if (!source.includes(":/") && destination.includes(":/")) { 45 - await localToSandbox(source, destination, signal, tempFiles); 46 - } 47 - 48 - if (source.includes(":/") && !destination.includes(":/")) { 49 - await sandboxToLocal(source, destination, signal, tempFiles); 50 - } 51 - 52 - if (source.includes(":/") && destination.includes(":/")) { 32 + await localToSandbox(source, destination, signal); 33 + } else if (source.includes(":/") && !destination.includes(":/")) { 34 + await sandboxToLocal(source, destination, signal); 35 + } else if (source.includes(":/") && destination.includes(":/")) { 53 36 await sandboxToSandbox(source, destination, signal); 54 - } 55 - 56 - if (!source.includes(":/") && !destination.includes(":/")) { 37 + } else { 57 38 consola.error("Both source and destination cannot be local paths."); 58 39 process.exit(1); 59 40 } ··· 66 47 } 67 48 } 68 49 69 - async function compressDirectory(source: string): Promise<string> { 70 - try { 71 - if ((await lstat(source)).isFile()) { 72 - const output = `${crypto 73 - .createHash("sha256") 74 - .update(source) 75 - .digest("hex")}.tar.gz`; 76 - await tar.create( 77 - { 78 - file: output, 79 - gzip: { 80 - level: 6, 81 - }, 82 - }, 83 - [source], 84 - ); 85 - return output; 86 - } 87 - 88 - const isIgnored = makeIsIgnored(await loadIgnoreFiles(source)); 89 - // readdir with recursive:true includes hidden files/dirs (.git, .env, …) 90 - // which glob("**/*") silently skips. 91 - const files = ( 92 - await Promise.all( 93 - (await readdir(source, { recursive: true })).map(async (entry) => { 94 - if (isIgnored(entry)) return null; 95 - const stat = await lstat(join(source, entry)); 96 - if (stat.isDirectory() || stat.isSymbolicLink()) return null; 97 - return entry; 98 - }), 99 - ) 100 - ).filter((f): f is string => f !== null); 101 - 102 - const output = `${crypto 103 - .createHash("sha256") 104 - .update(source) 105 - .digest("hex")}.tar.gz`; 106 - 107 - await tar.create( 108 - { 109 - cwd: source, 110 - file: output, 111 - portable: true, 112 - gzip: { 113 - level: 6, 114 - }, 115 - }, 116 - files, 117 - ); 118 - 119 - return output; 120 - } catch (error) { 121 - consola.error("Failed to compress directory:", error); 122 - process.exit(1); 123 - } 124 - } 125 - 126 - async function decompressDirectory(archive: string, destination: string) { 127 - try { 128 - await mkdir(destination, { recursive: true }); 129 - await tar.extract({ 130 - file: archive, 131 - cwd: destination, 132 - }); 133 - } catch (error) { 134 - consola.error("Failed to decompress archive:", error); 135 - process.exit(1); 136 - } 137 - } 138 - 139 - async function uploadToStorage( 140 - filePath: string, 141 - signal: AbortSignal, 142 - ): Promise<string> { 143 - try { 144 - const token = await getAccessToken(); 145 - 146 - const fileBuffer = await readFile(filePath); 147 - const form = new FormData(); 148 - form.append( 149 - "file", 150 - new Blob([fileBuffer], { type: "application/gzip" }), 151 - "archive.tar.gz", 152 - ); 153 - 154 - const BASE_URL = "https://sandbox.pocketenv.io"; 155 - const response = await fetch(`${BASE_URL}/cp`, { 156 - method: "POST", 157 - signal, 158 - headers: { 159 - Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 160 - }, 161 - body: form, 162 - }); 163 - const data = (await response.json()) as { uuid: string }; 164 - return data.uuid; 165 - } catch (error) { 166 - consola.error("Failed to upload", error); 167 - process.exit(1); 168 - } 169 - } 170 - 171 50 async function localToSandbox( 172 51 source: string, 173 52 destination: string, 174 53 signal: AbortSignal, 175 - tempFiles: string[], 176 54 ) { 177 55 const sandboxId = destination.split(":/")[0]!; 178 - const token = await getAccessToken(); 179 - 180 - const { data } = await client.get<{ sandbox: Sandbox }>( 181 - "/xrpc/io.pocketenv.sandbox.getSandbox", 182 - { 183 - params: { id: sandboxId }, 184 - signal, 185 - headers: { Authorization: `Bearer ${token}` }, 186 - }, 187 - ); 188 - 189 - if (!data.sandbox) { 190 - consola.error(`Sandbox not found: ${c.primary(sandboxId)}`); 191 - process.exit(1); 192 - } 56 + const sandbox = await Sandbox.get(sandboxId); 193 57 194 - if (data.sandbox.status !== "RUNNING") { 58 + if (sandbox.data.status !== "RUNNING") { 195 59 consola.error(`Sandbox ${c.primary(sandboxId)} is not running.`); 196 60 process.exit(1); 197 61 } 198 62 199 - const output = await compressDirectory(source); 200 - tempFiles.push(output); 201 - 202 - signal.throwIfAborted(); 203 - 204 - const uuid = await uploadToStorage(output, signal); 205 - await unlink(output); 206 - tempFiles.splice(tempFiles.indexOf(output), 1); 207 - 208 - await client.post( 209 - "/xrpc/io.pocketenv.sandbox.pullDirectory", 210 - { uuid, sandboxId, directoryPath: destination.split(":")[1] }, 211 - { 212 - signal, 213 - headers: { 214 - Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 215 - }, 216 - }, 217 - ); 63 + await sandbox.copy.upload(source, destination.split(":")[1]!, { signal }); 218 64 } 219 65 220 66 async function sandboxToLocal( 221 67 source: string, 222 68 destination: string, 223 69 signal: AbortSignal, 224 - tempFiles: string[], 225 70 ) { 226 - const token = await getAccessToken(); 227 71 const sandboxId = source.split(":/")[0]!; 228 - 229 - const { data } = await client.get<{ sandbox: Sandbox }>( 230 - "/xrpc/io.pocketenv.sandbox.getSandbox", 231 - { 232 - params: { id: sandboxId }, 233 - signal, 234 - headers: { 235 - Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 236 - }, 237 - }, 238 - ); 239 - 240 - if (!data.sandbox) { 241 - consola.error(`Sandbox not found: ${c.primary(sandboxId)}`); 242 - process.exit(1); 243 - } 72 + const sandbox = await Sandbox.get(sandboxId); 244 73 245 - if (data.sandbox.status !== "RUNNING") { 74 + if (sandbox.data.status !== "RUNNING") { 246 75 consola.error(`Sandbox ${c.primary(sandboxId)} is not running.`); 247 76 process.exit(1); 248 77 } 249 78 250 - const response = await client.post<{ uuid: string }>( 251 - "/xrpc/io.pocketenv.sandbox.pushDirectory", 252 - { sandboxId, directoryPath: source.split(":")[1] }, 253 - { 254 - signal, 255 - headers: { 256 - Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 257 - }, 258 - }, 259 - ); 260 - 261 - const { uuid } = response.data; 262 - 263 - const downloadResponse = await fetch( 264 - `https://sandbox.pocketenv.io/cp/${uuid}`, 265 - { 266 - signal, 267 - headers: { 268 - Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 269 - }, 270 - }, 271 - ); 272 - 273 - if (!downloadResponse.ok) { 274 - consola.error(`Failed to download archive: ${downloadResponse.statusText}`); 275 - process.exit(1); 276 - } 277 - 278 - const arrayBuffer = await downloadResponse.arrayBuffer(); 279 - const buffer = Buffer.from(arrayBuffer); 280 - const tempFile = `${crypto.randomBytes(16).toString("hex")}.tar.gz`; 281 - tempFiles.push(tempFile); 282 - await writeFile(tempFile, buffer); 283 - await decompressDirectory(tempFile, destination); 284 - await unlink(tempFile); 285 - tempFiles.splice(tempFiles.indexOf(tempFile), 1); 79 + await sandbox.copy.download(source.split(":")[1]!, destination, { signal }); 286 80 } 287 81 288 82 async function sandboxToSandbox( ··· 293 87 const sourceSandboxId = source.split(":/")[0]!; 294 88 const destinationSandboxId = destination.split(":/")[0]!; 295 89 296 - const token = await getAccessToken(); 90 + const [sourceSandbox, destinationSandbox] = await Promise.all([ 91 + Sandbox.get(sourceSandboxId), 92 + Sandbox.get(destinationSandboxId), 93 + ]); 297 94 298 - const [{ data: sourceSandbox }, { data: destinationSandbox }] = 299 - await Promise.all([ 300 - client.get<{ sandbox: Sandbox }>( 301 - "/xrpc/io.pocketenv.sandbox.getSandbox", 302 - { 303 - params: { id: sourceSandboxId }, 304 - signal, 305 - headers: { 306 - Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 307 - }, 308 - }, 309 - ), 310 - client.get<{ sandbox: Sandbox }>( 311 - "/xrpc/io.pocketenv.sandbox.getSandbox", 312 - { 313 - params: { id: destinationSandboxId }, 314 - signal, 315 - headers: { 316 - Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 317 - }, 318 - }, 319 - ), 320 - ]); 321 - 322 - if (!sourceSandbox.sandbox) { 323 - consola.error(`Source sandbox not found: ${c.primary(sourceSandboxId)}`); 324 - process.exit(1); 325 - } 326 - 327 - if (!destinationSandbox.sandbox) { 328 - consola.error( 329 - `Destination Sandbox not found: ${c.primary(destinationSandboxId)}`, 330 - ); 331 - process.exit(1); 332 - } 333 - 334 - if (sourceSandbox.sandbox.status !== "RUNNING") { 95 + if (sourceSandbox.data.status !== "RUNNING") { 335 96 consola.error( 336 97 `Source Sandbox ${c.primary(sourceSandboxId)} is not running.`, 337 98 ); 338 99 process.exit(1); 339 100 } 340 101 341 - if (destinationSandbox.sandbox.status !== "RUNNING") { 102 + if (destinationSandbox.data.status !== "RUNNING") { 342 103 consola.error( 343 104 `Destination Sandbox ${c.primary(destinationSandboxId)} is not running.`, 344 105 ); 345 106 process.exit(1); 346 107 } 347 108 348 - const { data } = await client.post<{ uuid: string }>( 349 - "/xrpc/io.pocketenv.sandbox.pushDirectory", 350 - { sandboxId: sourceSandboxId, directoryPath: source.split(":")[1] }, 351 - { 352 - signal, 353 - headers: { 354 - Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 355 - }, 356 - }, 357 - ); 358 - 359 - await client.post( 360 - "/xrpc/io.pocketenv.sandbox.pullDirectory", 361 - { 362 - uuid: data.uuid, 363 - sandboxId: destinationSandboxId, 364 - directoryPath: destination.split(":")[1], 365 - }, 366 - { 367 - signal, 368 - headers: { 369 - Authorization: `Bearer ${process.env.POCKETENV_TOKEN || token}`, 370 - }, 371 - }, 109 + await sourceSandbox.copy.to( 110 + destinationSandboxId, 111 + source.split(":")[1]!, 112 + destination.split(":")[1]!, 113 + { signal }, 372 114 ); 373 115 } 374 116
+13 -23
apps/cli/src/cmd/create.ts
··· 1 1 import consola from "consola"; 2 - import { client } from "../client"; 3 - import getAccessToken from "../lib/getAccessToken"; 4 - import type { Sandbox } from "../types/sandbox"; 2 + import { Sandbox } from "@pocketenv/sdk"; 5 3 import connectToSandbox from "./ssh"; 6 4 import { c } from "../theme"; 7 5 import { expandRepo } from "../lib/expandRepo"; 8 - import waitUntilRunning from "../lib/waitUntilRunning"; 9 6 import encrypt from "../lib/sodium"; 10 7 import redact from "../lib/redact"; 8 + import { configureSdk } from "../lib/sdk"; 11 9 12 10 async function createSandbox( 13 11 name: string, ··· 23 21 repo?: string; 24 22 }, 25 23 ) { 26 - const token = await getAccessToken(); 24 + await configureSdk(); 27 25 if (repo) repo = expandRepo(repo); 28 26 29 27 const providerOptions: Record<string, any> = {}; ··· 94 92 } 95 93 96 94 try { 97 - const sandbox = await client.post<Sandbox>( 98 - "/xrpc/io.pocketenv.sandbox.createSandbox", 99 - { 100 - name, 101 - base: 102 - base ?? 103 - "at://did:plc:aturpi2ls3yvsmhc6wybomun/io.pocketenv.sandbox/openclaw", 104 - provider: provider ?? "cloudflare", 105 - repo, 106 - ...providerOptions, 107 - }, 108 - { 109 - headers: { 110 - Authorization: `Bearer ${token}`, 111 - }, 112 - }, 113 - ); 95 + const sandbox = await Sandbox.create({ 96 + name, 97 + base: 98 + base ?? 99 + "at://did:plc:aturpi2ls3yvsmhc6wybomun/io.pocketenv.sandbox/openclaw", 100 + provider: provider ?? "cloudflare", 101 + repo, 102 + providerOptions, 103 + }); 114 104 if (!ssh) { 115 105 consola.success( 116 106 `Sandbox created successfully: ${c.primary(sandbox.data.name)}`, 117 107 ); 118 108 return; 119 109 } 120 - await waitUntilRunning(sandbox.data.name, token); 110 + await sandbox.waitUntilRunning(); 121 111 await connectToSandbox(sandbox.data.name); 122 112 } catch (error) { 123 113 consola.error(`Failed to create sandbox: ${error}`);
+14 -64
apps/cli/src/cmd/env.ts
··· 1 - import { client } from "../client"; 2 - import getAccessToken from "../lib/getAccessToken"; 3 - import type { Sandbox } from "../types/sandbox"; 4 - import type { Variable } from "../types/variable"; 1 + import { Sandbox } from "@pocketenv/sdk"; 5 2 import dayjs from "dayjs"; 6 3 import consola from "consola"; 7 4 import Table from "cli-table3"; 5 + import { c } from "../theme"; 6 + import { configureSdk } from "../lib/sdk"; 7 + import { client } from "../client"; 8 + import getAccessToken from "../lib/getAccessToken"; 8 9 import { env } from "../lib/env"; 9 - import { c } from "../theme"; 10 10 11 - export async function listEnvs(sandbox: string) { 12 - const token = await getAccessToken(); 13 - const { data } = await client.get<{ sandbox: Sandbox }>( 14 - "/xrpc/io.pocketenv.sandbox.getSandbox", 15 - { 16 - params: { 17 - id: sandbox, 18 - }, 19 - headers: { 20 - Authorization: `Bearer ${token}`, 21 - }, 22 - }, 23 - ); 24 - 25 - if (!data.sandbox) { 26 - consola.error(`Sandbox not found: ${c.primary(sandbox)}`); 27 - process.exit(1); 28 - } 29 - 30 - const response = await client.get<{ variables: Variable[] }>( 31 - "/xrpc/io.pocketenv.variable.getVariables", 32 - { 33 - params: { 34 - sandboxId: data.sandbox.id, 35 - offset: 0, 36 - limit: 100, 37 - }, 38 - headers: { 39 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 40 - }, 41 - }, 42 - ); 11 + export async function listEnvs(sandboxName: string) { 12 + await configureSdk(); 13 + const sandbox = await Sandbox.get(sandboxName); 14 + const { variables } = await sandbox.env.list({ limit: 100, offset: 0 }); 43 15 44 16 const table = new Table({ 45 17 head: [ ··· 71 43 }, 72 44 }); 73 45 74 - for (const variable of response.data.variables) { 46 + for (const variable of variables) { 75 47 table.push([ 76 48 c.secondary(variable.id), 77 49 c.highlight(variable.name), ··· 83 55 consola.log(table.toString()); 84 56 } 85 57 86 - export async function putEnv(sandbox: string, key: string, value: string) { 87 - const token = await getAccessToken(); 88 - const { data } = await client.get<{ sandbox: Sandbox }>( 89 - "/xrpc/io.pocketenv.sandbox.getSandbox", 90 - { 91 - params: { 92 - id: sandbox, 93 - }, 94 - headers: { 95 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 96 - }, 97 - }, 98 - ); 99 - 100 - await client.post( 101 - "/xrpc/io.pocketenv.variable.addVariable", 102 - { 103 - variable: { sandboxId: data.sandbox.id, name: key, value: value }, 104 - }, 105 - { 106 - headers: { 107 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 108 - }, 109 - }, 110 - ); 111 - 58 + export async function putEnv(sandboxName: string, key: string, value: string) { 59 + await configureSdk(); 60 + const sandbox = await Sandbox.get(sandboxName); 61 + await sandbox.env.put(key, value); 112 62 consola.success("Variable updated successfully"); 113 63 } 114 64
+13 -34
apps/cli/src/cmd/exec.ts
··· 1 1 import consola from "consola"; 2 - import getAccessToken from "../lib/getAccessToken"; 3 - import { client } from "../client"; 4 - import { env } from "../lib/env"; 2 + import { Sandbox } from "@pocketenv/sdk"; 3 + import { configureSdk } from "../lib/sdk"; 5 4 6 - export async function exec(sandbox: string, command: string[]) { 7 - const token = await getAccessToken(); 5 + export async function exec(sandboxName: string, command: string[]) { 6 + await configureSdk(); 8 7 9 8 try { 9 + const sandbox = await Sandbox.get(sandboxName); 10 10 const [cmd, ...args] = command; 11 - const response = await client.post<{ 12 - stderr: string; 13 - stdout: string; 14 - exitCode: number; 15 - }>( 16 - "/xrpc/io.pocketenv.sandbox.exec", 17 - { 18 - command: `${cmd} ${args.join(" ")}`, 19 - }, 20 - { 21 - params: { 22 - id: sandbox, 23 - }, 24 - headers: { 25 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 26 - }, 27 - }, 28 - ); 11 + const result = await sandbox.exec(`${cmd} ${args.join(" ")}`); 29 12 30 - if (response.data.stdout) { 13 + if (result.stdout) { 31 14 process.stdout.write( 32 - response.data.stdout.endsWith("\n") 33 - ? response.data.stdout 34 - : response.data.stdout + "\n", 15 + result.stdout.endsWith("\n") ? result.stdout : result.stdout + "\n", 35 16 ); 36 17 } 37 - if (response.data.stderr) { 18 + if (result.stderr) { 38 19 process.stderr.write( 39 - response.data.stderr.endsWith("\n") 40 - ? response.data.stderr 41 - : response.data.stderr + "\n", 20 + result.stderr.endsWith("\n") ? result.stderr : result.stderr + "\n", 42 21 ); 43 22 } 44 23 45 - if (response.data.exitCode !== 0) { 46 - consola.error(`Command exited with code ${response.data.exitCode}`); 24 + if (result.exitCode !== 0) { 25 + consola.error(`Command exited with code ${result.exitCode}`); 47 26 } 48 27 49 - process.exit(response.data.exitCode); 28 + process.exit(result.exitCode); 50 29 } catch (error) { 51 30 consola.error("Failed to execute command:", error); 52 31 }
+9 -20
apps/cli/src/cmd/expose.ts
··· 1 1 import consola from "consola"; 2 - import getAccessToken from "../lib/getAccessToken"; 3 - import { client } from "../client"; 4 - import { env } from "../lib/env"; 2 + import { Sandbox } from "@pocketenv/sdk"; 5 3 import { c } from "../theme"; 4 + import { configureSdk } from "../lib/sdk"; 6 5 7 6 export async function exposePort( 8 - sandbox: string, 7 + sandboxName: string, 9 8 port: number, 10 9 description?: string, 11 10 ) { 12 - const token = await getAccessToken(); 11 + await configureSdk(); 13 12 try { 14 - const response = await client.post<{ previewUrl?: string }>( 15 - `/xrpc/io.pocketenv.sandbox.exposePort`, 16 - { port, description }, 17 - { 18 - params: { 19 - id: sandbox, 20 - }, 21 - headers: { 22 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 23 - }, 24 - }, 25 - ); 13 + const sandbox = await Sandbox.get(sandboxName); 14 + const result = await sandbox.expose(port, description); 26 15 27 16 consola.success( 28 - `Port ${c.primary(port)} exposed for sandbox ${c.primary(sandbox)}`, 17 + `Port ${c.primary(port)} exposed for sandbox ${c.primary(sandboxName)}`, 29 18 ); 30 19 31 - if (response.data.previewUrl) { 32 - consola.success(`Preview URL: ${c.secondary(response.data.previewUrl)}`); 20 + if (result.previewUrl) { 21 + consola.success(`Preview URL: ${c.secondary(result.previewUrl)}`); 33 22 } 34 23 } catch (error) { 35 24 consola.error("Failed to expose port:", error);
+17 -42
apps/cli/src/cmd/file.ts
··· 1 1 import consola from "consola"; 2 - import getAccessToken from "../lib/getAccessToken"; 3 2 import dayjs from "dayjs"; 4 3 import relativeTime from "dayjs/plugin/relativeTime"; 5 - import { client } from "../client"; 6 - import { env } from "../lib/env"; 4 + import { Sandbox } from "@pocketenv/sdk"; 7 5 import CliTable3 from "cli-table3"; 8 - import type { File } from "../types/file"; 9 6 import { c } from "../theme"; 10 7 import { editor } from "@inquirer/prompts"; 11 8 import fs from "fs/promises"; 12 9 import path from "path"; 13 - import encrypt from "../lib/sodium"; 10 + import { configureSdk } from "../lib/sdk"; 11 + import { client } from "../client"; 12 + import getAccessToken from "../lib/getAccessToken"; 13 + import { env } from "../lib/env"; 14 14 15 15 dayjs.extend(relativeTime); 16 16 17 17 export async function putFile( 18 - sandbox: string, 18 + sandboxName: string, 19 19 remotePath: string, 20 20 localPath?: string, 21 21 ) { 22 - const token = await getAccessToken(); 23 - 24 22 let content: string; 25 23 if (!process.stdin.isTTY) { 26 24 const chunks: Buffer[] = []; ··· 44 42 ).trim(); 45 43 } 46 44 45 + await configureSdk(); 46 + 47 47 try { 48 - await client.post( 49 - "/xrpc/io.pocketenv.file.addFile", 50 - { 51 - file: { 52 - sandboxId: sandbox, 53 - path: remotePath, 54 - content: await encrypt(content), 55 - }, 56 - }, 57 - { 58 - headers: { 59 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 60 - }, 61 - }, 62 - ); 63 - 48 + const sandbox = await Sandbox.get(sandboxName); 49 + await sandbox.file.write(remotePath, content); 64 50 consola.success( 65 - `File ${c.primary(remotePath)} successfully created in sandbox ${c.primary(sandbox)}`, 51 + `File ${c.primary(remotePath)} successfully created in sandbox ${c.primary(sandboxName)}`, 66 52 ); 67 53 } catch (error) { 68 54 consola.error(`Failed to create file: ${error}`); 69 55 } 70 56 } 71 57 72 - export async function listFiles(sandboxId: string) { 73 - const token = await getAccessToken(); 58 + export async function listFiles(sandboxName: string) { 59 + await configureSdk(); 74 60 75 - const response = await client.get<{ files: File[] }>( 76 - "/xrpc/io.pocketenv.file.getFiles", 77 - { 78 - params: { 79 - sandboxId, 80 - }, 81 - headers: { 82 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 83 - }, 84 - }, 85 - ); 61 + const sandbox = await Sandbox.get(sandboxName); 62 + const { files } = await sandbox.file.list(); 86 63 87 64 const table = new CliTable3({ 88 65 head: [c.primary("ID"), c.primary("PATH"), c.primary("CREATED AT")], ··· 109 86 }, 110 87 }); 111 88 112 - for (const file of response.data.files) { 89 + for (const file of files) { 113 90 table.push([ 114 91 c.secondary(file.id), 115 92 file.path, ··· 125 102 126 103 try { 127 104 await client.post(`/xrpc/io.pocketenv.file.deleteFile`, undefined, { 128 - params: { 129 - id, 130 - }, 105 + params: { id }, 131 106 headers: { 132 107 Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 133 108 },
+8 -29
apps/cli/src/cmd/list.ts
··· 1 - import { client } from "../client"; 2 1 import consola from "consola"; 3 - import { env } from "../lib/env"; 4 - import getAccessToken from "../lib/getAccessToken"; 5 - import type { Sandbox } from "../types/sandbox"; 2 + import { Sandbox } from "@pocketenv/sdk"; 6 3 import Table from "cli-table3"; 7 4 import dayjs from "dayjs"; 8 5 import relativeTime from "dayjs/plugin/relativeTime"; 9 - import type { Profile } from "../types/profile"; 10 6 import { c } from "../theme"; 7 + import { configureSdk } from "../lib/sdk"; 8 + 11 9 dayjs.extend(relativeTime); 12 10 13 11 async function listSandboxes() { 14 - const token = await getAccessToken(); 15 - 16 - const profile = await client.get<Profile>( 17 - "/xrpc/io.pocketenv.actor.getProfile", 18 - { 19 - headers: { 20 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 21 - }, 22 - }, 23 - ); 24 - 25 - const response = await client.get<{ sandboxes: Sandbox[] }>( 26 - "/xrpc/io.pocketenv.actor.getActorSandboxes", 27 - { 28 - params: { 29 - did: profile.data.did, 30 - offset: 0, 31 - limit: 100, 32 - }, 33 - }, 34 - ); 12 + await configureSdk(); 13 + const { sandboxes } = await Sandbox.list({ limit: 100, offset: 0 }); 35 14 36 15 const table = new Table({ 37 16 head: [ ··· 63 42 }, 64 43 }); 65 44 66 - for (const sandbox of response.data.sandboxes) { 45 + for (const sandbox of sandboxes) { 67 46 table.push([ 68 47 c.secondary(sandbox.name), 69 - sandbox.baseSandbox, 48 + sandbox.baseSandbox ?? "", 70 49 sandbox.status === "RUNNING" 71 50 ? c.highlight(sandbox.status) 72 - : sandbox.status, 51 + : sandbox.status ?? "", 73 52 dayjs(sandbox.createdAt).fromNow(), 74 53 ]); 75 54 }
+7 -19
apps/cli/src/cmd/ports.ts
··· 1 - import chalk from "chalk"; 2 - import { client } from "../client"; 3 - import { env } from "../lib/env"; 4 - import getAccessToken from "../lib/getAccessToken"; 5 - import type { Port } from "../types/port"; 1 + import { Sandbox } from "@pocketenv/sdk"; 6 2 import CliTable3 from "cli-table3"; 7 3 import consola from "consola"; 8 4 import { c } from "../theme"; 5 + import { configureSdk } from "../lib/sdk"; 9 6 10 - export async function listPorts(sandbox: string) { 11 - const token = await getAccessToken(); 7 + export async function listPorts(sandboxName: string) { 8 + await configureSdk(); 12 9 13 - const response = await client.get<{ ports: Port[] }>( 14 - "/xrpc/io.pocketenv.sandbox.getExposedPorts", 15 - { 16 - params: { 17 - id: sandbox, 18 - }, 19 - headers: { 20 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 21 - }, 22 - }, 23 - ); 10 + const sandbox = await Sandbox.get(sandboxName); 11 + const ports = await sandbox.ports.list(); 24 12 25 13 const table = new CliTable3({ 26 14 head: [ ··· 51 39 }, 52 40 }); 53 41 54 - for (const port of response.data.ports) { 42 + for (const port of ports) { 55 43 table.push([ 56 44 c.secondary(port.port), 57 45 port.description || "-",
+7 -29
apps/cli/src/cmd/ps.ts
··· 1 - import { client } from "../client"; 2 1 import consola from "consola"; 3 - import { env } from "../lib/env"; 4 - import getAccessToken from "../lib/getAccessToken"; 5 - import type { Sandbox } from "../types/sandbox"; 2 + import { Sandbox } from "@pocketenv/sdk"; 6 3 import Table from "cli-table3"; 7 4 import dayjs from "dayjs"; 8 5 import relativeTime from "dayjs/plugin/relativeTime"; 9 - import type { Profile } from "../types/profile"; 10 6 import { c } from "../theme"; 7 + import { configureSdk } from "../lib/sdk"; 8 + 11 9 dayjs.extend(relativeTime); 12 10 13 11 async function ps() { 14 - const token = await getAccessToken(); 15 - 16 - const profile = await client.get<Profile>( 17 - "/xrpc/io.pocketenv.actor.getProfile", 18 - { 19 - headers: { 20 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 21 - }, 22 - }, 23 - ); 24 - 25 - const response = await client.get<{ sandboxes: Sandbox[] }>( 26 - "/xrpc/io.pocketenv.actor.getActorSandboxes", 27 - { 28 - params: { 29 - did: profile.data.did, 30 - isRunning: true, 31 - offset: 0, 32 - limit: 100, 33 - }, 34 - }, 35 - ); 12 + await configureSdk(); 13 + const { sandboxes } = await Sandbox.list({ limit: 100, offset: 0, isRunning: true }); 36 14 37 15 const table = new Table({ 38 16 head: [ ··· 64 42 }, 65 43 }); 66 44 67 - for (const sandbox of response.data.sandboxes) { 45 + for (const sandbox of sandboxes) { 68 46 table.push([ 69 47 c.secondary(sandbox.name), 70 - sandbox.baseSandbox, 48 + sandbox.baseSandbox ?? "", 71 49 c.highlight( 72 50 `Up ${dayjs(sandbox.startedAt).fromNow().replace("ago", "")}`, 73 51 ),
+5 -13
apps/cli/src/cmd/rm.ts
··· 1 1 import consola from "consola"; 2 - import { client } from "../client"; 3 - import { env } from "../lib/env"; 4 - import getAccessToken from "../lib/getAccessToken"; 2 + import { Sandbox } from "@pocketenv/sdk"; 3 + import { configureSdk } from "../lib/sdk"; 5 4 6 5 async function deleteSandbox(id: string) { 7 - const token = await getAccessToken(); 6 + await configureSdk(); 8 7 try { 9 - await client.post("/xrpc/io.pocketenv.sandbox.deleteSandbox", undefined, { 10 - params: { 11 - id, 12 - }, 13 - headers: { 14 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 15 - }, 16 - }); 17 - 8 + const sandbox = await Sandbox.get(id); 9 + await sandbox.delete(); 18 10 consola.success("Sandbox deleted successfully"); 19 11 } catch { 20 12 consola.error("Failed to delete sandbox");
+20 -66
apps/cli/src/cmd/secret.ts
··· 1 - import { client } from "../client"; 2 - import getAccessToken from "../lib/getAccessToken"; 1 + import { Sandbox } from "@pocketenv/sdk"; 3 2 import { password } from "@inquirer/prompts"; 4 - import type { Sandbox } from "../types/sandbox"; 5 - import type { Secret } from "../types/secret"; 6 3 import chalk from "chalk"; 7 4 import consola from "consola"; 8 5 import Table from "cli-table3"; 9 6 import dayjs from "dayjs"; 10 7 import relativeTime from "dayjs/plugin/relativeTime"; 8 + import { c } from "../theme"; 9 + import { configureSdk } from "../lib/sdk"; 10 + import { client } from "../client"; 11 + import getAccessToken from "../lib/getAccessToken"; 11 12 import { env } from "../lib/env"; 12 - import encrypt from "../lib/sodium"; 13 - import { c } from "../theme"; 14 13 15 14 dayjs.extend(relativeTime); 16 15 17 - export async function listSecrets(sandbox: string) { 18 - const token = await getAccessToken(); 19 - const { data } = await client.get<{ sandbox: Sandbox }>( 20 - "/xrpc/io.pocketenv.sandbox.getSandbox", 21 - { 22 - params: { 23 - id: sandbox, 24 - }, 25 - headers: { 26 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 27 - }, 28 - }, 29 - ); 30 - const response = await client.get<{ secrets: Secret[] }>( 31 - "/xrpc/io.pocketenv.secret.getSecrets", 32 - { 33 - params: { 34 - sandboxId: data.sandbox.id, 35 - offset: 0, 36 - limit: 100, 37 - }, 38 - headers: { 39 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 40 - }, 41 - }, 42 - ); 16 + export async function listSecrets(sandboxName: string) { 17 + await configureSdk(); 18 + const sandbox = await Sandbox.get(sandboxName); 19 + const { secrets } = await sandbox.secret.list({ limit: 100, offset: 0 }); 43 20 44 21 const table = new Table({ 45 22 head: [c.primary("ID"), c.primary("NAME"), c.primary("CREATED AT")], ··· 66 43 }, 67 44 }); 68 45 69 - for (const secret of response.data.secrets) { 46 + for (const secret of secrets) { 70 47 table.push([ 71 48 c.secondary(secret.id), 72 49 c.highlight(secret.name), ··· 77 54 consola.log(table.toString()); 78 55 } 79 56 80 - export async function putSecret(sandbox: string, key: string) { 81 - const token = await getAccessToken(); 57 + export async function putSecret(sandboxName: string, key: string) { 82 58 const isStdinPiped = !process.stdin.isTTY; 83 59 const value = isStdinPiped 84 60 ? await new Promise<string>((resolve) => { ··· 89 65 }) 90 66 : await password({ message: "Enter secret value" }); 91 67 92 - const { data } = await client.get("/xrpc/io.pocketenv.sandbox.getSandbox", { 93 - params: { 94 - id: sandbox, 95 - }, 96 - headers: { 97 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 98 - }, 99 - }); 68 + await configureSdk(); 100 69 101 - if (!data.sandbox) { 102 - consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`); 103 - process.exit(1); 104 - } 70 + try { 71 + const sandbox = await Sandbox.get(sandboxName); 105 72 106 - try { 107 - await client.post( 108 - "/xrpc/io.pocketenv.secret.addSecret", 109 - { 110 - secret: { 111 - sandboxId: data.sandbox.id, 112 - name: key, 113 - value: await encrypt(value), 114 - }, 115 - }, 116 - { 117 - headers: { 118 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 119 - }, 120 - }, 121 - ); 73 + if (!sandbox) { 74 + consola.error(`Sandbox not found: ${chalk.greenBright(sandboxName)}`); 75 + process.exit(1); 76 + } 122 77 78 + await sandbox.secret.put(key, value); 123 79 consola.success("Secret added successfully"); 124 80 } catch (error) { 125 81 consola.error("Failed to add secret:", error); ··· 131 87 132 88 try { 133 89 await client.post("/xrpc/io.pocketenv.secret.deleteSecret", undefined, { 134 - params: { 135 - id, 136 - }, 90 + params: { id }, 137 91 headers: { 138 92 Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 139 93 },
+20 -50
apps/cli/src/cmd/service.ts
··· 1 1 import consola from "consola"; 2 - import { client } from "../client"; 3 - import { env } from "../lib/env"; 4 - import getAccessToken from "../lib/getAccessToken"; 5 - import type { Service } from "../types/service"; 2 + import { Sandbox } from "@pocketenv/sdk"; 6 3 import Table from "cli-table3"; 7 4 import dayjs from "dayjs"; 8 5 import relativeTime from "dayjs/plugin/relativeTime"; 9 6 import { c } from "../theme"; 10 7 import process from "node:process"; 8 + import { configureSdk } from "../lib/sdk"; 9 + import { client } from "../client"; 10 + import getAccessToken from "../lib/getAccessToken"; 11 + import { env } from "../lib/env"; 11 12 12 13 dayjs.extend(relativeTime); 13 14 ··· 22 23 command: string[], 23 24 { ports, description }: CreateServiceOptions, 24 25 ) { 25 - const token = await getAccessToken(); 26 + await configureSdk(); 26 27 27 28 try { 28 - await client.post( 29 - "/xrpc/io.pocketenv.service.addService", 30 - { 31 - service: { 32 - name, 33 - command: command.join(" "), 34 - description, 35 - ports: ports?.map((port) => parseInt(port)), 36 - }, 37 - }, 38 - { 39 - params: { 40 - sandboxId, 41 - }, 42 - headers: { 43 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 44 - }, 45 - }, 46 - ); 29 + const sandbox = await Sandbox.get(sandboxId); 30 + await sandbox.service.add(name, command.join(" "), { 31 + description, 32 + ports: ports?.map((port) => parseInt(port)), 33 + }); 47 34 48 35 consola.success(`Service ${c.highlight(name)} created successfully`); 49 36 } catch (error) { ··· 53 40 } 54 41 55 42 export async function listServices(sandboxId: string) { 56 - const token = await getAccessToken(); 43 + await configureSdk(); 57 44 58 45 try { 59 - const { data } = await client.get<{ services: Service[] }>( 60 - "/xrpc/io.pocketenv.service.getServices", 61 - { 62 - params: { 63 - sandboxId, 64 - }, 65 - headers: { 66 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 67 - }, 68 - }, 69 - ); 46 + const sandbox = await Sandbox.get(sandboxId); 47 + const { services } = await sandbox.service.list(); 70 48 71 49 const table = new Table({ 72 50 head: [ ··· 99 77 }, 100 78 }); 101 79 102 - for (const service of data.services) { 80 + for (const service of services) { 103 81 table.push([ 104 82 c.secondary(service.id), 105 83 service.name, 106 84 service.command, 107 85 service.status === "RUNNING" 108 86 ? c.highlight(service.status) 109 - : service.status, 87 + : service.status ?? "", 110 88 dayjs(service.createdAt).fromNow(), 111 89 ]); 112 90 } ··· 122 100 const token = await getAccessToken(); 123 101 try { 124 102 await client.post("/xrpc/io.pocketenv.service.restartService", undefined, { 125 - params: { 126 - serviceId, 127 - }, 103 + params: { serviceId }, 128 104 headers: { 129 105 Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 130 106 }, ··· 140 116 141 117 try { 142 118 await client.post("/xrpc/io.pocketenv.service.startService", undefined, { 143 - params: { 144 - serviceId, 145 - }, 119 + params: { serviceId }, 146 120 headers: { 147 121 Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 148 122 }, ··· 158 132 159 133 try { 160 134 await client.post("/xrpc/io.pocketenv.service.stopService", undefined, { 161 - params: { 162 - serviceId, 163 - }, 135 + params: { serviceId }, 164 136 headers: { 165 137 Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 166 138 }, ··· 176 148 177 149 try { 178 150 await client.post("/xrpc/io.pocketenv.service.deleteService", undefined, { 179 - params: { 180 - serviceId, 181 - }, 151 + params: { serviceId }, 182 152 headers: { 183 153 Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 184 154 },
+16 -103
apps/cli/src/cmd/sshkeys.ts
··· 1 1 import { editor, input } from "@inquirer/prompts"; 2 - import getAccessToken from "../lib/getAccessToken"; 3 - import { generateEd25519SSHKeyPair } from "../lib/sshKeys"; 4 2 import consola from "consola"; 5 3 import fs from "node:fs/promises"; 6 - import encrypt from "../lib/sodium"; 7 - import { client } from "../client"; 8 - import type { Sandbox } from "../types/sandbox"; 9 - import { env } from "../lib/env"; 10 - import type { SshKeys } from "../types/sshkeys"; 4 + import { Sandbox } from "@pocketenv/sdk"; 11 5 import chalk from "chalk"; 12 - 13 - export async function getSshKey(sandbox: string) { 14 - const token = await getAccessToken(); 15 - 16 - const { data } = await client.get<{ sandbox: Sandbox }>( 17 - "/xrpc/io.pocketenv.sandbox.getSandbox", 18 - { 19 - params: { 20 - id: sandbox, 21 - }, 22 - headers: { 23 - Authorization: `Bearer ${token}`, 24 - }, 25 - }, 26 - ); 6 + import { configureSdk } from "../lib/sdk"; 27 7 28 - if (!data.sandbox) { 29 - consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`); 30 - process.exit(1); 31 - } 8 + export async function getSshKey(sandboxName: string) { 9 + await configureSdk(); 32 10 33 11 try { 34 - const { data: sshKeys } = await client.get<SshKeys>( 35 - "/xrpc/io.pocketenv.sandbox.getSshKeys", 36 - { 37 - params: { 38 - id: data.sandbox.id, 39 - }, 40 - headers: { 41 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 42 - }, 43 - }, 44 - ); 12 + const sandbox = await Sandbox.get(sandboxName); 13 + const sshKeys = await sandbox.sshKeys.get(); 45 14 46 15 consola.log("\nPrivate Key:"); 47 - consola.log(sshKeys.privateKey.replace(/\\n/g, "\n")); 16 + consola.log((sshKeys.privateKey ?? "").replace(/\\n/g, "\n")); 48 17 consola.log("\nPublic Key:"); 49 18 consola.log(sshKeys.publicKey, "\n"); 50 19 } catch (error) { 51 20 consola.info( 52 - `No SSH keys found for this sandbox.\n Create one with ${chalk.greenBright(`pocketenv sshkeys put ${sandbox} --generate`)}.`, 21 + `No SSH keys found for this sandbox.\n Create one with ${chalk.greenBright(`pocketenv sshkeys put ${sandboxName} --generate`)}.`, 53 22 ); 54 23 } 55 24 } 56 25 57 26 export async function putKeys( 58 - sandbox: string, 27 + sandboxName: string, 59 28 options: { generate?: boolean; publicKey?: string; privateKey?: string }, 60 29 ) { 61 - const token = await getAccessToken(); 62 30 let privateKey: string | undefined; 63 31 let publicKey: string | undefined; 64 32 33 + await configureSdk(); 34 + const sandbox = await Sandbox.get(sandboxName); 35 + 65 36 if (options.generate) { 66 - const generated = await generateEd25519SSHKeyPair(""); 37 + const generated = await sandbox.sshKeys.generate(); 67 38 privateKey = generated.privateKey; 68 39 publicKey = generated.publicKey; 69 40 } ··· 108 79 ).trim(); 109 80 } 110 81 111 - const { data } = await client.get<{ sandbox: Sandbox }>( 112 - "/xrpc/io.pocketenv.sandbox.getSandbox", 113 - { 114 - params: { 115 - id: sandbox, 116 - }, 117 - headers: { 118 - Authorization: `Bearer ${token}`, 119 - }, 120 - }, 121 - ); 122 - 123 - if (!data.sandbox) { 124 - consola.error(`Sandbox not found: ${chalk.greenBright(sandbox)}`); 82 + if (!sandbox) { 83 + consola.error(`Sandbox not found: ${chalk.greenBright(sandboxName)}`); 125 84 process.exit(1); 126 85 } 127 86 128 - const encryptedPrivateKey = await encrypt(privateKey); 129 - 130 - const redacted = (() => { 131 - const header = "-----BEGIN OPENSSH PRIVATE KEY-----"; 132 - const footer = "-----END OPENSSH PRIVATE KEY-----"; 133 - const headerIndex = privateKey.indexOf(header); 134 - const footerIndex = privateKey.indexOf(footer); 135 - if (headerIndex === -1 || footerIndex === -1) 136 - return privateKey.replace(/\n/g, "\\n"); 137 - const body = privateKey.slice(headerIndex + header.length, footerIndex); 138 - const chars = body.split(""); 139 - const nonNewlineIndices = chars 140 - .map((c, i) => (c !== "\n" ? i : -1)) 141 - .filter((i) => i !== -1); 142 - const maskedBody = 143 - nonNewlineIndices.length > 15 144 - ? (() => { 145 - const middleIndices = nonNewlineIndices.slice(10, -5); 146 - middleIndices.forEach((i) => { 147 - chars[i] = "*"; 148 - }); 149 - return chars.join(""); 150 - })() 151 - : body; 152 - return `${header}${maskedBody}${footer}`.replace(/\n/g, "\\n"); 153 - })(); 154 - 155 87 try { 156 - await client.post( 157 - "/xrpc/io.pocketenv.sandbox.putSshKeys", 158 - { 159 - id: data.sandbox.id, 160 - privateKey: encryptedPrivateKey, 161 - publicKey, 162 - redacted, 163 - }, 164 - { 165 - headers: { 166 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 167 - }, 168 - }, 169 - ); 170 - 171 - consola.log("\nPrivate Key:"); 172 - consola.log(redacted.replace(/\\n/g, "\n")); 173 - consola.log("\nPublic Key:"); 174 - consola.log(publicKey, "\n"); 175 - 88 + await sandbox.sshKeys.put(publicKey, privateKey); 176 89 consola.success("SSH keys saved successfully!"); 177 90 } catch { 178 91 consola.error("Failed to save SSH keys");
+6 -22
apps/cli/src/cmd/start.ts
··· 1 1 import consola from "consola"; 2 2 import chalk from "chalk"; 3 - import getAccessToken from "../lib/getAccessToken"; 4 - import { client } from "../client"; 5 - import { env } from "../lib/env"; 3 + import { Sandbox } from "@pocketenv/sdk"; 6 4 import connectToSandbox from "./ssh"; 7 5 import { expandRepo } from "../lib/expandRepo"; 8 - import waitUntilRunning from "../lib/waitUntilRunning"; 6 + import { configureSdk } from "../lib/sdk"; 9 7 10 8 async function start( 11 9 name: string, ··· 15 13 keepAlive, 16 14 }: { ssh?: boolean; repo?: string; keepAlive?: boolean }, 17 15 ) { 18 - const token = await getAccessToken(); 16 + await configureSdk(); 19 17 if (repo) repo = expandRepo(repo); 20 18 21 19 try { 22 - const authToken = env.POCKETENV_TOKEN || token; 23 - await client.post( 24 - "/xrpc/io.pocketenv.sandbox.startSandbox", 25 - { 26 - repo, 27 - keepAlive, 28 - }, 29 - { 30 - params: { 31 - id: name, 32 - }, 33 - headers: { 34 - Authorization: `Bearer ${authToken}`, 35 - }, 36 - }, 37 - ); 20 + const sandbox = await Sandbox.get(name); 21 + await sandbox.start({ repo, keepAlive }); 38 22 39 23 if (ssh) { 40 - await waitUntilRunning(name, authToken); 24 + await sandbox.waitUntilRunning(); 41 25 await connectToSandbox(name); 42 26 return; 43 27 }
+5 -13
apps/cli/src/cmd/stop.ts
··· 1 1 import chalk from "chalk"; 2 2 import consola from "consola"; 3 - import getAccessToken from "../lib/getAccessToken"; 4 - import { client } from "../client"; 5 - import { env } from "../lib/env"; 3 + import { Sandbox } from "@pocketenv/sdk"; 4 + import { configureSdk } from "../lib/sdk"; 6 5 7 6 async function stop(name: string) { 8 - const token = await getAccessToken(); 7 + await configureSdk(); 9 8 10 9 try { 11 - await client.post("/xrpc/io.pocketenv.sandbox.stopSandbox", undefined, { 12 - params: { 13 - id: name, 14 - }, 15 - headers: { 16 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 17 - }, 18 - }); 19 - 10 + const sandbox = await Sandbox.get(name); 11 + await sandbox.stop(); 20 12 consola.success(`Sandbox ${chalk.greenBright(name)} stopped`); 21 13 } catch { 22 14 consola.error("Failed to stop sandbox");
+18 -85
apps/cli/src/cmd/tailscale.ts
··· 1 1 import { password } from "@inquirer/prompts"; 2 - import getAccessToken from "../lib/getAccessToken"; 3 - import { client } from "../client"; 4 - import type { Sandbox } from "../types/sandbox"; 2 + import { Sandbox } from "@pocketenv/sdk"; 5 3 import consola from "consola"; 6 - import type { TailscaleAuthKey } from "../types/tailscale-auth-key"; 7 - import { env } from "../lib/env"; 8 - import encrypt from "../lib/sodium"; 9 4 import { c } from "../theme"; 10 - 11 - export async function putAuthKey(sandbox: string) { 12 - const token = await getAccessToken(); 5 + import { configureSdk } from "../lib/sdk"; 13 6 7 + export async function putAuthKey(sandboxName: string) { 14 8 const authKey = ( 15 9 await password({ message: "Enter Tailscale Auth Key" }) 16 10 ).trim(); 17 11 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 - ); 12 + await configureSdk(); 34 13 35 - if (!data.sandbox) { 36 - consola.error(`Sandbox not found: ${c.primary(sandbox)}`); 14 + try { 15 + const sandbox = await Sandbox.get(sandboxName); 16 + await sandbox.tailscale.setAuthKey(authKey); 17 + consola.success( 18 + `Tailscale auth key saved for sandbox: ${c.primary(sandboxName)}`, 19 + ); 20 + } catch (error) { 21 + consola.error(`Failed to save Tailscale auth key: ${error}`); 37 22 process.exit(1); 38 23 } 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: ${c.primary(sandbox)}`, 64 - ); 65 24 } 66 25 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: ${c.primary(sandbox)}`); 84 - process.exit(1); 85 - } 26 + export async function getTailscaleAuthKey(sandboxName: string) { 27 + await configureSdk(); 86 28 87 29 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: ${c.primary(tailscale.authKey)}`); 30 + const sandbox = await Sandbox.get(sandboxName); 31 + const tailscale = await sandbox.tailscale.getAuthKey(); 32 + consola.info(`Tailscale auth key: ${c.primary(tailscale.authKey ?? "")}`); 100 33 } catch { 101 34 consola.error( 102 - `No Tailscale Auth Key found for sandbox: ${c.primary(sandbox)}`, 35 + `No Tailscale Auth Key found for sandbox: ${c.primary(sandboxName)}`, 103 36 ); 104 37 process.exit(1); 105 38 }
+7 -18
apps/cli/src/cmd/unexpose.ts
··· 1 1 import consola from "consola"; 2 - import getAccessToken from "../lib/getAccessToken"; 3 - import { client } from "../client"; 4 - import { env } from "../lib/env"; 2 + import { Sandbox } from "@pocketenv/sdk"; 5 3 import { c } from "../theme"; 4 + import { configureSdk } from "../lib/sdk"; 6 5 7 - export async function unexposePort(sandbox: string, port: number) { 8 - const token = await getAccessToken(); 6 + export async function unexposePort(sandboxName: string, port: number) { 7 + await configureSdk(); 9 8 10 9 try { 11 - await client.post( 12 - `/xrpc/io.pocketenv.sandbox.unexposePort`, 13 - { port }, 14 - { 15 - params: { 16 - id: sandbox, 17 - }, 18 - headers: { 19 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 20 - }, 21 - }, 22 - ); 10 + const sandbox = await Sandbox.get(sandboxName); 11 + await sandbox.unexpose(port); 23 12 24 13 consola.success( 25 - `Port ${c.primary(port)} unexposed for sandbox ${c.primary(sandbox)}`, 14 + `Port ${c.primary(port)} unexposed for sandbox ${c.primary(sandboxName)}`, 26 15 ); 27 16 } catch (error) { 28 17 consola.error(`Failed to unexpose port: ${error}`);
+18 -41
apps/cli/src/cmd/volume.ts
··· 1 1 import chalk from "chalk"; 2 2 import consola from "consola"; 3 - import getAccessToken from "../lib/getAccessToken"; 4 - import { client } from "../client"; 5 - import { env } from "../lib/env"; 6 - import type { Volume } from "../types/volume"; 3 + import { Sandbox } from "@pocketenv/sdk"; 7 4 import CliTable3 from "cli-table3"; 8 5 import { c } from "../theme"; 9 6 import dayjs from "dayjs"; 10 7 import relativeTime from "dayjs/plugin/relativeTime"; 8 + import { configureSdk } from "../lib/sdk"; 9 + import { client } from "../client"; 10 + import getAccessToken from "../lib/getAccessToken"; 11 + import { env } from "../lib/env"; 11 12 12 13 dayjs.extend(relativeTime); 13 14 14 - export async function listVolumes(sandboxId: string) { 15 - const token = await getAccessToken(); 15 + export async function listVolumes(sandboxName: string) { 16 + await configureSdk(); 16 17 17 - const response = await client.get<{ volumes: Volume[] }>( 18 - "/xrpc/io.pocketenv.volume.getVolumes", 19 - { 20 - params: { 21 - sandboxId, 22 - }, 23 - headers: { 24 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 25 - }, 26 - }, 27 - ); 18 + const sandbox = await Sandbox.get(sandboxName); 19 + const { volumes } = await sandbox.volume.list(); 28 20 29 21 const table = new CliTable3({ 30 22 head: [ ··· 56 48 }, 57 49 }); 58 50 59 - for (const volume of response.data.volumes) { 51 + for (const volume of volumes) { 60 52 table.push([ 61 53 c.secondary(volume.id), 62 54 volume.name, 63 - volume.path, 55 + volume.path ?? "", 64 56 dayjs(volume.createdAt).fromNow(), 65 57 ]); 66 58 } ··· 69 61 } 70 62 71 63 export async function createVolume( 72 - sandbox: string, 64 + sandboxName: string, 73 65 name: string, 74 - path: string, 66 + volumePath: string, 75 67 ) { 76 - const token = await getAccessToken(); 68 + await configureSdk(); 77 69 78 70 try { 79 - await client.post( 80 - "/xrpc/io.pocketenv.volume.addVolume", 81 - { 82 - volume: { 83 - sandboxId: sandbox, 84 - name, 85 - path, 86 - }, 87 - }, 88 - { 89 - headers: { 90 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 91 - }, 92 - }, 93 - ); 71 + const sandbox = await Sandbox.get(sandboxName); 72 + await sandbox.volume.create(name, { path: volumePath }); 94 73 95 74 consola.success( 96 - `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)}`, 75 + `Volume ${chalk.rgb(0, 232, 198)(name)} successfully mounted in sandbox ${chalk.rgb(0, 232, 198)(sandboxName)} at path ${chalk.rgb(0, 232, 198)(volumePath)}`, 97 76 ); 98 77 } catch (error) { 99 78 consola.error("Failed to create volume:", error); ··· 105 84 106 85 try { 107 86 await client.post(`/xrpc/io.pocketenv.volume.deleteVolume`, undefined, { 108 - params: { 109 - id, 110 - }, 87 + params: { id }, 111 88 headers: { 112 89 Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 113 90 },
+7 -23
apps/cli/src/cmd/vscode.ts
··· 1 1 import consola from "consola"; 2 - import getAccessToken from "../lib/getAccessToken"; 3 - import { client } from "../client"; 4 - import { env } from "../lib/env"; 2 + import { Sandbox } from "@pocketenv/sdk"; 5 3 import { c } from "../theme"; 4 + import { configureSdk } from "../lib/sdk"; 6 5 7 - export async function exposeVscode(sandbox: string) { 8 - const token = await getAccessToken(); 6 + export async function exposeVscode(sandboxName: string) { 7 + await configureSdk(); 9 8 try { 10 - const response = await client.post<{ previewUrl?: string }>( 11 - `/xrpc/io.pocketenv.sandbox.exposeVscode`, 12 - undefined, 13 - { 14 - params: { 15 - id: sandbox, 16 - }, 17 - headers: { 18 - Authorization: `Bearer ${env.POCKETENV_TOKEN || token}`, 19 - }, 20 - }, 21 - ); 22 - 23 - consola.success(`VS Code Server exposed for sandbox ${c.primary(sandbox)}`); 24 - 25 - if (response.data.previewUrl) { 26 - consola.success(`Preview URL: ${c.secondary(response.data.previewUrl)}`); 27 - } 9 + const sandbox = await Sandbox.get(sandboxName); 10 + await sandbox.vscode(); 11 + consola.success(`VS Code Server exposed for sandbox ${c.primary(sandboxName)}`); 28 12 } catch (error) { 29 13 consola.error("Failed to expose VS Code:", error); 30 14 process.exit(1);
+6 -15
apps/cli/src/cmd/whoami.ts
··· 1 1 import chalk from "chalk"; 2 - import { client } from "../client"; 3 - import { env } from "../lib/env"; 4 2 import consola from "consola"; 5 - import getAccessToken from "../lib/getAccessToken"; 6 - import type { Profile } from "../types/profile"; 3 + import { Sandbox } from "@pocketenv/sdk"; 4 + import { configureSdk } from "../lib/sdk"; 7 5 8 6 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}`; 7 + await configureSdk(); 8 + const profile = await Sandbox.getProfile(); 9 + const handle = `@${profile.handle}`; 19 10 consola.log( 20 - `You are logged in as ${chalk.cyan(handle)} (${profile.data.displayName}).`, 11 + `You are logged in as ${chalk.cyan(handle)} (${profile.displayName}).`, 21 12 ); 22 13 } 23 14
+12
apps/cli/src/lib/sdk.ts
··· 1 + import { Sandbox } from "@pocketenv/sdk"; 2 + import getAccessToken from "./getAccessToken"; 3 + import { env } from "./env"; 4 + 5 + export async function configureSdk(): Promise<void> { 6 + const token = await getAccessToken(); 7 + Sandbox.configure({ 8 + token: env.POCKETENV_TOKEN || token, 9 + baseUrl: env.POCKETENV_API_URL, 10 + publicKey: env.POCKETENV_PUBLIC_KEY, 11 + }); 12 + }
-27
apps/cli/src/lib/waitUntilRunning.ts
··· 1 - import { client } from "../client"; 2 - import type { Sandbox } from "../types/sandbox"; 3 - 4 - async function waitUntilRunning( 5 - name: string, 6 - authToken: string, 7 - timeoutMs = 60_000, 8 - intervalMs = 2_000, 9 - ): Promise<void> { 10 - const deadline = Date.now() + timeoutMs; 11 - while (Date.now() < deadline) { 12 - const response = await client.get<{ sandbox: Sandbox | null }>( 13 - "/xrpc/io.pocketenv.sandbox.getSandbox", 14 - { 15 - params: { id: name }, 16 - headers: { Authorization: `Bearer ${authToken}` }, 17 - }, 18 - ); 19 - if (response.data.sandbox?.status === "RUNNING") return; 20 - await new Promise((resolve) => setTimeout(resolve, intervalMs)); 21 - } 22 - throw new Error( 23 - `Sandbox ${name} did not reach RUNNING state within ${timeoutMs / 1000}s`, 24 - ); 25 - } 26 - 27 - export default waitUntilRunning;