diff --git a/examples/tpl-sbx-world-js/.env.example b/examples/tpl-sbx-world-js/.env.example new file mode 100644 index 00000000..2d8a7ff7 --- /dev/null +++ b/examples/tpl-sbx-world-js/.env.example @@ -0,0 +1,6 @@ +E2B_API_KEY=e2b_xxxxxxx +E2B_DOMAIN=abc.my-e2b.ai +NODE_TLS_REJECT_UNAUTHORIZED=0 +SANDBOX_MINUTES=10 +SANDBOX_MODE=code +E2B_IMAGE_REGISTRY= diff --git a/examples/tpl-sbx-world-js/README.md b/examples/tpl-sbx-world-js/README.md new file mode 100644 index 00000000..259e58c5 --- /dev/null +++ b/examples/tpl-sbx-world-js/README.md @@ -0,0 +1,147 @@ +# E2B Sandbox Template (Code and Base Modes) + +This example provides two sandbox template modes for E2B: +- Code Execution mode: based on `e2bdev/code-interpreter`, suitable for Python code execution via Jupyter. +- Base mode: based on `e2bdev/base`, suitable for general-purpose shell and tooling. + +## Prerequisites + +Before you begin, make sure you have: +- An E2B account (sign up at [e2b.dev](https://e2b.dev)) +- Your E2B API key (get it from your [E2B dashboard](https://e2b.dev/dashboard)) +- Node.js and npm/yarn (or similar) installed + +## Configuration + +1. Copy `.env.example` and fill values: + ``` + cp .env.example .env + ``` + Recommended variables: + - `E2B_API_KEY` (required) + - `E2B_DOMAIN` (optional, for private/self-hosted deployments) + - `SANDBOX_MINUTES` (optional, TTL for sandbox sessions) + - `SANDBOX_MODE` (optional, `code` or `base`, default `code`) + - `E2B_IMAGE_REGISTRY` (optional, image registry prefix like `192.168.123.81:5000`) + +### Private Deployment + +For private/self-hosted E2B deployments, configure these environment variables: + +``` +# Authentication +E2B_API_KEY=your_api_key_here + +# Domain +E2B_DOMAIN=your.domain.tld +``` + +You can also export variables in your shell if you prefer, but using `.env` is recommended. + +## Install Dependencies + +```bash +npm install +``` + +## Build Template + +Choose the mode via CLI or environment variables. + +```bash +# Code Execution mode (Jupyter & Code Interpreter) +npm run e2b:build:template -- --alias=my-code --mode=code + +# Base mode (general-purpose shell) +npm run e2b:build:template -- --alias=my-base --mode=base + +# With private image registry +npm run e2b:build:template -- --alias=my-code --mode=code --registry=192.168.123.81:5000 +``` + +Environment alternatives: +- `SANDBOX_MODE=code|base` +- `E2B_IMAGE_REGISTRY=` + +## Use the Template + +```ts +import { Sandbox } from 'e2b' + +// Create a new sandbox instance +const sandbox = await Sandbox.create('my-base') + +// Your sandbox is ready to use! +console.log('Sandbox created successfully') +``` + +## CLI Commands + +### Create / Connect + +``` +# Create new sandbox from alias (alias is required) +npm run e2b:create:sandbox -- --alias= + +# Connect to existing sandbox +npm run e2b:connect:sandbox -- --id= + +# Enter interactive shell (no time parameter required; session lasts until exit) +npm run e2b:connect:sandbox -- --id= --shell +npm run e2b:create:sandbox -- --alias= --shell + +# Optionally set runtime in minutes +npm run e2b:create:sandbox -- --minutes=10 +``` + +### List / Info / Kill / Pause / Resume + +``` +# List sandboxes (ID, STATE, NAME, START AT, END AT) +npm run e2b:list:sandbox + +# Show sandbox details +npm run e2b:info:sandbox -- --id= + +# Kill sandbox +npm run e2b:kill:sandbox -- --id= + +# Pause sandbox +npm run e2b:pause:sandbox -- --id= + +# Resume sandbox +npm run e2b:resume:sandbox -- --id= +``` + +### Templates + +``` +# List templates (ID, ALIASES, STATUS, BUILDS, CREATED/UPDATED/LAST USED) +npm run e2b:list:template + +# Delete template by ID +npm run e2b:delete:template -- --id= + +# Delete template by alias (auto resolves to ID) +npm run e2b:delete:template -- --alias= +``` + +## Template Structure + +- `template.ts` – template factory supporting `code` and `base` modes +- `build.template.ts` – build script with `--mode` and `--registry` support +- `operate.sandbox.ts` – CLI for create/connect/shell/list/info/kill/pause/resume + +## Code Interpreter Demo (Code Mode) + +Run a quick Python snippet via Code Interpreter (only in `code` mode): + +``` +npm run e2b:run:code -- --alias=my-code --code="print('hello')" +``` + +## Notes + +- For private deployments, set `E2B_DOMAIN` and ensure certificates are trusted. +- The `code` mode waits for Jupyter health at `http://localhost:49999/health`. +- The `base` mode starts with `sudo /bin/bash` and is ready for shell commands. diff --git a/examples/tpl-sbx-world-js/build.template.ts b/examples/tpl-sbx-world-js/build.template.ts new file mode 100644 index 00000000..9183ef04 --- /dev/null +++ b/examples/tpl-sbx-world-js/build.template.ts @@ -0,0 +1,33 @@ +import { Template, defaultBuildLogger } from 'e2b' +import dotenv from 'dotenv' +import path from 'node:path' +import { createTemplate } from './template' + +function getArg(name: string) { + const prefix = `--${name}=` + const arg = process.argv.slice(2).find((a) => a.startsWith(prefix)) + return arg ? arg.slice(prefix.length) : undefined +} + +async function main() { + dotenv.config({ override: true, path: path.resolve(process.cwd(), '.env') }) + const alias = getArg('alias') + if (!alias) { + process.stderr.write('alias is required. usage: npm run e2b:build:template -- --alias=\n') + process.exit(1) + } + const modeArg = (getArg('mode') as 'code' | 'base' | undefined) || (process.env.SANDBOX_MODE as 'code' | 'base' | undefined) || 'code' + const registry = getArg('registry') || process.env.E2B_IMAGE_REGISTRY + const tpl = createTemplate(modeArg, registry) + await Template.build(tpl, { + alias, + cpuCount: 1, + memoryMB: 512, + onBuildLogs: defaultBuildLogger(), + }) +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/examples/tpl-sbx-world-js/operate.sandbox.ts b/examples/tpl-sbx-world-js/operate.sandbox.ts new file mode 100644 index 00000000..d9721781 --- /dev/null +++ b/examples/tpl-sbx-world-js/operate.sandbox.ts @@ -0,0 +1,319 @@ +import { Sandbox, ApiClient, ConnectionConfig } from 'e2b' +import dotenv from 'dotenv' +import path from 'node:path' + +function getArg(name: string) { + const prefix = `--${name}=` + const arg = process.argv.slice(2).find((a) => a.startsWith(prefix)) + return arg ? arg.slice(prefix.length) : undefined +} + +function hasFlag(name: string) { + return process.argv.slice(2).includes(`--${name}`) +} + +function padEnd(s: string, w: number) { + return s.length >= w ? s : s + ' '.repeat(w - s.length) +} + +function formatDate(d: Date) { + const iso = d.toISOString() + return iso.replace('T', ' ').replace('.000Z', 'Z') +} + +function getAlias() { + const fromArg = getArg('alias') + return fromArg +} + +function getMinutes(): number | undefined { + const fromArg = getArg('minutes') + if (fromArg) { + const n = Number(fromArg) + if (!Number.isNaN(n) && n > 0) return n + } + const fromEnv = process.env.SANDBOX_MINUTES + if (fromEnv) { + const n = Number(fromEnv) + if (!Number.isNaN(n) && n > 0) return n + } + return undefined +} + +async function main() { + dotenv.config({ override: true, path: path.resolve(process.cwd(), '.env') }) + if (!process.env.E2B_API_KEY) { + process.stderr.write('E2B_API_KEY not set\n') + process.exit(1) + } + + const client = new ApiClient(new ConnectionConfig(), { requireApiKey: true }) + + if (hasFlag('list-templates')) { + const res = await client.api.GET('/templates') + if (res.error) { + const status = res.response?.status + const content = typeof res.error === 'string' ? res.error : res.error?.message + const msg = status === 401 + ? `Authentication failed: invalid E2B_API_KEY or domain${content ? ` - ${content}` : ''}` + : `${status ?? ''}: ${content ?? 'API error'}` + process.stderr.write(msg + '\n') + process.exit(1) + } + const items = res.data || [] + if (!items.length) { + process.stdout.write('No templates found\n') + return + } + const rows = items.map((t) => ({ + id: t.templateID, + aliases: (t.aliases || []).join(','), + status: t.buildStatus, + builds: String(t.buildCount ?? ''), + createdAt: formatDate(new Date(t.createdAt)), + updatedAt: formatDate(new Date(t.updatedAt)), + lastUsedAt: t.lastSpawnedAt ? formatDate(new Date(t.lastSpawnedAt)) : '', + })) + const wId = Math.max('TEMPLATE ID'.length, ...rows.map((r) => r.id.length)) + const wAliases = Math.max('ALIASES'.length, ...rows.map((r) => r.aliases.length)) + const wStatus = Math.max('STATUS'.length, ...rows.map((r) => r.status.length)) + const wBuilds = Math.max('BUILDS'.length, ...rows.map((r) => r.builds.length)) + const wCreated = Math.max('CREATED AT'.length, ...rows.map((r) => r.createdAt.length)) + const wUpdated = Math.max('UPDATED AT'.length, ...rows.map((r) => r.updatedAt.length)) + const wLast = Math.max('LAST USED AT'.length, ...rows.map((r) => r.lastUsedAt.length)) + process.stdout.write( + `${padEnd('TEMPLATE ID', wId)} ${padEnd('ALIASES', wAliases)} ${padEnd('STATUS', wStatus)} ${padEnd('BUILDS', wBuilds)} ${padEnd('CREATED AT', wCreated)} ${padEnd('UPDATED AT', wUpdated)} ${padEnd('LAST USED AT', wLast)}\n` + ) + process.stdout.write( + `${'-'.repeat(wId)} ${'-'.repeat(wAliases)} ${'-'.repeat(wStatus)} ${'-'.repeat(wBuilds)} ${'-'.repeat(wCreated)} ${'-'.repeat(wUpdated)} ${'-'.repeat(wLast)}\n` + ) + for (const r of rows) { + process.stdout.write( + `${padEnd(r.id, wId)} ${padEnd(r.aliases, wAliases)} ${padEnd(r.status, wStatus)} ${padEnd(r.builds, wBuilds)} ${padEnd(r.createdAt, wCreated)} ${padEnd(r.updatedAt, wUpdated)} ${padEnd(r.lastUsedAt, wLast)}\n` + ) + } + return + } + + const deleteTemplateId = hasFlag('delete-template') ? getArg('id') || getArg('template-id') || getArg('alias') : undefined + if (deleteTemplateId) { + let id = deleteTemplateId + // resolve alias to templateID if alias provided + if (!/^\w/.test(id) || id.includes('-') || id.length < 10) { + const res = await client.api.GET('/templates') + if (res.error) { + const status = res.response?.status + const content = typeof res.error === 'string' ? res.error : res.error?.message + const msg = status === 401 + ? `Authentication failed: invalid E2B_API_KEY or domain${content ? ` - ${content}` : ''}` + : `${status ?? ''}: ${content ?? 'API error'}` + process.stderr.write(msg + '\n') + process.exit(1) + } + const match = (res.data || []).find((t) => t.aliases && t.aliases.includes(id)) + if (match) id = match.templateID + } + const del = await client.api.DELETE('/templates/{templateID}', { params: { path: { templateID: id } } }) + if (del.error) { + process.stderr.write(`DELETE FAILED: ${id}\n`) + process.exit(1) + } + process.stdout.write(`DELETED TEMPLATE: ${id}\n`) + return + } + + if (hasFlag('connect')) { + const id = getArg('id') || getArg('sandbox-id') + if (!id) { + process.stderr.write('Usage: --connect --id= [--shell] [--minutes=N]\n') + return + } + const minutes = getMinutes() + const shell = hasFlag('shell') + const ttlCapMs = 3_600_000 + const timeoutMs = minutes ? minutes * 60_000 : shell ? ttlCapMs : undefined + const sandbox = await Sandbox.connect(id, timeoutMs ? { timeoutMs } : undefined) + if (timeoutMs) { + await sandbox.setTimeout(timeoutMs) + } + const info = await sandbox.getInfo() + process.stdout.write( + `mode:connect id:${sandbox.sandboxId} template:${info.name ?? ''} minutes:${minutes ?? ''} endAt:${info.endAt.toISOString()}\n` + ) + if (shell) { + const cols = process.stdout.columns || 80 + const rows = process.stdout.rows || 24 + const handle = await sandbox.pty.create({ + cols, + rows, + onData: (data) => process.stdout.write(Buffer.from(data)), + timeoutMs: minutes ? minutes * 60_000 : 0, + }) + if (process.stdin.isTTY) { + process.stdin.setRawMode?.(true) + } + process.stdin.resume() + const onInput = (chunk: Buffer) => { + const u8 = new Uint8Array(chunk) + sandbox.pty.sendInput(handle.pid, u8).catch(() => {}) + } + process.stdin.on('data', onInput) + const onResize = () => { + const c = process.stdout.columns || 80 + const r = process.stdout.rows || 24 + sandbox.pty.resize(handle.pid, { cols: c, rows: r }).catch(() => {}) + } + process.stdout.on('resize', onResize) + try { + await handle.wait().catch(() => {}) + } finally { + process.stdout.off('resize', onResize) + process.stdin.off('data', onInput) + if (process.stdin.isTTY) { + process.stdin.setRawMode?.(false) + } + } + } + return + } + + if (hasFlag('list')) { + const paginator = Sandbox.list() + const items = await paginator.nextItems() + const rows = items.map((it) => ({ + id: it.sandboxId, + state: it.state, + name: it.name ?? '', + startAt: formatDate(it.startedAt), + endAt: formatDate(it.endAt), + })) + const wId = Math.max('ID'.length, ...rows.map((r) => r.id.length)) + const wState = Math.max('STATE'.length, ...rows.map((r) => r.state.length)) + const wName = Math.max('NAME'.length, ...rows.map((r) => r.name.length)) + const wStart = Math.max('START AT'.length, ...rows.map((r) => r.startAt.length)) + const wEnd = Math.max('END AT'.length, ...rows.map((r) => r.endAt.length)) + process.stdout.write( + `${padEnd('ID', wId)} ${padEnd('STATE', wState)} ${padEnd('NAME', wName)} ${padEnd('START AT', wStart)} ${padEnd('END AT', wEnd)}\n` + ) + process.stdout.write( + `${'-'.repeat(wId)} ${'-'.repeat(wState)} ${'-'.repeat(wName)} ${'-'.repeat(wStart)} ${'-'.repeat(wEnd)}\n` + ) + for (const r of rows) { + process.stdout.write( + `${padEnd(r.id, wId)} ${padEnd(r.state, wState)} ${padEnd(r.name, wName)} ${padEnd(r.startAt, wStart)} ${padEnd(r.endAt, wEnd)}\n` + ) + } + return + } + + const infoId = hasFlag('info') ? getArg('id') || getArg('info') : undefined + if (infoId) { + const it = await Sandbox.getInfo(infoId) + const lines = [ + `ID : ${it.sandboxId}`, + `STATE : ${it.state}`, + `NAME : ${it.name ?? ''}`, + `START AT: ${formatDate(it.startedAt)}`, + `END AT : ${formatDate(it.endAt)}`, + `CPU : ${it.cpuCount}`, + `MEM MB : ${it.memoryMB}`, + ] + for (const line of lines) process.stdout.write(line + '\n') + return + } + + const killId = hasFlag('kill') ? getArg('id') || getArg('kill') : undefined + if (killId) { + const s = await Sandbox.connect(killId) + await s.kill() + process.stdout.write(`KILLED : ${killId}\n`) + return + } + + const pauseId = hasFlag('pause') ? getArg('id') || getArg('pause') : undefined + if (pauseId) { + const paused = await Sandbox.betaPause(pauseId) + process.stdout.write(`${paused ? 'PAUSED :' : 'PAUSE :'} ${pauseId}\n`) + return + } + + const resumeId = hasFlag('resume') ? getArg('id') || getArg('resume') : undefined + if (resumeId) { + const s = await Sandbox.connect(resumeId) + const it = await s.getInfo() + process.stdout.write(`RESUMED : ${resumeId} STATE:${it.state}\n`) + return + } + + const sandboxIdArg = getArg('sandbox-id') + const minutes = getMinutes() + const shell = hasFlag('shell') + const ttlCapMs = 3_600_000 + const timeoutMs = minutes ? minutes * 60_000 : shell ? ttlCapMs : undefined + + let sandbox: Sandbox + let mode: 'create' | 'connect' + + if (sandboxIdArg) { + mode = 'connect' + sandbox = await Sandbox.connect(sandboxIdArg, timeoutMs ? { timeoutMs } : undefined) + if (timeoutMs) { + await sandbox.setTimeout(timeoutMs) + } + } else { + mode = 'create' + const alias = getAlias() + if (!alias) { + process.stderr.write('alias is required. usage: --alias= [--shell] [--minutes=N]\n') + process.exit(1) + } + sandbox = await Sandbox.create(alias, timeoutMs ? { timeoutMs } : undefined) + } + + const info = await sandbox.getInfo() + const endAt = info.endAt.toISOString() + + process.stdout.write( + `mode:${mode} id:${sandbox.sandboxId} template:${info.name ?? ''} minutes:${minutes ?? ''} endAt:${endAt}\n` + ) + + if (shell) { + const cols = process.stdout.columns || 80 + const rows = process.stdout.rows || 24 + const handle = await sandbox.pty.create({ + cols, + rows, + onData: (data) => process.stdout.write(Buffer.from(data)), + timeoutMs: minutes ? minutes * 60_000 : 0, + }) + if (process.stdin.isTTY) { + process.stdin.setRawMode?.(true) + } + process.stdin.resume() + const onInput = (chunk: Buffer) => { + const u8 = new Uint8Array(chunk) + sandbox.pty.sendInput(handle.pid, u8).catch(() => {}) + } + process.stdin.on('data', onInput) + const onResize = () => { + const c = process.stdout.columns || 80 + const r = process.stdout.rows || 24 + sandbox.pty.resize(handle.pid, { cols: c, rows: r }).catch(() => {}) + } + process.stdout.on('resize', onResize) + try { + await handle.wait().catch(() => {}) + } finally { + process.stdout.off('resize', onResize) + process.stdin.off('data', onInput) + if (process.stdin.isTTY) { + process.stdin.setRawMode?.(false) + } + } + } +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/examples/tpl-sbx-world-js/package.json b/examples/tpl-sbx-world-js/package.json new file mode 100644 index 00000000..daf98981 --- /dev/null +++ b/examples/tpl-sbx-world-js/package.json @@ -0,0 +1,20 @@ +{ + "scripts": { + "e2b:build:template": "npx tsx build.template.ts", + "e2b:create:sandbox": "npx tsx operate.sandbox.ts", + "e2b:list:sandbox": "npx tsx operate.sandbox.ts --list", + "e2b:kill:sandbox": "npx tsx operate.sandbox.ts --kill", + "e2b:info:sandbox": "npx tsx operate.sandbox.ts --info", + "e2b:pause:sandbox": "npx tsx operate.sandbox.ts --pause", + "e2b:resume:sandbox": "npx tsx operate.sandbox.ts --resume", + "e2b:connect:sandbox": "npx tsx operate.sandbox.ts --connect --shell", + "e2b:list:template": "npx tsx operate.sandbox.ts --list-templates", + "e2b:delete:template": "npx tsx operate.sandbox.ts --delete-template", + "e2b:run:code": "npx tsx run.code-interpreter.ts" + }, + "dependencies": { + "e2b": "^2.7.0", + "dotenv": "^16.4.5", + "@e2b/code-interpreter": "^2.3.1" + } +} diff --git a/examples/tpl-sbx-world-js/run.code-interpreter.ts b/examples/tpl-sbx-world-js/run.code-interpreter.ts new file mode 100644 index 00000000..e17e07c8 --- /dev/null +++ b/examples/tpl-sbx-world-js/run.code-interpreter.ts @@ -0,0 +1,56 @@ +import dotenv from 'dotenv' +import path from 'node:path' +import { Sandbox, OutputMessage, Result, ExecutionError } from '@e2b/code-interpreter' + +function getArg(name: string) { + const p = `--${name}=` + const a = process.argv.slice(2).find((x) => x.startsWith(p)) + return a ? a.slice(p.length) : undefined +} + +function getMinutes(): number | undefined { + const fromArg = getArg('minutes') + if (fromArg) { + const n = Number(fromArg) + if (!Number.isNaN(n) && n > 0) return n + } + const fromEnv = process.env.SANDBOX_MINUTES + if (fromEnv) { + const n = Number(fromEnv) + if (!Number.isNaN(n) && n > 0) return n + } + return undefined +} + +async function main() { + dotenv.config({ override: true, path: path.resolve(process.cwd(), '.env') }) + if (!process.env.E2B_API_KEY) { + process.stderr.write('E2B_API_KEY not set\n') + process.exit(1) + } + const alias = getArg('alias') + if (!alias) { + process.stderr.write('alias is required. usage: --alias= [--minutes=N] [--code=]\n') + process.exit(1) + } + const minutes = getMinutes() + const timeoutMs = minutes ? minutes * 60_000 : undefined + const sandbox = await Sandbox.create(alias, timeoutMs ? { timeoutMs } : undefined) + const code = getArg('code') || 'print("hello from code interpreter")' + const exec = await sandbox.runCode(code, { + onStdout: (o: OutputMessage) => console.log('[stdout]', o.line), + onStderr: (o: OutputMessage) => console.log('[stderr]', o.line), + onResult: (r: Result) => console.log('[result]', r.text || r.html || r.markdown || r.json || ''), + onError: (e: ExecutionError) => console.error('[error]', e.name, e.value), + }) + if (exec.error) { + const e = exec.error as ExecutionError + process.stderr.write(`[exec-error] ${e.name}: ${e.value}\n`) + } + await sandbox.kill() +} + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/examples/tpl-sbx-world-js/template.ts b/examples/tpl-sbx-world-js/template.ts new file mode 100644 index 00000000..273f8395 --- /dev/null +++ b/examples/tpl-sbx-world-js/template.ts @@ -0,0 +1,23 @@ +import { Template, waitForURL } from 'e2b' + +type Mode = 'code' | 'base' + +export function createTemplate(mode: Mode, registry?: string) { + const prefix = registry ? registry.replace(/\/$/, '') + '/' : '' + + if (mode === 'code') { + return Template() + .fromImage(`${prefix}e2bdev/code-interpreter:latest`) + .setUser('user') + .setWorkdir('/home/user') + .setStartCmd('sudo /root/.jupyter/start-up.sh', waitForURL('http://localhost:49999/health')) + .runCmd('echo Hello World E2B! > hello.txt') + } + + return Template() + .fromImage(`${prefix}e2bdev/base:latest`) + .setUser('user') + .setWorkdir('/home/user') + .setStartCmd('sudo /bin/bash') + .runCmd('echo Hello World E2B! > hello.txt') +}