chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:47:58 +08:00
commit b16403ea71
789 changed files with 115226 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# Changesets
To add changeset run:
```bash
npx changeset
```
in the root of the project. This will create a new changeset in the `.changeset` folder.
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"ignore": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"privatePackages": {
"version": true,
"tag": true
}
}
+5
View File
@@ -0,0 +1,5 @@
---
"e2b": patch
---
Move the Connect/Protobuf runtime dependencies off the `2.0.0-rc.3` pre-release pin to the stable line: `@connectrpc/connect` and `@connectrpc/connect-web` upgrade to `^2.1.2`, and `@bufbuild/protobuf` upgrades from `^2.6.2` to `^2.12.1`. No public API changes — the sandbox filesystem and command RPCs continue to use the same Connect transport configuration.
+6
View File
@@ -0,0 +1,6 @@
---
"e2b": minor
"@e2b/python-sdk": minor
---
Replace the per-call `integration` connection option with a set-once, process-wide `ConnectionConfig.setIntegration()` (JS) / `ConnectionConfig.set_integration()` (Python). Integrations wrapping the SDK call it once at startup and every request is attributed via the `User-Agent` header — no more threading the option through individual SDK calls. The method is internal and hidden from docs. The `integration` option on `ConnectionConfigOpts` (JS) and the `integration` keyword argument on `ConnectionConfig` (Python) are removed; `ConnectionConfigOpts` remains as a deprecated alias of `ConnectionOpts`. In both SDKs, an explicitly provided `User-Agent` header now always takes precedence over the SDK-built one, while SDK-built values are recomputed whenever a config is rebuilt so they always reflect the current integration.
+8
View File
@@ -0,0 +1,8 @@
---
"@e2b/cli": patch
"e2b": patch
---
Switch the build tooling from `tsup` to `tsdown`. The published artifacts are unchanged: the SDK still ships `dist/index.js` (CJS), `dist/index.mjs` (ESM) and `dist/index.d.ts`/`dist/index.d.mts`, and the CLI still ships an executable `dist/index.js` with its `dist/templates`.
`engines.node` for both packages is set to `>=20.18.1 <21 || >=22` (Node 20.18.1+, or 22 and above — keeping the minimum required by `undici` while excluding the end-of-life Node 21 line).
+4
View File
@@ -0,0 +1,4 @@
root = true
[*.py]
indent_size = 4
+4
View File
@@ -0,0 +1,4 @@
packages/python-sdk/e2b/api/client/** linguist-generated=true
packages/python-sdk/e2b/envd/filesystem/** linguist-generated=true
packages/python-sdk/e2b/envd/process/** linguist-generated=true
**/*.gen.ts linguist-generated=true
+75
View File
@@ -0,0 +1,75 @@
name: Bug report
description: Create a report to help us improve
title: "[Bug]: "
labels: ["bug"]
body:
- type: input
id: sandbox_id
attributes:
label: Sandbox ID or Build ID
- type: textarea
id: environment
attributes:
label: Environment
description: Environment details (used libraries version, OS, etc..)
placeholder: e2b==1.2.3, macOS 14
validations:
required: true
- type: input
id: timestamp
attributes:
label: Timestamp of the issue
description: When did the error occur? (include timezone please)
placeholder: e.g. 2026-01-12 14:32 UTC
validations:
required: true
- type: dropdown
id: frequency
attributes:
label: Frequency
options:
- One-time occurrence
- Happens occasionally
- Happens every time
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: What did you expect to happen?
placeholder: Describe the expected behavior...
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
description: What actually happened?
placeholder: Describe what actually occurred...
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Issue reproduction
description: Steps to reproduce the issue
placeholder: |
1. Go to `...`
2. Run `...`
3. See error
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Logs, screenshots, or any other relevant information
placeholder: Add any additional context here...
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: E2B Docs
url: https://e2b.dev/docs
about: Read the E2B documentation.
- name: E2B Discord
url: https://discord.gg/dSBY3ms2Qr
about: Ask questions and get help from the E2B community.
+19
View File
@@ -0,0 +1,19 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'feature'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+41
View File
@@ -0,0 +1,41 @@
name: 'Build and install E2B CLI'
description: >-
Builds the e2b CLI from source in this repo and installs it globally so the
`e2b` command is available on PATH. Assumes the repository has already been
checked out.
runs:
using: 'composite'
steps:
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: '${{ env.TOOL_VERSION_PNPM }}'
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile
- name: Build the CLI
shell: bash
run: pnpm build
working-directory: ./packages/cli
- name: Install the CLI globally
shell: bash
run: npm install -g .
working-directory: ./packages/cli
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# This script checks if the current commit contains changesets.
set -eu
CHANGES=$(node -e "require('@changesets/read').default(process.cwd()).then(result => console.log(!!result.length))")
echo "${CHANGES}"
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
# This script checks if the specified package has changesets in the current commit.
set -eu
if [ $# -lt 1 ]; then
echo "Error: Package name is required as the first argument." >&2
exit 1
fi
PACKAGE_NAME=$1
PACKAGE_CHANGES=$(node -e "require('@changesets/read').default(process.cwd()).then(result => console.log(result.flatMap(changeset => changeset.releases.flatMap(release => release.name)).includes('${PACKAGE_NAME}')))")
echo "${PACKAGE_CHANGES}"
+72
View File
@@ -0,0 +1,72 @@
name: Test CLI
on:
workflow_call:
inputs:
E2B_DOMAIN:
required: false
type: string
default: ''
secrets:
E2B_API_KEY:
required: true
permissions:
contents: read
jobs:
test:
defaults:
run:
working-directory: ./packages/cli
shell: bash
name: CLI - Build (${{ matrix.os }})
strategy:
matrix:
os: [ubuntu-22.04, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: '${{ env.TOOL_VERSION_PNPM }}'
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
registry-url: 'https://registry.npmjs.org'
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Configure pnpm
run: |
pnpm config set auto-install-peers true
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build the SDK (pre-requisite for the tests)
run: pnpm build
working-directory: ./packages/js-sdk
- name: Build the CLI
run: pnpm build
working-directory: ./packages/cli
- name: Run tests
run: pnpm test
working-directory: ./packages/cli
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }}
+99
View File
@@ -0,0 +1,99 @@
name: Generated files
on:
pull_request:
permissions:
contents: read
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
generated: ${{ steps.filter.outputs.generated }}
steps:
- name: Filter changed paths
uses: dorny/paths-filter@v3
id: filter
with:
# `**/!(*.md)` excludes Markdown so docs-only changes don't trigger codegen checks.
filters: |
generated:
- 'spec/**/!(*.md)'
- 'codegen.Dockerfile'
- 'Makefile'
- 'packages/**/!(*.md)'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'package.json'
- '.tool-versions'
- '.github/workflows/generated_files.yml'
check-generated:
name: Generated files
needs: changes
if: ${{ needs.changes.outputs.generated == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- name: Install pnpm
uses: pnpm/action-setup@v4
id: pnpm-install
with:
version: '${{ env.TOOL_VERSION_PNPM }}'
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
registry-url: 'https://registry.npmjs.org'
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Configure pnpm
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build codegen image with caching
uses: docker/build-push-action@v6
with:
context: .
file: codegen.Dockerfile
tags: codegen-env:latest
load: true # makes the image available for `docker run`
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run codegen
run: CODEGEN_IMAGE=codegen-env:latest make codegen
- name: Check for uncommitted changes
run: |
if [[ -n $(git status --porcelain) ]]; then
echo "❌ Generated files are not up to date:"
git status --short
git diff
exit 1
else
echo "✅ No changes detected."
fi
+100
View File
@@ -0,0 +1,100 @@
name: Test JS SDK
on:
workflow_call:
inputs:
E2B_DOMAIN:
required: false
type: string
default: ''
secrets:
E2B_API_KEY:
required: true
permissions:
contents: read
jobs:
test:
defaults:
run:
working-directory: ./packages/js-sdk
shell: bash
name: JS SDK - Build and test (${{ matrix.os }})
strategy:
matrix:
os: [ubuntu-22.04, windows-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- name: Install pnpm
uses: pnpm/action-setup@v4
id: pnpm-install
with:
version: '${{ env.TOOL_VERSION_PNPM }}'
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
registry-url: 'https://registry.npmjs.org'
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Configure pnpm
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true
- name: Install dependencies
run: |
pnpm install --frozen-lockfile
- name: Get Playwright version
id: playwright-version
run: echo "version=$(node -p "require('playwright/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ${{ matrix.os == 'windows-latest' && '~/AppData/Local/ms-playwright' || '~/.cache/ms-playwright' }}
key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }}
- name: Test build
run: pnpm build
- name: Run Node tests
run: pnpm test
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }}
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Run Bun tests
run: pnpm test:bun
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }}
- name: Install Deno
uses: denoland/setup-deno@v1
with:
deno-version: v${{ env.TOOL_VERSION_DENO }}
- name: Run Deno tests
run: pnpm test:deno
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }}
+101
View File
@@ -0,0 +1,101 @@
name: Lint
permissions:
contents: read
on:
pull_request:
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- name: Filter changed paths
uses: dorny/paths-filter@v3
id: filter
with:
# `**/!(*.md)` excludes Markdown so docs-only changes don't trigger linting.
filters: |
code:
- 'packages/**/!(*.md)'
- 'spec/**/!(*.md)'
- '.oxlintrc.json'
- '.prettierrc'
- '.prettierignore'
- '.editorconfig'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'package.json'
- '.tool-versions'
- '.github/workflows/lint.yml'
lint:
name: Lint
needs: changes
if: ${{ needs.changes.outputs.code == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- uses: pnpm/action-setup@v4
with:
version: '${{ env.TOOL_VERSION_PNPM }}'
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
cache: pnpm
- name: Configure pnpm
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: '${{ env.TOOL_VERSION_UV }}'
python-version: '${{ env.TOOL_VERSION_PYTHON }}'
enable-cache: true
- name: Install Python dependencies
working-directory: packages/python-sdk
run: |
uv sync --locked
- name: Run linting
run: |
pnpm run lint
- name: Run formatting
run: |
pnpm run format
- name: Check for uncommitted changes
run: |
if [[ -n $(git status --porcelain) ]]; then
echo "❌ Files are not formatted properly:"
git status --short
git diff
exit 1
else
echo "✅ No changes detected."
fi
+142
View File
@@ -0,0 +1,142 @@
name: Package Artifacts
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
build:
name: Build Packages
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- uses: pnpm/action-setup@v4
with:
version: '${{ env.TOOL_VERSION_PNPM }}'
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
cache: pnpm
- name: Configure pnpm
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true
- name: Sanitize branch name
env:
BRANCH: ${{ github.head_ref }}
run: |
echo "BRANCH_ID=$(echo "$BRANCH" | sed 's/[^0-9A-Za-z-]/-/g')" >> "$GITHUB_ENV"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build JS SDK
working-directory: packages/js-sdk
run: pnpm run build
- name: Pack JS SDK
working-directory: packages/js-sdk
run: |
npm version prerelease --preid=${{ env.BRANCH_ID }} --no-git-tag-version
npm pack
- name: Upload JS SDK artifact
uses: actions/upload-artifact@v4
with:
name: e2b-js-sdk
path: packages/js-sdk/*.tgz
- name: Build CLI
working-directory: packages/cli
run: pnpm run build
- name: Pack CLI
working-directory: packages/cli
run: |
npm version prerelease --preid=${{ env.BRANCH_ID }} --no-git-tag-version
npm pack
- name: Upload CLI artifact
uses: actions/upload-artifact@v4
with:
name: e2b-cli
path: packages/cli/*.tgz
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: '${{ env.TOOL_VERSION_UV }}'
python-version: '${{ env.TOOL_VERSION_PYTHON }}'
enable-cache: true
- name: Build Python SDK
working-directory: packages/python-sdk
run: |
BASE_VERSION=$(uv version --short)
uv version "${BASE_VERSION}+${BRANCH_ID}"
uv build
- name: Upload Python SDK artifact
uses: actions/upload-artifact@v4
with:
name: e2b-python-sdk
path: packages/python-sdk/dist/*
- name: Comment on PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
JS_VERSION=$(node -p "require('./packages/js-sdk/package.json').version")
CLI_VERSION=$(node -p "require('./packages/cli/package.json').version")
JS_TGZ=$(ls packages/js-sdk/*.tgz | xargs -n1 basename)
CLI_TGZ=$(ls packages/cli/*.tgz | xargs -n1 basename)
PY_VERSION=$(grep '^version' packages/python-sdk/pyproject.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
PY_WHL=$(ls packages/python-sdk/dist/*.whl | xargs -n1 basename)
BODY="<!-- e2b-pkg-artifacts -->"$'\n'
BODY+="### Package Artifacts"$'\n\n'
BODY+="Built from ${GITHUB_SHA::7}. Download artifacts from [this workflow run](${RUN_URL})."$'\n\n'
BODY+="**JS SDK** (\`e2b@${JS_VERSION}\`):"$'\n'
BODY+='```sh'$'\n'
BODY+="npm install ./${JS_TGZ}"$'\n'
BODY+='```'$'\n\n'
BODY+="**CLI** (\`@e2b/cli@${CLI_VERSION}\`):"$'\n'
BODY+='```sh'$'\n'
BODY+="npm install ./${CLI_TGZ}"$'\n'
BODY+='```'$'\n\n'
BODY+="**Python SDK** (\`e2b==${PY_VERSION}\`):"$'\n'
BODY+='```sh'$'\n'
BODY+="pip install ./${PY_WHL}"$'\n'
BODY+='```'$'\n'
COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
--paginate \
--jq '.[] | select(.body | contains("<!-- e2b-pkg-artifacts -->")) | .id' \
| tail -1)
if [ -n "$COMMENT_ID" ]; then
gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
-X PATCH -f body="$BODY"
else
gh pr comment "$PR_NUMBER" --body "$BODY"
fi
+114
View File
@@ -0,0 +1,114 @@
name: Publish Candidates
on:
workflow_call:
inputs:
js-sdk:
required: false
type: boolean
default: false
python-sdk:
required: false
type: boolean
default: false
cli:
required: false
type: boolean
default: false
tag:
required: true
type: string
preid:
required: true
type: string
permissions:
contents: read
id-token: write
jobs:
publish:
name: Publish Release Candidates
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- uses: pnpm/action-setup@v4
if: ${{ inputs.js-sdk || inputs.cli }}
with:
version: '${{ env.TOOL_VERSION_PNPM }}'
- name: Setup Node.js
uses: actions/setup-node@v6
if: ${{ inputs.js-sdk || inputs.cli }}
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
registry-url: https://registry.npmjs.org
cache: pnpm
- name: Configure pnpm
if: ${{ inputs.js-sdk || inputs.cli }}
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true
- name: Update npm
if: ${{ inputs.js-sdk || inputs.cli }}
run: |
npm install -g npm@^11.6
npm --version
- name: Install uv
uses: astral-sh/setup-uv@v6
if: ${{ inputs.python-sdk }}
with:
version: '${{ env.TOOL_VERSION_UV }}'
python-version: '${{ env.TOOL_VERSION_PYTHON }}'
enable-cache: true
- name: Publish Python RC
if: ${{ inputs.python-sdk }}
working-directory: packages/python-sdk
run: |
BASE_VERSION=$(uv version --short)
uv version "${BASE_VERSION}rc${{ github.run_id }}"
uv build
uv publish --token ${PYPI_TOKEN} --check-url https://pypi.org/simple/
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
- name: Install JS dependencies
if: ${{ inputs.js-sdk || inputs.cli }}
run: pnpm install --frozen-lockfile
- name: Publish JS RC
if: ${{ inputs.js-sdk }}
working-directory: packages/js-sdk
env:
RC_PREID: ${{ inputs.preid }}
RC_TAG: ${{ inputs.tag }}
run: |
npm version prerelease --preid="${RC_PREID}.${{ github.run_id }}" --no-git-tag-version
npm publish --tag "$RC_TAG" --provenance
- name: Reinstall dependencies
if: ${{ inputs.js-sdk || inputs.cli }}
run: pnpm install --frozen-lockfile
- name: Publish CLI RC
if: ${{ inputs.cli }}
working-directory: packages/cli
env:
RC_PREID: ${{ inputs.preid }}
RC_TAG: ${{ inputs.tag }}
run: |
npm version prerelease --preid="${RC_PREID}.${{ github.run_id }}" --no-git-tag-version
npm publish --tag "$RC_TAG" --provenance
+111
View File
@@ -0,0 +1,111 @@
name: Publish Packages
on:
workflow_call:
secrets:
E2B_API_KEY:
required: true
PYPI_TOKEN:
required: true
permissions:
contents: write
id-token: write
jobs:
test:
name: Build and test SDK
runs-on: ubuntu-22.04
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.VERSION_BUMPER_APPID }}
private-key: ${{ secrets.VERSION_BUMPER_SECRET }}
- name: Checkout Repo
uses: actions/checkout@v3
with:
token: ${{ steps.app-token.outputs.token }}
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: '${{ env.TOOL_VERSION_UV }}'
python-version: '${{ env.TOOL_VERSION_PYTHON }}'
enable-cache: true
- uses: pnpm/action-setup@v4
with:
version: '${{ env.TOOL_VERSION_PNPM }}'
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
registry-url: 'https://registry.npmjs.org'
cache: pnpm
- name: Configure pnpm
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true
- name: Update npm
run: |
npm install -g npm@^11.6
npm --version
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Create new versions
run: pnpm run version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Release new versions
uses: changesets/action@v1
with:
publish: pnpm run publish
createGithubReleases: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: "" # See https://github.com/changesets/changesets/issues/1152#issuecomment-3190884868
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
- name: Update lock file
run: pnpm i --no-link --no-frozen-lockfile
- name: Commit new versions
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -am "[skip ci] Release new versions" || exit 0
# A PR merging mid-release makes this push a non-fast-forward. The
# artifacts were already published from the checked-out tree, so we
# only rebase the version bump over incoming changes that don't touch
# packages/. Otherwise the source would claim the published version
# contains code that isn't in the artifacts, so we fail loudly.
if git push; then
exit 0
fi
git fetch origin "${GITHUB_REF_NAME}"
if git diff --name-only "HEAD~1" FETCH_HEAD -- packages/ | grep -q .; then
echo "::error::'${GITHUB_REF_NAME}' advanced with changes under packages/ during the release. Refusing to rebase the version bump onto unpublished package code (the source would diverge from the published artifacts). Reconcile manually."
exit 1
fi
git pull --rebase origin "${GITHUB_REF_NAME}"
git push
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+57
View File
@@ -0,0 +1,57 @@
name: Test Python SDK
on:
workflow_call:
inputs:
E2B_DOMAIN:
required: false
type: string
default: ''
secrets:
E2B_API_KEY:
required: true
permissions:
contents: read
jobs:
test:
defaults:
run:
working-directory: ./packages/python-sdk
shell: bash
name: Python SDK - Build and test (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: '${{ env.TOOL_VERSION_UV }}'
python-version: '${{ env.TOOL_VERSION_PYTHON }}'
enable-cache: true
- name: Install dependencies
run: uv sync --locked
- name: Test build
run: uv build
- name: Run tests
run: uv run pytest --verbose --numprocesses=4
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }}
+106
View File
@@ -0,0 +1,106 @@
name: Release candidate
on:
workflow_dispatch:
inputs:
js-sdk:
description: 'Release JS SDK'
required: false
default: false
type: boolean
python-sdk:
description: 'Release Python SDK'
required: false
default: false
type: boolean
cli:
description: 'Release CLI'
required: false
default: false
type: boolean
tag:
description: 'Dist-tag (e.g. rc, beta, snapshot)'
required: false
default: 'rc'
type: string
preid:
description: 'Prerelease identifier (defaults to branch name)'
required: false
default: ''
type: string
skip-tests:
description: 'Skip tests'
required: false
default: false
type: boolean
# Shared literal group so production and candidate releases on the same ref serialize.
concurrency: release-${{ github.ref }}
permissions:
id-token: write
contents: write
jobs:
rc-validate:
name: Validate RC inputs
runs-on: ubuntu-latest
outputs:
preid: ${{ steps.preid.outputs.preid }}
tag: ${{ steps.tag.outputs.tag }}
steps:
- name: Sanitize tag
id: tag
env:
RAW_TAG: ${{ github.event.inputs.tag }}
run: |
SAFE_TAG="$(echo "$RAW_TAG" | sed 's/[^0-9A-Za-z-]/-/g')"
echo "tag=$SAFE_TAG" >> "$GITHUB_OUTPUT"
- name: Block production tags
run: |
if [ "${{ steps.tag.outputs.tag }}" = "latest" ]; then
echo "::error::Publishing with the 'latest' tag is not allowed for candidates. Use the 'Release' workflow instead."
exit 1
fi
- name: Sanitize preid
id: preid
env:
RAW_PREID: ${{ github.event.inputs.preid || github.ref_name }}
run: |
echo "preid=$(echo "$RAW_PREID" | sed 's/[^0-9A-Za-z-]/-/g')" >> "$GITHUB_OUTPUT"
rc-python-tests:
name: RC Python Tests
needs: [rc-validate]
if: github.event.inputs.python-sdk == 'true' && github.event.inputs.skip-tests != 'true'
uses: ./.github/workflows/python_sdk_tests.yml
secrets: inherit
rc-js-tests:
name: RC JS Tests
needs: [rc-validate]
if: github.event.inputs.js-sdk == 'true' && github.event.inputs.skip-tests != 'true'
uses: ./.github/workflows/js_sdk_tests.yml
secrets: inherit
rc-cli-tests:
name: RC CLI Tests
needs: [rc-validate]
if: github.event.inputs.cli == 'true' && github.event.inputs.skip-tests != 'true'
uses: ./.github/workflows/cli_tests.yml
secrets: inherit
rc-publish:
name: Publish RC
needs: [rc-validate, rc-python-tests, rc-js-tests, rc-cli-tests]
if: (!cancelled()) && !contains(needs.*.result, 'failure') && needs.rc-validate.result == 'success'
uses: ./.github/workflows/publish_candidates.yml
with:
js-sdk: ${{ github.event.inputs.js-sdk == 'true' }}
python-sdk: ${{ github.event.inputs.python-sdk == 'true' }}
cli: ${{ github.event.inputs.cli == 'true' }}
tag: ${{ needs.rc-validate.outputs.tag }}
preid: ${{ needs.rc-validate.outputs.preid }}
secrets: inherit
+182
View File
@@ -0,0 +1,182 @@
name: Release
on:
workflow_dispatch: {}
# Shared literal group so production and candidate releases on the same ref serialize.
concurrency: release-${{ github.ref }}
permissions:
id-token: write
contents: write
jobs:
preflight:
name: Release preflight
runs-on: ubuntu-latest
outputs:
release: ${{ steps.version.outputs.release }}
js-sdk: ${{ steps.js.outputs.release }}
python-sdk: ${{ steps.python.outputs.release }}
cli: ${{ steps.cli.outputs.release }}
itinerary: ${{ steps.itinerary.outputs.itinerary }}
steps:
- name: Checkout Repo
uses: actions/checkout@v3
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- name: Install pnpm
uses: pnpm/action-setup@v4
id: pnpm-install
with:
version: '${{ env.TOOL_VERSION_PNPM }}'
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
registry-url: 'https://registry.npmjs.org'
cache: pnpm
cache-dependency-path: pnpm-lock.yaml
- name: Configure pnpm
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check if new version
id: version
run: |
IS_RELEASE=$(./.github/scripts/is_release.sh)
echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT"
- name: Check JavaScript SDK Release
id: js
if: steps.version.outputs.release == 'true'
run: |
IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "e2b")
echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT"
- name: Check Python SDK Release
id: python
if: steps.version.outputs.release == 'true'
run: |
IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/python-sdk")
echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT"
- name: Check CLI Release
id: cli
if: steps.version.outputs.release == 'true'
run: |
IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/cli")
echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT"
- name: Build release itinerary
id: itinerary
if: steps.version.outputs.release == 'true'
run: |
pnpm changeset status --output=.cs-status.json
ITINERARY=$(node -e '
const data = require("./.cs-status.json");
const labels = { "e2b": "JS SDK (e2b)", "@e2b/python-sdk": "Python SDK (e2b)", "@e2b/cli": "CLI (@e2b/cli)" };
const order = ["e2b", "@e2b/python-sdk", "@e2b/cli"];
const byName = Object.fromEntries(data.releases.map(r => [r.name, r]));
const lines = order.filter(n => byName[n]).map(n => `• ${labels[n]} v${byName[n].newVersion}`);
process.stdout.write(lines.join("\n") || "• No packages were published");
')
rm -f .cs-status.json
{
echo "itinerary<<EOF"
echo "$ITINERARY"
echo "EOF"
} >> "$GITHUB_OUTPUT"
python-tests:
name: Python SDK Tests
needs: [preflight]
if: needs.preflight.outputs.python-sdk == 'true'
uses: ./.github/workflows/python_sdk_tests.yml
secrets: inherit
js-tests:
name: JS SDK Tests
needs: [preflight]
if: needs.preflight.outputs.js-sdk == 'true'
uses: ./.github/workflows/js_sdk_tests.yml
secrets: inherit
cli-tests:
name: CLI Tests
needs: [preflight]
if: needs.preflight.outputs.cli == 'true'
uses: ./.github/workflows/cli_tests.yml
secrets: inherit
publish:
name: Publish
needs: [preflight, python-tests, js-tests, cli-tests]
if: (!cancelled()) && !contains(needs.*.result, 'failure') && needs.preflight.outputs.release == 'true'
uses: ./.github/workflows/publish_packages.yml
secrets: inherit
report-start:
needs: [preflight]
if: needs.preflight.outputs.release == 'true'
name: Release Started - Slack Notification
runs-on: ubuntu-latest
steps:
- name: Release Started - Slack Notification
uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0
env:
SLACK_COLOR: '#3aa3e3'
SLACK_MESSAGE: |
:rocket: A new release has been triggered :hourglass_flowing_sand:
*Releasing:*
${{ needs.preflight.outputs.itinerary }}
SLACK_TITLE: Release Started
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: 'monitoring-releases'
report-failure:
needs: [python-tests, js-tests, cli-tests, publish]
if: failure()
name: Release Failed - Slack Notification
runs-on: ubuntu-latest
steps:
- name: Release Failed - Slack Notification
uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0
env:
SLACK_COLOR: '#ff0000'
SLACK_MESSAGE: ':here-we-go-again: :bob-the-destroyer: We need :fix-parrot: ASAP :pray:'
SLACK_TITLE: Release Failed
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: 'monitoring-releases'
report-success:
needs: [preflight, publish]
if: needs.publish.result == 'success'
name: Release Succeeded - Slack Notification
runs-on: ubuntu-latest
steps:
- name: Release Succeeded - Slack Notification
uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0
env:
SLACK_COLOR: '#36a64f'
SLACK_MESSAGE: |
:tada: A new version has been released successfully! :ship-it-parrot:
*Released:*
${{ needs.preflight.outputs.itinerary }}
SLACK_TITLE: Release Succeeded
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: 'monitoring-releases'
+133
View File
@@ -0,0 +1,133 @@
name: SDK Tests
on:
pull_request:
branches:
- main
workflow_dispatch:
permissions:
contents: read
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
js: ${{ steps.filter.outputs.js }}
python: ${{ steps.filter.outputs.python }}
cli: ${{ steps.filter.outputs.cli }}
steps:
- name: Filter changed paths
# On workflow_dispatch there is no PR diff and paths-filter would fall
# back to git (failing without a checkout); the job-level `if`s below
# force a full run for that event instead, so we only filter on pull_request.
if: github.event_name == 'pull_request'
uses: dorny/paths-filter@v3
id: filter
with:
# `**/!(*.md)` excludes Markdown so docs-only changes don't trigger the suites.
filters: |
shared: &shared
- '.github/workflows/sdk_tests.yml'
- 'spec/**/!(*.md)'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'package.json'
- '.tool-versions'
js:
- *shared
- '.github/workflows/js_sdk_tests.yml'
- 'packages/js-sdk/**/!(*.md)'
python:
- *shared
- '.github/workflows/python_sdk_tests.yml'
- 'packages/python-sdk/**/!(*.md)'
- 'packages/connect-python/**/!(*.md)'
cli:
- *shared
- '.github/workflows/cli_tests.yml'
- 'packages/cli/**/!(*.md)'
- 'packages/js-sdk/**/!(*.md)'
js-tests:
name: Production / JS SDK Tests
needs: changes
if: ${{ needs.changes.outputs.js == 'true' || github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/js_sdk_tests.yml
secrets:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
python-tests:
name: Production / Python SDK Tests
needs: changes
if: ${{ needs.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/python_sdk_tests.yml
secrets:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
cli-tests:
name: Production / CLI Tests
needs: changes
if: ${{ needs.changes.outputs.cli == 'true' || github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/cli_tests.yml
secrets:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
js-tests-staging:
name: Staging / JS SDK Tests
needs: changes
if: ${{ needs.changes.outputs.js == 'true' || github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/js_sdk_tests.yml
with:
E2B_DOMAIN: ${{ vars.E2B_DOMAIN_STAGING }}
secrets:
E2B_API_KEY: ${{ secrets.E2B_API_KEY_STAGING }}
python-tests-staging:
name: Staging / Python SDK Tests
needs: changes
if: ${{ needs.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/python_sdk_tests.yml
with:
E2B_DOMAIN: ${{ vars.E2B_DOMAIN_STAGING }}
secrets:
E2B_API_KEY: ${{ secrets.E2B_API_KEY_STAGING }}
cli-tests-staging:
name: Staging / CLI Tests
needs: changes
if: ${{ needs.changes.outputs.cli == 'true' || github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/cli_tests.yml
with:
E2B_DOMAIN: ${{ vars.E2B_DOMAIN_STAGING }}
secrets:
E2B_API_KEY: ${{ secrets.E2B_API_KEY_STAGING }}
status:
name: SDK Tests Status
# `changes` is included so a failure in change-detection (which would skip the
# suites) surfaces as a failed required check instead of a false green.
needs:
- changes
- js-tests
- python-tests
- cli-tests
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- name: Report aggregate result
run: |
echo "Job results: ${{ join(needs.*.result, ', ') }}"
if ${{ contains(needs.*.result, 'failure') }}; then
echo "::error::Change detection or a Production SDK test suite failed."
exit 1
fi
if ${{ contains(needs.*.result, 'cancelled') }}; then
echo "::error::Change detection or a Production SDK test suite was cancelled; tests were not verified — re-run before merging."
exit 1
fi
echo "All Production SDK test suites passed or were skipped."
+52
View File
@@ -0,0 +1,52 @@
name: Build and push prepared templates
on:
workflow_dispatch:
permissions:
contents: read
jobs:
buildAndPushImage:
defaults:
run:
working-directory: ./templates/base
name: Build and Push Image to DockerHub
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
run: |
docker buildx build \
--file e2b.Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--tag ${{ secrets.DOCKERHUB_USERNAME }}/base:latest .
buildTemplate:
name: Build and Publish E2B Template
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Build and install E2B CLI
uses: ./.github/actions/build-cli
- name: Build and publish base template
working-directory: ./templates/base
env:
E2B_API_KEY: ${{ secrets.E2B_API_KEY }}
run: e2b template create base --memory-mb 512
+82
View File
@@ -0,0 +1,82 @@
name: Typecheck
on:
pull_request:
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- name: Filter changed paths
uses: dorny/paths-filter@v3
id: filter
with:
# `**/!(*.md)` excludes Markdown so docs-only changes don't trigger typecheck.
filters: |
code:
- 'packages/**/!(*.md)'
- 'spec/**/!(*.md)'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- 'package.json'
- '.tool-versions'
- '.github/workflows/typecheck.yml'
typecheck:
name: Typecheck
needs: changes
if: ${{ needs.changes.outputs.code == 'true' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Parse .tool-versions
uses: wistia/parse-tool-versions@v2.1.1
with:
filename: '.tool-versions'
uppercase: 'true'
prefix: 'tool_version_'
- uses: pnpm/action-setup@v4
with:
version: '${{ env.TOOL_VERSION_PNPM }}'
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '${{ env.TOOL_VERSION_NODEJS }}'
cache: pnpm
- name: Configure pnpm
run: |
pnpm config set auto-install-peers true
pnpm config set exclude-links-from-lockfile true
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: '${{ env.TOOL_VERSION_UV }}'
python-version: '${{ env.TOOL_VERSION_PYTHON }}'
enable-cache: true
- name: Install Python dependencies
working-directory: packages/python-sdk
run: |
uv sync --locked
- name: Run typecheck
run: |
pnpm run typecheck
+297
View File
@@ -0,0 +1,297 @@
vale-styles
.vercel
.vscode
.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.*
!.env.template
!.env.example
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# SDK reference artifacts
sdk_ref/
sdkRefRoutes.json
# SDK Client generated spec
spec/openapi_generated.yml
+6
View File
@@ -0,0 +1,6 @@
enable-pre-post-scripts=true
auto-install-peers=true
exclude-links-from-lockfile=true
prefer-workspace-packages=false
link-workspace-packages=false
engine-strict=true
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json",
"ignorePatterns": ["**/dist/**", "**/node_modules/**", "**/*.gen.ts"],
"rules": {
"no-unused-vars": ["error", { "caughtErrors": "none" }],
"typescript/no-explicit-any": "off",
"typescript/ban-ts-comment": "off",
"unicorn/no-useless-fallback-in-spread": "off"
}
}
+8
View File
@@ -0,0 +1,8 @@
/.next/
# imporatant to keep // $HighlightLine comments at the end of the line
apps/web/src/code/
**/*.mdx
**/code/**/*
+5
View File
@@ -0,0 +1,5 @@
{
"singleQuote": true,
"semi": false,
"trailingComma": "es5"
}
+5
View File
@@ -0,0 +1,5 @@
deno 1.46.3
nodejs 22.18.0
pnpm 9.15.5
python 3.10
uv 0.10.0
+8
View File
@@ -0,0 +1,8 @@
StylesPath = vale-styles
MinAlertLevel = suggestion
Packages = Microsoft, proselint, write-good, alex, Readability, Joblint, Google
[*.{md,ts,py}]
BasedOnStyles = Vale, Microsoft, proselint, write-good, alex, Readability, Joblint, Google
Symlink
+1
View File
@@ -0,0 +1 @@
CLAUDE.md
+9
View File
@@ -0,0 +1,9 @@
Use pnpm for node and uv for python to install and update dependencies.
Run `pnpm run format`, `pnpm run lint` and `pnpm run typecheck` before committing changes.
When modifying the SDK packages, ensure equivalent changes are applied to both JS as well as sync and async Python implementations.
To re-generate the API client run `make codegen` in the repository root when modifying spec/.
Create or update tests covering affected codepaths and run them using `pnpm run test`.
Generate a changeset when updating packages/cli, packages/js-sdk, packages/python-sdk with `pnpm changeset` in the repository root.
When creating a pull request, add usage examples for user-facing changes to the PR description.
Keep PR descriptions up-to-date with changes.
Default credentials are stored in .env.local in the repository root or inside ~/.e2b/config.json.
+7
View File
@@ -0,0 +1,7 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence.
* @jakubno @ValentaTomas
# SDKs
packages @jakubno @ValentaTomas @mishushakov
packages/connect-python @ValentaTomas
+2
View File
@@ -0,0 +1,2 @@
# Contributing
If you want to contribute, open a PR, issue, or start a discussion on our [Discord](https://discord.gg/dSBY3ms2Qr).
+3
View File
@@ -0,0 +1,3 @@
# Releasing e2b cli
to create a changeset run `pnpm run changeset`
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2023 FoundryLabs, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+19
View File
@@ -0,0 +1,19 @@
.PHONY: codegen
codegen:
@echo "Building codegen image"
docker build -q -t codegen-env . -f codegen.Dockerfile
@echo "Generating code"
docker run -v $$PWD:/workspace codegen-env make generate
generate: generate-js generate-python
generate-js:
cd packages/js-sdk && pnpm generate
generate-python:
cd packages/python-sdk && make generate
.PHONY: init-styles
init-styles:
vale sync
+95
View File
@@ -0,0 +1,95 @@
<p align="center">
<img src="/readme-assets/logo-black.png#gh-light-mode-only" alt="E2B Logo" width="200">
<img src="/readme-assets/logo-white.png#gh-dark-mode-only" alt="E2B Logo" width="200">
</p>
<h4 align="center">
<a href="https://pypi.org/project/e2b/">
<img alt="Last 1 month downloads for the Python SDK" loading="lazy" decoding="async" style="color:transparent;width:170px;height:18px" src="https://static.pepy.tech/personalized-badge/e2b?period=monthly&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=GREEN&left_text=PyPi%20Monthly%20Downloads">
</a>
<a href="https://www.npmjs.com/package/e2b">
<img alt="Last 1 month downloads for the JavaScript SDK" loading="lazy" width="200" height="30" decoding="async" data-nimg="1"
style="color:transparent;width:170px;height:18px" src="https://img.shields.io/npm/dm/e2b?label=NPM%20Monthly%20Downloads">
</a>
</h4>
<!---
<img width="100%" src="/readme-assets/preview.png" alt="Cover image">
--->
## What is E2B?
[E2B](https://e2b.dev/?utm_source=github&utm_medium=referral&utm_campaign=readme&utm_content=E2B) is an open-source infrastructure that allows you to run AI-generated code in secure isolated sandboxes in the cloud. To start and control sandboxes, use our [JavaScript SDK](https://www.npmjs.com/package/e2b) or [Python SDK](https://pypi.org/project/e2b).
## Run your first Sandbox
### 1. Install SDK
JavaScript / TypeScript
```
npm i e2b
```
Python
```
pip install e2b
```
### 2. Get your E2B API key
1. Sign up to E2B [here](https://e2b.dev/?utm_source=github&utm_medium=referral&utm_campaign=readme&utm_content=E2B).
2. Get your API key [here](https://e2b.dev/dashboard?tab=keys&utm_source=github&utm_medium=referral&utm_campaign=readme&utm_content=E2B).
3. Set environment variable with your API key
```
E2B_API_KEY=e2b_***
```
### 3. Start a sandbox and run commands
JavaScript / TypeScript
```ts
import Sandbox from 'e2b'
const sandbox = await Sandbox.create()
const result = await sandbox.commands.run('echo "Hello from E2B!"')
console.log(result.stdout) // Hello from E2B!
```
Python
```py
from e2b import Sandbox
with Sandbox.create() as sandbox:
result = sandbox.commands.run('echo "Hello from E2B!"')
print(result.stdout) # Hello from E2B!
```
### 4. Code execution with Code Interpreter
If you need to execute code with [`runCode()`](https://e2b.dev/docs/code-interpreting?utm_source=github&utm_medium=referral&utm_campaign=readme&utm_content=E2B)/[`run_code()`](https://e2b.dev/docs/code-interpreting?utm_source=github&utm_medium=referral&utm_campaign=readme&utm_content=E2B), install the [Code Interpreter SDK](https://github.com/e2b-dev/code-interpreter):
```
npm i @e2b/code-interpreter # JavaScript/TypeScript
pip install e2b-code-interpreter # Python
```
```ts
import { Sandbox } from '@e2b/code-interpreter'
const sandbox = await Sandbox.create()
const execution = await sandbox.runCode('x = 1; x += 1; x')
console.log(execution.text) // outputs 2
```
### 5. Check docs
Visit [E2B documentation](https://e2b.dev/docs?utm_source=github&utm_medium=referral&utm_campaign=readme&utm_content=E2B).
### 6. E2B cookbook
Visit our [Cookbook](https://github.com/e2b-dev/e2b-cookbook/tree/main) to get inspired by examples with different LLMs and AI frameworks.
## Self-hosting
Read the [self-hosting guide](https://github.com/e2b-dev/infra/blob/main/self-host.md) to learn how to set up the [E2B infrastructure](https://github.com/e2b-dev/infra) on your own. The infrastructure is deployed using Terraform.
Supported cloud providers:
- 🟢 AWS
- 🟢 Google Cloud (GCP)
- [ ] Azure
- [ ] General Linux machine
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`e2b-dev/E2B`
- 原始仓库:https://github.com/e2b-dev/E2B
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+56
View File
@@ -0,0 +1,56 @@
FROM golang:1.23
# Install Golang deps
RUN go install github.com/bufbuild/buf/cmd/buf@v1.50.1 && \
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1 && \
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@v1.18.1
# Install our custom protoc plugin, connect-python
COPY ./packages/connect-python /packages/connect-python
RUN cd /packages/connect-python && make bin/protoc-gen-connect-python
FROM python:3.10
# Set working directory
WORKDIR /workspace
ENV PROTOC_VERSION=26.1
RUN ARCH=$(uname -m) && \
case "$ARCH" in \
x86_64) PROTOC_ARCH="x86_64" ;; \
arm64|aarch64) PROTOC_ARCH="aarch_64" ;; \
*) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
esac && \
curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip && \
unzip -o protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip -d /usr/local && \
rm protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip
# Copy installed Go deps from previous build step
COPY --from=0 /go /go
# Add Go binary to PATH
ENV PATH="/go/bin:${PATH}"
# Install Python deps (e2b-openapi-python-client is patched version to fix issue with explode)
# https://github.com/openapi-generators/openapi-python-client/pull/1296
RUN pip install black==26.3.1 pyyaml==6.0.2 e2b-openapi-python-client==0.26.2 datamodel-code-generator==0.34.0
# Install Node.js (pinned to match .tool-versions)
ENV NODE_VERSION=22.18.0
RUN ARCH=$(uname -m) && \
case "$ARCH" in \
x86_64) NODE_ARCH="x64" ;; \
arm64|aarch64) NODE_ARCH="arm64" ;; \
*) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
esac && \
curl -fsSL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${NODE_ARCH}.tar.xz | tar -xJ -C /usr/local --strip-components=1
# Install Node.js deps
ENV PNPM_VERSION=9.15.5
RUN npm install -g \
pnpm@${PNPM_VERSION} \
@connectrpc/protoc-gen-connect-es@1.6.1 \
@bufbuild/protoc-gen-es@2.6.2
CMD ["make", "generate"]
+48
View File
@@ -0,0 +1,48 @@
{
"name": "e2b",
"private": true,
"scripts": {
"version": "pnpm changeset version && pnpm run -r postVersion",
"publish": "pnpm changeset publish && pnpm run -r postPublish",
"test": "pnpm test --recursive --if-present",
"rm-node-modules": "find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
"pnpm-install-hack": "cd packages/js-sdk && sed -i '' 's/\"version\": \".*\"/\"version\": \"9.9.9\"/g' package.json && cd ../.. && pnpm i && git checkout -- packages/js-sdk/package.json",
"lint": "pnpm --if-present --recursive run lint",
"typecheck": "pnpm --if-present --recursive run typecheck",
"format": "pnpm --if-present --recursive run format",
"changeset": "pnpm dlx @changesets/cli"
},
"packageManager": "pnpm@9.15.5",
"dependencies": {
"@changesets/read": "^0.6.2"
},
"devDependencies": {
"changeset": "^0.2.6",
"oxlint": "^1.72.0"
},
"engines": {
"pnpm": ">=9.0.0 <10"
},
"pnpm": {
"overrides": {
"@next/eslint-plugin-next>glob@*": "10.5.0",
"rollup@>=4": ">=4.59.0",
"postcss@<8.5.10": "^8.5.10",
"vite@>=6.0.0 <6.4.2": "^6.4.2",
"lodash@<4.18.0": "^4.18.0",
"brace-expansion@>=2.0.0 <2.0.3": "^2.0.3",
"picomatch@<2.3.2": "^2.3.2",
"picomatch@>=4.0.0 <4.0.4": "^4.0.4",
"yaml@>=2.0.0 <2.8.3": "^2.8.3",
"@tootallnate/once@<3.0.1": "^3.0.1",
"smol-toml@<1.6.1": "^1.6.1",
"flatted@<3.4.2": "^3.4.2",
"minimatch@<3.1.3": "^3.1.3",
"minimatch@>=5.0.0 <5.1.8": "^5.1.8",
"minimatch@>=9.0.0 <9.0.7": "^9.0.7",
"minimatch@>=10.0.0 <10.2.3": "^10.2.3",
"ws@>=8.0.0 <8.20.1": "^8.20.1",
"shell-quote@<1.8.4": "^1.8.4"
}
}
}
+3
View File
@@ -0,0 +1,3 @@
source_up
dotenv ../../../.env.local
+45
View File
@@ -0,0 +1,45 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
tests/temp
coverage
/.pnp
.pnp.js
# testing
/coverage
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# generate output
dist
# compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
/test.md
logs.txt
/dist
/files
+2
View File
@@ -0,0 +1,2 @@
*.hbs
tests/**/fixtures/**/*
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) 2025 FOUNDRYLABS, INC.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+44
View File
@@ -0,0 +1,44 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/e2b-dev/E2B/refs/heads/main/readme-assets/logo-white.png">
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/e2b-dev/E2B/refs/heads/main/readme-assets/logo-black.png">
<img alt="E2B Logo" src="https://raw.githubusercontent.com/e2b-dev/E2B/refs/heads/main/readme-assets/logo-black.png" width="200">
</picture>
</p>
# E2B CLI
This CLI tool allows you to build manager your running E2B sandbox and sandbox templates. Learn more in [our documentation](https://e2b.dev/docs).
### 1. Install the CLI
**Using Homebrew (on macOS)**
```bash
brew install e2b
```
**Using NPM**
```bash
npm install -g @e2b/cli
```
### 2. Authenticate
```bash
e2b auth login
```
> [!NOTE]
> To authenticate without the ability to open the browser, provide
> `E2B_ACCESS_TOKEN` as an environment variable. You can find your token
> in Account Settings under the Team selector at [e2b.dev/dashboard](https://e2b.dev/dashboard). Then use the CLI like this:
> `E2B_ACCESS_TOKEN=sk_e2b_... e2b template create`.
> [!IMPORTANT]
> Note the distinction between `E2B_ACCESS_TOKEN` and `E2B_API_KEY`.
### 3. Check out docs
Visit our [CLI documentation](https://e2b.dev/docs) to learn more.
+94
View File
@@ -0,0 +1,94 @@
{
"name": "@e2b/cli",
"version": "2.13.1",
"description": "CLI for managing e2b sandbox templates",
"homepage": "https://e2b.dev",
"license": "MIT",
"author": {
"name": "FoundryLabs, Inc.",
"email": "hello@e2b.dev",
"url": "https://e2b.dev"
},
"bugs": "https://github.com/e2b-dev/e2b/issues",
"repository": {
"type": "git",
"url": "https://github.com/e2b-dev/e2b",
"directory": "packages/cli"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"e2b",
"ai-agents",
"agents",
"ai",
"code-interpreter",
"sandbox",
"code",
"cli",
"runtime",
"vm",
"nodejs",
"javascript",
"typescript"
],
"sideEffects": false,
"scripts": {
"prepublishOnly": "pnpm build",
"build": "tsc --noEmit --skipLibCheck && tsdown --minify",
"dev": "tsdown --watch",
"typecheck": "tsc --noEmit --skipLibCheck",
"lint": "oxlint --config ../../.oxlintrc.json src",
"format": "prettier --write src",
"test:interactive": "pnpm build && ./dist/index.js",
"test": "vitest run",
"test:watch": "vitest watch",
"test:coverage": "vitest run --coverage",
"check-deps": "knip"
},
"devDependencies": {
"@types/handlebars": "^4.1.0",
"@types/inquirer": "^9.0.7",
"@types/json2md": "^1.5.4",
"@types/node": "^20.19.19",
"@types/npmcli__package-json": "^4.0.4",
"@types/statuses": "^2.0.5",
"@typescript/native": "npm:typescript@^7.0.2",
"@vitest/coverage-v8": "^4.1.0",
"json2md": "^2.0.1",
"knip": "^5.43.6",
"tsdown": "^0.22.3",
"typescript": "npm:@typescript/typescript6@^6.0.2",
"vitest": "^4.1.8"
},
"files": [
"dist",
"LICENSE",
"README",
"package.json"
],
"bin": {
"e2b": "dist/index.js"
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@inquirer/prompts": "^7.9.0",
"@npmcli/package-json": "^5.2.1",
"async-listen": "^3.0.1",
"boxen": "^7.1.1",
"chalk": "^5.3.0",
"cli-highlight": "^2.1.11",
"commander": "^11.1.0",
"console-table-printer": "^2.11.2",
"e2b": "^2.32.0",
"handlebars": "^4.7.9",
"inquirer": "^12.10.0",
"simple-update-notifier": "^2.0.0",
"statuses": "^2.0.1",
"yup": "^1.3.2"
},
"engines": {
"node": ">=20.18.1 <21 || >=22"
}
}
+114
View File
@@ -0,0 +1,114 @@
import * as boxen from 'boxen'
import * as e2b from 'e2b'
import { getUserConfig, UserConfig } from './user'
import { asBold, asPrimary } from './utils/format'
export type Teams =
e2b.paths['/teams']['get']['responses'][200]['content']['application/json']
export let apiKey = process.env.E2B_API_KEY
export let accessToken = process.env.E2B_ACCESS_TOKEN
export const teamId = process.env.E2B_TEAM_ID
const authErrorBox = (keyName: 'E2B_API_KEY' | 'E2B_ACCESS_TOKEN') => {
const link =
keyName === 'E2B_API_KEY'
? 'https://e2b.dev/dashboard?tab=keys'
: 'https://e2b.dev/dashboard?tab=personal'
const msg = keyName === 'E2B_API_KEY' ? 'API key' : 'access token'
const body = `You must be logged in to use this command. Run ${asBold(
'e2b auth login'
)}.
If you are seeing this message in CI/CD you may need to set the ${asBold(
keyName
)} environment variable.
Visit ${asPrimary(link)} to get the ${msg}.`
return boxen.default(body, {
width: 70,
float: 'center',
padding: 0.5,
margin: 1,
borderStyle: 'round',
borderColor: 'redBright',
})
}
export function ensureAPIKey() {
// If apiKey is not already set (either from env var or from user config), try to get it from config file
if (!apiKey) {
const userConfig = getUserConfig()
apiKey = userConfig?.teamApiKey
}
if (!apiKey) {
console.error(authErrorBox('E2B_API_KEY'))
process.exit(1)
} else {
return apiKey
}
}
export function ensureUserConfig(): UserConfig {
const userConfig = getUserConfig()
if (!userConfig) {
console.error('No user config found, run `e2b auth login` to log in first.')
process.exit(1)
}
return userConfig
}
export function ensureAccessToken() {
// If accessToken is not already set (either from env var or from user config), try to get it from config file
if (!accessToken) {
const userConfig = getUserConfig()
accessToken = userConfig?.tokens.access_token
}
if (!accessToken) {
console.error(authErrorBox('E2B_ACCESS_TOKEN'))
process.exit(1)
} else {
return accessToken
}
}
/**
* Resolve team ID with proper precedence:
* 1. CLI --team flag
* 2. E2B_TEAM_ID env var
* 3. Local e2b.toml team_id (if provided)
* 4. ~/.e2b/config.json teamId (only if E2B_API_KEY env var is NOT set,
* to avoid mismatch between env var API key and config file team ID)
*/
export function resolveTeamId(
cliTeamId?: string,
localConfigTeamId?: string
): string | undefined {
if (cliTeamId) return cliTeamId
if (teamId) return teamId
if (localConfigTeamId) return localConfigTeamId
if (!process.env.E2B_API_KEY) {
const config = getUserConfig()
return config?.teamId
}
return undefined
}
const userConfig = getUserConfig()
const resolvedAccessToken =
process.env.E2B_ACCESS_TOKEN || userConfig?.tokens.access_token
export const connectionConfig = new e2b.ConnectionConfig({
apiKey: process.env.E2B_API_KEY || userConfig?.teamApiKey,
apiHeaders: resolvedAccessToken
? { Authorization: `Bearer ${resolvedAccessToken}` }
: undefined,
})
// The CLI authenticates team-scoped endpoints (e.g. `/teams`) with the access
// token instead of an API key, so don't require an API key here.
export const client = new e2b.ApiClient(connectionConfig, {
requireApiKey: false,
})
@@ -0,0 +1,68 @@
import * as commander from 'commander'
import * as fs from 'fs'
import * as chalk from 'chalk'
import * as e2b from 'e2b'
import {
USER_CONFIG_PATH,
getConfigRefreshTimestamp,
writeUserConfig,
} from 'src/user'
import { ensureUserConfig, Teams } from 'src/api'
import { ensureValidAccessToken } from 'src/utils/token-refresh'
import { asBold, asFormattedTeam } from '../../utils/format'
import { handleE2BRequestError } from '../../utils/errors'
export const configureCommand = new commander.Command('configure')
.description('configure user')
.action(async () => {
const inquirer = await import('inquirer')
console.log('Configuring user...\n')
if (!fs.existsSync(USER_CONFIG_PATH)) {
console.log('No user config found, run `e2b auth login` to log in first.')
return
}
// ensureValidAccessToken may refresh tokens and write them to disk.
// Re-read the config afterwards so we persist the refreshed tokens
// instead of overwriting them with stale in-memory copies.
const accessToken = await ensureValidAccessToken()
const userConfig = ensureUserConfig()
const config = new e2b.ConnectionConfig({
apiHeaders: { Authorization: `Bearer ${accessToken}` },
})
const authClient = new e2b.ApiClient(config, { requireApiKey: false })
const res = await authClient.api.GET('/teams', {
signal: config.getSignal(),
})
handleE2BRequestError(res, 'Error getting teams')
const teams = res.data as Teams
const team = (
await inquirer.default.prompt([
{
name: 'team',
message: chalk.default.underline('Select team'),
type: 'list',
pageSize: 50,
choices: teams.map((team) => ({
name: asFormattedTeam(team, userConfig.teamId),
value: team,
})),
},
])
)['team']
userConfig.teamName = team.name
userConfig.teamId = team.teamID
userConfig.teamApiKey = team.apiKey
userConfig.last_refresh = getConfigRefreshTimestamp()
writeUserConfig(USER_CONFIG_PATH, userConfig)
console.log(`Team ${asBold(team.name)} (${team.teamID}) selected.\n`)
})
+12
View File
@@ -0,0 +1,12 @@
import * as commander from 'commander'
import { loginCommand } from './login'
import { logoutCommand } from './logout'
import { infoCommand } from './info'
import { configureCommand } from './configure'
export const authCommand = new commander.Command('auth')
.description('authentication commands')
.addCommand(loginCommand)
.addCommand(logoutCommand)
.addCommand(infoCommand)
.addCommand(configureCommand)
+24
View File
@@ -0,0 +1,24 @@
import * as commander from 'commander'
import { getUserConfig } from 'src/user'
import { asFormattedConfig, asFormattedError } from 'src/utils/format'
export const infoCommand = new commander.Command('info')
.description('get information about the current user')
.action(async () => {
let userConfig
try {
userConfig = getUserConfig()
} catch (err) {
console.error(asFormattedError('Failed to read user config', err))
}
if (!userConfig) {
console.log('Not logged in')
return
}
console.log(asFormattedConfig(userConfig))
process.exit(0)
})
+169
View File
@@ -0,0 +1,169 @@
import * as listen from 'async-listen'
import * as commander from 'commander'
import * as http from 'http'
import * as e2b from 'e2b'
import { pkg } from 'src'
import {
DOCS_BASE,
getConfigRefreshTimestamp,
getUserConfig,
writeUserConfig,
USER_CONFIG_PATH,
UserConfig,
} from 'src/user'
import { asBold, asFormattedConfig, asFormattedError } from 'src/utils/format'
import { openUrlInBrowser } from 'src/utils/openBrowser'
import { connectionConfig, Teams } from 'src/api'
import { handleE2BRequestError } from '../../utils/errors'
export const loginCommand = new commander.Command('login')
.description('log in to CLI')
.action(async () => {
let userConfig: UserConfig | null = null
try {
userConfig = getUserConfig()
} catch (err) {
console.error(asFormattedError('Failed to read user config', err))
}
if (userConfig) {
console.log(
`\nAlready logged in. ${asFormattedConfig(
userConfig
)}.\n\nIf you want to log in as a different user, log out first by running 'e2b auth logout'.\nTo change the team, run 'e2b auth configure'.\n`
)
return
} else if (!userConfig) {
console.log('Attempting to log in...')
const signInResponse = await signInWithBrowser()
if (!signInResponse) {
console.info('Login aborted')
return
}
const accessToken = signInResponse.accessToken
const signal = connectionConfig.getSignal()
const config = new e2b.ConnectionConfig({
apiHeaders: { Authorization: `Bearer ${accessToken}` },
})
const client = new e2b.ApiClient(config, { requireApiKey: false })
const res = await client.api.GET('/teams', {
signal,
})
handleE2BRequestError(res, 'Error getting teams')
const teams = res.data as Teams
const defaultTeam = teams.find((team) => team.isDefault)
if (!defaultTeam) {
console.error(
asFormattedError('No default team found, please contact support')
)
process.exit(1)
}
userConfig = {
version: 1,
identity: {
email: signInResponse.email,
},
oauth: {
token_endpoint: signInResponse.tokenEndpoint,
revoke_endpoint: signInResponse.revokeEndpoint,
client_id: signInResponse.clientId,
},
tokens: {
access_token: accessToken,
refresh_token: signInResponse.refreshToken,
},
last_refresh: getConfigRefreshTimestamp(),
teamName: defaultTeam.name,
teamId: defaultTeam.teamID,
teamApiKey: defaultTeam.apiKey,
}
writeUserConfig(USER_CONFIG_PATH, userConfig)
}
console.log(
`Logged in as ${asBold(
userConfig.identity.email
)} with selected team ${asBold(userConfig.teamName)}`
)
process.exit(0)
})
interface SignInWithBrowserResponse {
email: string
accessToken: string
refreshToken: string
tokenEndpoint: string
revokeEndpoint: string
clientId: string
}
async function signInWithBrowser(): Promise<SignInWithBrowserResponse> {
const server = http.createServer()
const { port } = await listen.default(server, 0, '127.0.0.1')
const loginUrl = new URL(`${DOCS_BASE}/api/cli`)
loginUrl.searchParams.set('next', `http://localhost:${port}`)
loginUrl.searchParams.set('cliVersion', pkg.version)
return new Promise((resolve, reject) => {
server.once('request', (req, res) => {
// Close the HTTP connection to prevent `server.close()` from hanging
res.setHeader('connection', 'close')
const followUpUrl = new URL(`${DOCS_BASE}/api/cli`)
const searchParams = new URL(req.url || '/', 'http://localhost')
.searchParams
const searchParamsObj = Object.fromEntries(
searchParams.entries()
) as unknown as SignInWithBrowserResponse & {
error?: string
}
const { error } = searchParamsObj
if (error) {
reject(new Error(error))
followUpUrl.searchParams.set('state', 'error')
followUpUrl.searchParams.set('error', error)
} else if (
!searchParamsObj.email ||
!searchParamsObj.accessToken ||
!searchParamsObj.refreshToken ||
!searchParamsObj.tokenEndpoint ||
!searchParamsObj.revokeEndpoint ||
!searchParamsObj.clientId
) {
reject(new Error('Incomplete login response from server'))
followUpUrl.searchParams.set('state', 'error')
followUpUrl.searchParams.set(
'error',
'Incomplete login response from server'
)
} else {
resolve(searchParamsObj)
followUpUrl.searchParams.set('state', 'success')
followUpUrl.searchParams.set('email', searchParamsObj.email!)
}
res.statusCode = 302
res.setHeader('location', followUpUrl.href)
res.end()
})
let manualUrlPrinted = false
const printManualUrl = () => {
if (manualUrlPrinted) return
manualUrlPrinted = true
console.log(
`\nCould not open a browser automatically. Please open the following URL manually to continue:\n\n${loginUrl.toString()}\n\nIf interactive login is unavailable, you can also authenticate by setting the ${asBold(
'E2B_API_KEY'
)} environment variable instead.\n`
)
}
openUrlInBrowser(loginUrl.toString(), printManualUrl)
})
}
+36
View File
@@ -0,0 +1,36 @@
import * as commander from 'commander'
import * as fs from 'fs'
import { getUserConfig, USER_CONFIG_PATH } from 'src/user'
export const logoutCommand = new commander.Command('logout')
.description('log out of CLI')
.action(async () => {
if (!fs.existsSync(USER_CONFIG_PATH)) {
console.log('Not logged in, nothing to do')
return
}
let config
try {
config = getUserConfig()
} catch {
// Malformed config file — proceed to delete it below
}
if (config) {
await fetch(config.oauth.revoke_endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
token: config.tokens.refresh_token,
token_type_hint: 'refresh_token',
client_id: config.oauth.client_id,
}),
}).catch(() => {})
}
if (fs.existsSync(USER_CONFIG_PATH)) {
fs.unlinkSync(USER_CONFIG_PATH)
}
console.log('Logged out.')
})
+21
View File
@@ -0,0 +1,21 @@
import * as commander from 'commander'
import { asPrimary } from 'src/utils/format'
import { templateCommand } from './template'
import { sandboxCommand } from './sandbox'
import { authCommand } from './auth'
export const program = new commander.Command()
.description(
`Create sandbox templates from Dockerfiles by running ${asPrimary(
'e2b template create'
)} then use our SDKs to create sandboxes from these templates.
Visit ${asPrimary(
'E2B docs (https://e2b.dev/docs)'
)} to learn how to create sandbox templates and start sandboxes.
`
)
.addCommand(authCommand)
.addCommand(templateCommand)
.addCommand(sandboxCommand)
@@ -0,0 +1,50 @@
import * as e2b from 'e2b'
import * as commander from 'commander'
import { spawnConnectedTerminal } from 'src/terminal'
import { asBold, asPrimary } from 'src/utils/format'
import { ensureAPIKey } from '../../api'
import { printDashboardSandboxInspectUrl } from 'src/utils/urls'
export const connectCommand = new commander.Command('connect')
.description('connect terminal to already running sandbox')
.argument('<sandboxID>', `connect to sandbox with ${asBold('<sandboxID>')}`)
.alias('cn')
.action(async (sandboxID: string) => {
try {
const apiKey = ensureAPIKey()
if (!sandboxID) {
console.error('You need to specify sandbox ID')
process.exit(1)
}
await connectToSandbox({ apiKey, sandboxID })
// We explicitly call exit because the sandbox is keeping the program alive.
// We also don't want to call sandbox.close because that would disconnect other users from the edit session.
process.exit(0)
} catch (err: any) {
console.error(err)
process.exit(1)
}
})
async function connectToSandbox({
apiKey,
sandboxID,
}: {
apiKey: string
sandboxID: string
}) {
const sandbox = await e2b.Sandbox.connect(sandboxID, { apiKey })
printDashboardSandboxInspectUrl(sandbox.sandboxId)
console.log(
`Terminal connecting to sandbox ${asPrimary(`${sandbox.sandboxId}`)}`
)
await spawnConnectedTerminal(sandbox)
console.log(
`Closing terminal connection to sandbox ${asPrimary(sandbox.sandboxId)}`
)
}
+209
View File
@@ -0,0 +1,209 @@
import * as e2b from 'e2b'
import * as commander from 'commander'
import * as path from 'path'
import { ensureAPIKey } from 'src/api'
import { spawnConnectedTerminal } from 'src/terminal'
import { asBold, asFormattedSandboxTemplate } from 'src/utils/format'
import { getRoot } from '../../utils/filesystem'
import { getConfigPath, loadConfig } from '../../config'
import fs from 'fs'
import { configOption, pathOption } from '../../options'
import { printDashboardSandboxInspectUrl } from 'src/utils/urls'
type SandboxLifecycle = {
onTimeout: 'pause' | 'kill'
autoResume?: boolean
}
const MIN_TIMEOUT_MS = 30_000
export function createCommand(
name: string,
alias: string,
deprecated: boolean
) {
return new commander.Command(name)
.description('create sandbox and connect terminal to it')
.argument(
'[template]',
`create and connect to sandbox specified by ${asBold('[template]')}`
)
.addOption(pathOption)
.addOption(configOption)
.option('-d, --detach', 'create sandbox without connecting terminal to it')
.option(
'--lifecycle.ontimeout <action>',
'action when sandbox timeout is reached: pause or kill',
parseOnTimeout
)
.option(
'--lifecycle.autoresume',
'enable sandbox auto-resume, requires --lifecycle.ontimeout pause'
)
.option('--timeout <seconds>', 'sandbox timeout in seconds', parseTimeout)
.alias(alias)
.action(
async (
template: string | undefined,
opts: {
name?: string
path?: string
config?: string
detach?: boolean
'lifecycle.ontimeout'?: SandboxLifecycle['onTimeout']
'lifecycle.autoresume'?: boolean
timeout?: number
}
) => {
if (deprecated) {
console.warn(
`Warning: The '${name}' command is deprecated and will be removed in future releases. Please use 'e2b sandbox create' instead.`
)
}
try {
const apiKey = ensureAPIKey()
let templateID = template
const root = getRoot(opts.path)
const configPath = getConfigPath(root, opts.config)
const config = fs.existsSync(configPath)
? await loadConfig(configPath)
: undefined
const relativeConfigPath = path.relative(root, configPath)
if (!templateID && config) {
console.log(
`Found sandbox template ${asFormattedSandboxTemplate(
{
templateID: config.template_id,
aliases: config.template_name
? [config.template_name]
: undefined,
},
relativeConfigPath
)}`
)
templateID = config.template_id
}
if (!templateID) {
templateID = 'base'
}
const lifecycle = buildLifecycle(
opts['lifecycle.ontimeout'],
opts['lifecycle.autoresume']
)
const sandboxOpts = {
apiKey,
...(lifecycle ? { lifecycle } : {}),
...(opts.timeout !== undefined ? { timeoutMs: opts.timeout } : {}),
}
const sandbox = await e2b.Sandbox.create(templateID, sandboxOpts)
printDashboardSandboxInspectUrl(sandbox.sandboxId)
if (!opts.detach) {
await connectSandbox({
sandbox,
template: { templateID },
timeoutMs: opts.timeout,
})
} else {
console.log(
`Sandbox created with ID ${sandbox.sandboxId} using template ${templateID}`
)
}
process.exit(0)
} catch (err: any) {
console.error(err)
process.exit(1)
}
}
)
}
function parseTimeout(timeoutRaw: string): number {
const timeoutSeconds = Number(timeoutRaw)
const timeoutMs = Math.floor(timeoutSeconds * 1000)
if (
!Number.isFinite(timeoutSeconds) ||
timeoutSeconds <= 0 ||
timeoutMs < MIN_TIMEOUT_MS
) {
throw new commander.InvalidArgumentError(
'--timeout must be at least 30 seconds'
)
}
return timeoutMs
}
function parseOnTimeout(onTimeout: string): SandboxLifecycle['onTimeout'] {
if (onTimeout !== 'pause' && onTimeout !== 'kill') {
throw new commander.InvalidArgumentError(
'--lifecycle.ontimeout must be "pause" or "kill"'
)
}
return onTimeout
}
function buildLifecycle(
onTimeout: SandboxLifecycle['onTimeout'] | undefined,
autoResume: boolean | undefined
): SandboxLifecycle | undefined {
if (!onTimeout && !autoResume) {
return undefined
}
if (autoResume && onTimeout !== 'pause') {
throw new commander.InvalidArgumentError(
'--lifecycle.autoresume requires --lifecycle.ontimeout pause'
)
}
if (!onTimeout) {
throw new commander.InvalidArgumentError(
'--lifecycle.ontimeout is required when using --lifecycle.autoresume'
)
}
return { onTimeout, ...(autoResume ? { autoResume: true } : {}) }
}
export async function connectSandbox({
sandbox,
template,
timeoutMs,
}: {
sandbox: e2b.Sandbox
template: Pick<e2b.components['schemas']['Template'], 'templateID'>
timeoutMs?: number
}) {
// keep-alive loop — track the in-flight promise so we can await it on shutdown
let pendingKeepAlive: Promise<void> = Promise.resolve()
const keepAliveTimeoutMs = timeoutMs ?? 30_000
const intervalId = setInterval(() => {
pendingKeepAlive = sandbox.setTimeout(keepAliveTimeoutMs)
}, 5_000)
console.log(
`Terminal connecting to template ${asFormattedSandboxTemplate(
template
)} with sandbox ID ${asBold(`${sandbox.sandboxId}`)}`
)
try {
await spawnConnectedTerminal(sandbox)
} finally {
clearInterval(intervalId)
await pendingKeepAlive.catch(() => {})
await sandbox.setTimeout(timeoutMs ?? 1_000)
console.log(
`Closing terminal connection to template ${asFormattedSandboxTemplate(
template
)} with sandbox ID ${asBold(`${sandbox.sandboxId}`)}`
)
}
}
+218
View File
@@ -0,0 +1,218 @@
/**
* Execute a command in a running sandbox.
*/
import { Sandbox, CommandExitError, NotFoundError } from 'e2b'
import * as commander from 'commander'
import { ensureAPIKey } from '../../api'
import { setupSignalHandlers } from 'src/utils/signal'
import { buildCommand, isPipedStdin, streamStdinChunks } from './exec_helpers'
interface ExecOptions {
background?: boolean
cwd?: string
user?: string
env?: Record<string, string>
}
const NO_COMMAND_TIMEOUT = 0
export const execCommand = new commander.Command('exec')
.description('execute a command in a running sandbox')
.argument('<sandboxID>', 'sandbox ID to execute command in')
.argument('<command...>', 'command to execute')
.option('-b, --background', 'run in background and return immediately')
.option('-c, --cwd <dir>', 'working directory')
.option('-u, --user <user>', 'run as specified user')
.option(
'-e, --env <KEY=VALUE>',
'set environment variable (repeatable)',
(value: string, previous: Record<string, string>) => {
const [key, ...rest] = value.split('=')
if (key && rest.length > 0) {
previous[key] = rest.join('=')
}
return previous
},
{} as Record<string, string>
)
.alias('ex')
.action(
async (sandboxID: string, commandParts: string[], opts: ExecOptions) => {
const hasPipedStdin = isPipedStdin()
const command = buildCommand(commandParts)
try {
const apiKey = ensureAPIKey()
const sandbox = await Sandbox.connect(sandboxID, { apiKey })
if (hasPipedStdin && !sandbox.commands.supportsStdinClose) {
console.error(
'e2b: Warning: Piped stdin is not supported by this sandbox version.\n' +
'e2b: Rebuild your template to pick up the latest sandbox version.\n' +
'e2b: Ignoring piped stdin.'
)
}
const canPipeStdin =
hasPipedStdin && sandbox.commands.supportsStdinClose
if (opts.background) {
const handle = await sandbox.commands.run(command, {
background: true,
cwd: opts.cwd,
user: opts.user,
envs: opts.env,
timeoutMs: NO_COMMAND_TIMEOUT,
...(canPipeStdin ? { stdin: true } : {}),
})
const removeSignalHandlers = setupSignalHandlers(async () => {
await handle.kill()
})
try {
if (canPipeStdin) {
await sendStdin(sandbox, handle.pid)
}
} finally {
removeSignalHandlers()
}
console.error(handle.pid)
await handle.disconnect()
// We always exit with code 0 when running in background.
process.exit(0)
}
const exitCode = await runCommand(sandbox, command, opts, canPipeStdin)
process.exit(exitCode)
} catch (err: any) {
console.error(err)
process.exit(1)
}
}
)
async function runCommand(
sandbox: Sandbox,
command: string,
opts: ExecOptions,
openStdin: boolean
): Promise<number> {
const handle = await sandbox.commands.run(command, {
background: true,
cwd: opts.cwd,
user: opts.user,
envs: opts.env,
timeoutMs: NO_COMMAND_TIMEOUT,
onStdout: async (data) => {
try {
process.stdout.write(data)
} catch (err: any) {
console.error(err)
await handle.kill()
}
},
onStderr: async (data) => {
try {
process.stderr.write(data)
} catch (err: any) {
console.error(err)
await handle.kill()
}
},
...(openStdin ? { stdin: true } : {}),
})
const removeSignalHandlers = setupSignalHandlers(async () => {
// Kill the remote process - main loop handles exit code.
await handle.kill()
})
try {
if (openStdin) {
await sendStdin(sandbox, handle.pid)
}
const result = await handle.wait()
return result.exitCode
} catch (err) {
if (handle.error) {
console.error(handle.error)
}
if (err instanceof CommandExitError) {
return err.exitCode
}
// If exit code is not from the command we throw the error.
throw err
} finally {
removeSignalHandlers()
}
}
async function sendStdin(sandbox: Sandbox, pid: number): Promise<void> {
const chunkSizeBytes = 64 * 1024
let processExited = false
await streamStdinChunks(
process.stdin,
async (chunk) => {
if (processExited) {
return false
}
try {
await sandbox.commands.sendStdin(pid, chunk)
} catch (err) {
if (err instanceof NotFoundError) {
processExited = true
console.error(
'e2b: Remote command exited before stdin could be delivered.'
)
return false
}
throw err
}
},
chunkSizeBytes
)
if (processExited) {
return
}
// Signal EOF so commands like cat/wc/grep terminate.
try {
await sandbox.commands.closeStdin(pid)
} catch (err) {
if (err instanceof NotFoundError) {
// Process already exited — EOF is moot.
return
}
// Fail fast instead of leaving a command blocked on stdin forever.
await killProcessBestEffort(sandbox, pid)
throw err
}
}
async function killProcessBestEffort(
sandbox: Sandbox,
pid: number
): Promise<void> {
try {
await sandbox.commands.kill(pid)
} catch (killErr) {
console.error(
'e2b: Failed to kill remote process after stdin EOF signaling failed.'
)
console.error(killErr)
}
}
@@ -0,0 +1,121 @@
import * as fs from 'fs'
const SHELL_SAFE_RE = /^[A-Za-z0-9_@%+=:,./-]+$/
export const shellQuote = (arg: string): string => {
if (arg === '') {
return "''"
}
if (SHELL_SAFE_RE.test(arg)) {
return arg
}
const q = "'\"'\"'"
return `'${arg.replace(/'/g, q)}'`
}
export const buildCommand = (commandParts: string[]): string => {
if (commandParts.length === 1) {
return commandParts[0]
}
return commandParts.map(shellQuote).join(' ')
}
type StatLike = {
isFIFO?: () => boolean
isFile?: () => boolean
isSocket?: () => boolean
isCharacterDevice?: () => boolean
}
type FsLike = { fstatSync: (fd: number) => StatLike }
export const isPipedStdin = (fd = 0, fsModule: FsLike = fs as FsLike) => {
try {
const stdinStats = fsModule.fstatSync(fd)
// Treat any non-interactive stdin as "piped": FIFO pipes and file redirection (`< file`).
// Keep this conservative so normal terminal stdin doesn't get eagerly drained.
if (stdinStats.isCharacterDevice?.()) {
return false
}
return Boolean(
stdinStats.isFIFO?.() || stdinStats.isFile?.() || stdinStats.isSocket?.()
)
} catch {
return false
}
}
type ReadStdinOptions = {
fd?: number
fsModule?: FsLike
stream?: NodeJS.ReadableStream
}
type StdinChunk = Uint8Array | Buffer | string
export const readStdinIfPiped = async (
options: ReadStdinOptions = {}
): Promise<Buffer | undefined> => {
const fd = options.fd ?? 0
const fsModule = options.fsModule ?? (fs as FsLike)
if (!isPipedStdin(fd, fsModule)) {
return undefined
}
const stream = options.stream ?? process.stdin
return await readStdinFrom(stream)
}
export const chunkBytesBySize = (
data: Uint8Array,
maxBytes: number
): Uint8Array[] => {
if (maxBytes <= 0) {
throw new Error('maxBytes must be greater than 0')
}
const chunks: Uint8Array[] = []
for (let offset = 0; offset < data.length; offset += maxBytes) {
chunks.push(data.subarray(offset, offset + maxBytes))
}
return chunks
}
export async function streamStdinChunks(
stream: NodeJS.ReadableStream,
onChunk: (chunk: Uint8Array) => Promise<void | boolean> | void | boolean,
maxBytes: number
): Promise<void> {
if (maxBytes <= 0) {
throw new Error('maxBytes must be greater than 0')
}
for await (const rawChunk of stream as AsyncIterable<StdinChunk>) {
const chunk =
typeof rawChunk === 'string'
? Buffer.from(rawChunk)
: Buffer.from(rawChunk)
if (chunk.byteLength === 0) {
continue
}
const pieces = chunkBytesBySize(chunk, maxBytes)
for (const piece of pieces) {
const shouldContinue = await onChunk(piece)
if (shouldContinue === false) {
return
}
}
}
}
export async function readStdinFrom(
stream: NodeJS.ReadableStream
): Promise<Buffer> {
return await new Promise((resolve, reject) => {
const chunks: Buffer[] = []
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
stream.on('end', () => resolve(Buffer.concat(chunks)))
stream.on('error', reject)
})
}
@@ -0,0 +1,27 @@
import * as commander from 'commander'
import { connectCommand } from './connect'
import { infoCommand } from './info'
import { listCommand } from './list'
import { killCommand } from './kill'
import { pauseCommand } from './pause'
import { resumeCommand } from './resume'
import { createCommand } from './create'
import { logsCommand } from './logs'
import { metricsCommand } from './metrics'
import { execCommand } from './exec'
export const sandboxCommand = new commander.Command('sandbox')
.description('work with sandboxes')
.alias('sbx')
.addCommand(connectCommand)
.addCommand(infoCommand)
.addCommand(listCommand)
.addCommand(killCommand)
.addCommand(pauseCommand)
.addCommand(resumeCommand)
.addCommand(createCommand('create', 'cr', false))
.addCommand(createCommand('spawn', 'sp', true), { hidden: true })
.addCommand(logsCommand)
.addCommand(metricsCommand)
.addCommand(execCommand)
+118
View File
@@ -0,0 +1,118 @@
import * as commander from 'commander'
import { NotFoundError, Sandbox } from 'e2b'
import { ensureAPIKey } from 'src/api'
import { asBold } from 'src/utils/format'
const fieldLabels: Partial<Record<string, string>> = {
sandboxId: 'Sandbox ID',
templateId: 'Template ID',
name: 'Alias',
startedAt: 'Started at',
endAt: 'End at',
state: 'State',
cpuCount: 'vCPUs',
memoryMB: 'RAM MiB',
envdVersion: 'Envd version',
allowInternetAccess: 'Internet access',
lifecycle: 'Lifecycle',
network: 'Network',
sandboxDomain: 'Sandbox domain',
metadata: 'Metadata',
}
const fieldOrder = [
'sandboxId',
'templateId',
'name',
'state',
'startedAt',
'endAt',
'cpuCount',
'memoryMB',
'envdVersion',
'allowInternetAccess',
'lifecycle',
'network',
'sandboxDomain',
'metadata',
]
export const infoCommand = new commander.Command('info')
.description('show information for a sandbox')
.argument(
'<sandboxID>',
`show information for sandbox specified by ${asBold('<sandboxID>')}`
)
.alias('in')
.option('-f, --format <format>', 'output format, eg. json, pretty')
.action(async (sandboxID: string, options: { format?: string }) => {
try {
const format = options.format || 'pretty'
const apiKey = ensureAPIKey()
const info = await Sandbox.getInfo(sandboxID, { apiKey })
if (format === 'pretty') {
renderPrettyInfo(info as unknown as Record<string, unknown>)
} else if (format === 'json') {
console.log(JSON.stringify(info, null, 2))
} else {
console.error(`Unsupported output format: ${format}`)
process.exit(1)
}
} catch (err: any) {
if (err instanceof NotFoundError) {
console.error(`Sandbox ${asBold(sandboxID)} wasn't found`)
process.exit(1)
return
}
console.error(err)
process.exit(1)
}
})
function renderPrettyInfo(info: Record<string, unknown>) {
console.log(
`\nSandbox info for ${asBold(String(info.sandboxId ?? 'unknown'))}:`
)
const orderedKeys = [
...fieldOrder.filter((key) => key in info),
...Object.keys(info).filter((key) => !fieldOrder.includes(key)),
]
for (const key of orderedKeys) {
const value = info[key]
if (value === undefined) {
continue
}
const label = fieldLabels[key] ?? key
const formattedValue = formatValue(value)
if (formattedValue.includes('\n')) {
const indentedValue = formattedValue
.split('\n')
.map((line) => ` ${line}`)
.join('\n')
console.log(`${asBold(label)}:\n${indentedValue}`)
continue
}
console.log(`${asBold(label)}: ${formattedValue}`)
}
process.stdout.write('\n')
}
function formatValue(value: unknown): string {
if (value instanceof Date) {
return value.toLocaleString()
}
if (Array.isArray(value) || (typeof value === 'object' && value !== null)) {
return JSON.stringify(value, null, 2)
}
return String(value)
}
+107
View File
@@ -0,0 +1,107 @@
import * as commander from 'commander'
import { ensureAPIKey } from 'src/api'
import { asBold } from 'src/utils/format'
import * as e2b from 'e2b'
import { Sandbox, components } from 'e2b'
import { parseMetadata } from './utils'
async function killSandbox(sandboxID: string, apiKey: string) {
const killed = await e2b.Sandbox.kill(sandboxID, { apiKey })
if (killed) {
console.log(`Sandbox ${asBold(sandboxID)} has been killed`)
} else {
console.error(`Sandbox ${asBold(sandboxID)} wasn't found`)
}
}
export const killCommand = new commander.Command('kill')
.description('kill sandbox')
.argument(
'[sandboxIDs...]',
`kill the sandboxes specified by ${asBold('[sandboxIDs...]')}`
)
.alias('kl')
.option('-a, --all', 'kill all sandboxes')
.option(
'-s, --state <state>',
'when used with -a/--all flag, filter by state, eg. running, paused. Defaults to running',
(value) => value.split(',')
)
.option(
'-m, --metadata <metadata>',
'when used with -a/--all flag, filter by metadata, eg. key1=value1'
)
.action(
async (
sandboxIDs: string[],
{
all,
state,
metadata,
}: {
all: boolean
state: components['schemas']['SandboxState'][]
metadata: string
}
) => {
try {
const apiKey = ensureAPIKey()
const sandboxesState = state || ['running']
const sandboxesMetadata = parseMetadata(metadata)
if ((!sandboxIDs || sandboxIDs.length === 0) && !all) {
console.error(
`You need to specify ${asBold('[sandboxIDs...]')} or use ${asBold(
'-a/--all'
)} flag`
)
process.exit(1)
}
if (all && sandboxIDs && sandboxIDs.length > 0) {
console.error(
`You cannot use ${asBold(
'-a/--all'
)} flag while specifying ${asBold('[sandboxIDs...]')}`
)
process.exit(1)
}
if (all) {
let total = 0
const iterator = Sandbox.list({
apiKey,
query: {
state: sandboxesState,
metadata: sandboxesMetadata,
},
})
while (iterator.hasNext) {
const sandboxes = await iterator.nextItems()
total += sandboxes.length
await Promise.all(
sandboxes.map((sandbox) => killSandbox(sandbox.sandboxId, apiKey))
)
}
if (total === 0) {
console.log('No running sandboxes')
} else {
console.log(`Killed ${total} running sandboxes`)
}
process.exit(0)
} else {
await Promise.all(
sandboxIDs.map((sandboxID) => killSandbox(sandboxID, apiKey))
)
}
} catch (err: any) {
console.error(err)
process.exit(1)
}
}
)
+178
View File
@@ -0,0 +1,178 @@
import * as tablePrinter from 'console-table-printer'
import * as commander from 'commander'
import { components, Sandbox, SandboxInfo } from 'e2b'
import { ensureAPIKey } from 'src/api'
import { parseMetadata } from './utils'
const DEFAULT_LIMIT = 1000
const PAGE_LIMIT = 100
function getStateTitle(state?: components['schemas']['SandboxState'][]) {
if (state?.length === 1) {
if (state?.includes('running')) return 'Running sandboxes'
if (state?.includes('paused')) return 'Paused sandboxes'
}
return 'Sandboxes'
}
export const listCommand = new commander.Command('list')
.description('list all sandboxes, by default it list only running ones')
.alias('ls')
.option(
'-s, --state <state>',
'filter by state, eg. running, paused. Defaults to running',
(value) => value.split(',')
)
.option('-m, --metadata <metadata>', 'filter by metadata, eg. key1=value1')
.option(
'-l, --limit <limit>',
`limit the number of sandboxes returned (default: ${DEFAULT_LIMIT}, 0 for no limit)`,
(value) => parseInt(value)
)
.option('-f, --format <format>', 'output format, eg. json, pretty')
.action(async (options) => {
try {
const state = options.state || ['running']
const format = options.format || 'pretty'
const limit =
options.limit === 0 ? undefined : (options.limit ?? DEFAULT_LIMIT)
const { sandboxes, hasMore } = await listSandboxes({
limit,
state,
metadataRaw: options.metadata,
})
if (format === 'pretty') {
renderTable(sandboxes, state)
if (hasMore) {
console.log(
`Showing first ${limit} sandboxes. Use --limit to change.`
)
}
} else if (format === 'json') {
console.log(JSON.stringify(sandboxes, null, 2))
} else {
console.error(`Unsupported output format: ${format}`)
process.exit(1)
}
} catch (err: any) {
console.error(err)
process.exit(1)
}
})
function renderTable(
sandboxes: SandboxInfo[],
state: components['schemas']['SandboxState'][]
) {
if (!sandboxes?.length) {
console.log('No sandboxes found')
return
}
const table = new tablePrinter.Table({
title: getStateTitle(state),
columns: [
{ name: 'sandboxId', alignment: 'left', title: 'Sandbox ID' },
{
name: 'templateId',
alignment: 'left',
title: 'Template ID',
maxLen: 20,
},
{ name: 'name', alignment: 'left', title: 'Alias' },
{ name: 'startedAt', alignment: 'left', title: 'Started at' },
{ name: 'endAt', alignment: 'left', title: 'End at' },
{ name: 'state', alignment: 'left', title: 'State' },
{ name: 'cpuCount', alignment: 'left', title: 'vCPUs' },
{ name: 'memoryMB', alignment: 'left', title: 'RAM MiB' },
{ name: 'envdVersion', alignment: 'left', title: 'Envd version' },
{ name: 'metadata', alignment: 'left', title: 'Metadata' },
],
disabledColumns: ['clientID'],
rows: sandboxes
.map((sandbox) => ({
...sandbox,
startedAt: new Date(sandbox.startedAt).toLocaleString(),
endAt: new Date(sandbox.endAt).toLocaleString(),
state: sandbox.state.charAt(0).toUpperCase() + sandbox.state.slice(1), // capitalize
metadata: JSON.stringify(sandbox.metadata),
}))
.sort(
(a, b) =>
a.startedAt.localeCompare(b.startedAt) ||
a.sandboxId.localeCompare(b.sandboxId)
),
style: {
headerTop: {
left: '',
right: '',
mid: '',
other: '',
},
headerBottom: {
left: '',
right: '',
mid: '',
other: '',
},
tableBottom: {
left: '',
right: '',
mid: '',
other: '',
},
vertical: '',
},
colorMap: {
orange: '\x1b[38;5;216m',
},
})
table.printTable()
process.stdout.write('\n')
}
type ListSandboxesOptions = {
limit?: number
state?: components['schemas']['SandboxState'][]
metadataRaw?: string
}
type ListSandboxesResult = {
sandboxes: SandboxInfo[]
hasMore: boolean
}
export async function listSandboxes({
limit,
state,
metadataRaw,
}: ListSandboxesOptions = {}): Promise<ListSandboxesResult> {
const apiKey = ensureAPIKey()
const metadata = parseMetadata(metadataRaw)
let pageLimit = limit
if (!limit || limit > PAGE_LIMIT) {
pageLimit = PAGE_LIMIT
}
const sandboxes: SandboxInfo[] = []
const iterator = Sandbox.list({
apiKey: apiKey,
limit: pageLimit,
query: { state, metadata },
})
while (iterator.hasNext && (!limit || sandboxes.length < limit)) {
const batch = await iterator.nextItems()
sandboxes.push(...batch)
}
return {
sandboxes: limit ? sandboxes.slice(0, limit) : sandboxes,
// We can't change the page size during iteration, so we may have to check if we have more sandboxes than the limit
hasMore: iterator.hasNext || (limit ? sandboxes.length > limit : false),
}
}
+265
View File
@@ -0,0 +1,265 @@
import * as commander from 'commander'
import * as e2b from 'e2b'
import * as util from 'util'
import * as chalk from 'chalk'
import { client, connectionConfig } from 'src/api'
import { asBold, asTimestamp, withUnderline } from 'src/utils/format'
import { wait } from 'src/utils/wait'
import { handleE2BRequestError } from '../../utils/errors'
import { waitForSandboxEnd, formatEnum, Format, isRunning } from './utils'
enum LogLevel {
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
}
function isLevelIncluded(level: LogLevel, allowedLevel?: LogLevel) {
if (!allowedLevel) {
return true
}
switch (allowedLevel) {
case LogLevel.DEBUG:
return true
case LogLevel.INFO:
return (
level === LogLevel.INFO ||
level === LogLevel.WARN ||
level === LogLevel.ERROR
)
case LogLevel.WARN:
return level === LogLevel.WARN || level === LogLevel.ERROR
case LogLevel.ERROR:
return level === LogLevel.ERROR
}
}
function cleanLogger(logger?: string) {
if (!logger) {
return ''
}
return logger.replaceAll('Svc', '')
}
export const logsCommand = new commander.Command('logs')
.description('show logs for sandbox')
.argument(
'<sandboxID>',
`show logs for sandbox specified by ${asBold('<sandboxID>')}`
)
.alias('lg')
.option(
'--level <level>',
`filter logs by level (${formatEnum(
LogLevel
)}). The logs with the higher levels will be also shown.`,
LogLevel.INFO
)
.option('-f, --follow', 'keep streaming logs until the sandbox is closed')
.option(
'--format <format>',
`specify format for printing logs (${formatEnum(Format)})`,
Format.PRETTY
)
.option(
'--loggers [loggers]',
'filter logs by loggers. Specify multiple loggers by separating them with a comma.',
(val: string) => val.split(',')
)
.action(
async (
sandboxID: string,
opts?: {
level: string
follow: boolean
format: Format
loggers?: string[]
}
) => {
try {
const level = opts?.level.toUpperCase() as LogLevel | undefined
if (level && !Object.values(LogLevel).includes(level)) {
throw new Error(`Invalid log level: ${level}`)
}
const format = opts?.format.toLowerCase() as Format | undefined
if (format && !Object.values(Format).includes(format)) {
throw new Error(`Invalid log format: ${format}`)
}
const getIsRunning = opts?.follow
? waitForSandboxEnd(sandboxID)
: () => false
let start: number | undefined
let isFirstRun = true
let firstLogsPrinted = false
if (format === Format.PRETTY) {
console.log(`\nLogs for sandbox ${asBold(sandboxID)}:`)
}
do {
const logs = await listSandboxLogs({ sandboxID, start })
if (logs.length !== 0 && firstLogsPrinted === false) {
firstLogsPrinted = true
process.stdout.write('\n')
}
for (const log of logs) {
printLog(
log.timestamp,
log.line,
level,
format,
opts?.loggers ?? undefined
)
}
if (!opts?.follow) break
const isSandboxRunning = await isRunning(sandboxID)
if (!isSandboxRunning && logs.length === 0 && isFirstRun) {
if (format === Format.PRETTY) {
console.log(
`\nStopped printing logs — sandbox ${withUnderline(
'not found'
)}`
)
}
break
}
if (!isSandboxRunning) {
if (format === Format.PRETTY) {
console.log(
`\nStopped printing logs — sandbox is ${withUnderline(
'closed'
)}`
)
}
break
}
const lastLog = logs.length > 0 ? logs[logs.length - 1] : undefined
if (lastLog) {
// TODO: Use the timestamp from the last log instead of the current time?
start = new Date(lastLog.timestamp).getTime() + 1
}
await wait(400)
isFirstRun = false
} while (getIsRunning() && opts?.follow)
} catch (err: any) {
console.error(err)
process.exit(1)
}
}
)
function printLog(
timestamp: string,
line: string,
allowedLevel: LogLevel | undefined,
format: Format | undefined,
allowedLoggers?: string[] | undefined
) {
const log = JSON.parse(line)
let level = log['level'].toUpperCase()
log.logger = cleanLogger(log.logger)
// Check if the current logger startsWith any of the allowed loggers. If there are no specified loggers, print logs from all loggers.
if (
allowedLoggers !== undefined &&
Array.isArray(allowedLoggers) &&
!allowedLoggers.some((allowedLogger) =>
log.logger.startsWith(allowedLogger)
)
) {
return
}
if (!isLevelIncluded(level, allowedLevel)) {
return
}
switch (level) {
case LogLevel.DEBUG:
level = chalk.default.black(chalk.default.bgWhite(level))
break
case LogLevel.INFO:
level = chalk.default.black(chalk.default.bgGreen(level) + ' ')
break
case LogLevel.WARN:
level = chalk.default.black(chalk.default.bgYellow(level) + ' ')
break
case LogLevel.ERROR:
level = chalk.default.white(chalk.default.bgRed(level))
break
}
delete log['traceID']
delete log['instanceID']
delete log['source_type']
delete log['teamID']
delete log['source']
delete log['service']
delete log['envID']
delete log['sandboxID']
if (format === Format.JSON) {
console.log(
JSON.stringify({
timestamp: new Date(timestamp).toISOString(),
level,
...log,
})
)
} else {
const time = `[${new Date(timestamp).toISOString().replace(/T/, ' ')}]`
delete log['level']
console.log(
`${asTimestamp(time)} ${level} ` +
util.inspect(log, {
colors: true,
depth: null,
maxArrayLength: Infinity,
sorted: true,
compact: true,
breakLength: Infinity,
})
)
}
}
export async function listSandboxLogs({
sandboxID,
start,
}: {
sandboxID: string
start?: number
}): Promise<e2b.components['schemas']['SandboxLog'][]> {
const signal = connectionConfig.getSignal()
const res = await client.api.GET('/sandboxes/{sandboxID}/logs', {
signal,
params: {
path: {
sandboxID,
},
query: {
start,
},
},
})
handleE2BRequestError(res, 'Error while getting sandbox logs')
return res.data.logs
}
@@ -0,0 +1,148 @@
import * as chalk from 'chalk'
import * as commander from 'commander'
import { asBold, asTimestamp, withUnderline } from 'src/utils/format'
import { wait } from 'src/utils/wait'
import { formatEnum, Format, isRunning } from './utils'
import { Sandbox } from 'e2b'
import { ensureAPIKey } from '../../api'
export const metricsCommand = new commander.Command('metrics')
.description('show metrics for sandbox')
.argument(
'<sandboxID>',
`show metrics for sandbox specified by ${asBold('<sandboxID>')}`
)
.alias('mt')
.option('-f, --follow', 'keep streaming metrics until the sandbox is closed')
.option(
'--format <format>',
`specify format for printing metrics (${formatEnum(Format)})`,
Format.PRETTY
)
.action(
async (
sandboxID: string,
opts?: {
follow: boolean
format: Format
}
) => {
try {
const format = opts?.format.toLowerCase() as Format | undefined
if (format && !Object.values(Format).includes(format)) {
throw new Error(`Invalid log format: ${format}`)
}
let start: Date | undefined
let isFirstRun = true
let firstMetricsPrinted = false
if (format === Format.PRETTY) {
console.log(`\nMetrics for sandbox ${asBold(sandboxID)}:`)
}
const apiKey = ensureAPIKey()
const isRunningPromise = isRunning(sandboxID)
do {
const metrics = await Sandbox.getMetrics(sandboxID, { start, apiKey })
if (metrics.length !== 0 && !firstMetricsPrinted) {
firstMetricsPrinted = true
process.stdout.write('\n')
}
for (const metric of metrics) {
if (start && metric.timestamp <= start) {
// Skip the metric if it has the same timestamp as the last one
continue
}
start = metric.timestamp
printMetric(metric.timestamp, JSON.stringify(metric), format)
}
const isRunning = await isRunningPromise
if (!isRunning && metrics.length === 0 && isFirstRun) {
if (format === Format.PRETTY) {
console.log(
`\nStopped printing metrics — sandbox ${withUnderline(
'not found'
)}`
)
}
break
}
if (!isRunning) {
if (format === Format.PRETTY) {
console.log(
`\nStopped printing metrics — sandbox is ${withUnderline(
'closed'
)}`
)
}
break
}
await wait(400)
isFirstRun = false
} while (opts?.follow)
} catch (err: any) {
console.error(err)
process.exit(1)
}
}
)
function printMetric(
timestamp: Date,
line: string,
format: Format | undefined
) {
const metric = JSON.parse(line)
const level = chalk.default.green()
if (format === Format.JSON) {
console.log(
JSON.stringify({
timestamp: timestamp.toISOString(),
...metric,
})
)
} else {
const time = `[${timestamp
.toISOString()
.replace(/\.\d{3}Z/, 'Z')
.replace(/T/, ' ')}]`
delete metric['timestamp']
const multipleCores = metric.cpuCount > 1
metric.cpuCount += 0
console.log(
`${asTimestamp(time)} ${level} ` +
asBold('CPU') +
`: ${metric.cpuUsedPct.toString().padStart(5)}% / ${metric.cpuCount
.toString()
.padStart(2)} Core${multipleCores && 's'} | ` +
asBold('Memory') +
`: ${toMB(metric.memUsed).toFixed(0).padStart(5)} / ${toMB(
metric.memTotal
)
.toFixed(0)
.padEnd(5)} MiB | ` +
asBold('Disk') +
`: ${toMB(metric.diskUsed).toFixed(0).padStart(5)} / ${toMB(
metric.diskTotal
)
.toFixed(0)
.padEnd(5)} MiB`
)
}
}
// we can't use bite shift here because shift is 32 bit operation and disk sizes can be greater than 2^32
function toMB(bytes: number): number {
return bytes / 1024 / 1024
}
@@ -0,0 +1,40 @@
import * as commander from 'commander'
import { ensureAPIKey } from 'src/api'
import { asBold } from 'src/utils/format'
import * as e2b from 'e2b'
import { NotFoundError } from 'e2b'
async function pauseSandbox(sandboxID: string, apiKey: string) {
try {
const paused = await e2b.Sandbox.betaPause(sandboxID, { apiKey })
if (paused) {
console.log(`Sandbox ${asBold(sandboxID)} has been paused`)
} else {
console.log(`Sandbox ${asBold(sandboxID)} is already paused`)
}
} catch (err: unknown) {
if (err instanceof NotFoundError) {
console.error(`Sandbox ${asBold(sandboxID)} wasn't found`)
process.exit(1)
}
throw err
}
}
export const pauseCommand = new commander.Command('pause')
.description('pause sandbox')
.argument(
'<sandboxID>',
`pause the sandbox specified by ${asBold('<sandboxID>')}`
)
.alias('ps')
.action(async (sandboxID: string) => {
try {
const apiKey = ensureAPIKey()
await pauseSandbox(sandboxID, apiKey)
} catch (err: unknown) {
console.error(err)
process.exit(1)
}
})
@@ -0,0 +1,36 @@
import * as commander from 'commander'
import { ensureAPIKey } from 'src/api'
import { asBold } from 'src/utils/format'
import * as e2b from 'e2b'
import { NotFoundError } from 'e2b'
async function resumeSandbox(sandboxID: string, apiKey: string) {
try {
await e2b.Sandbox.connect(sandboxID, { apiKey })
console.log(`Sandbox ${asBold(sandboxID)} has been resumed`)
} catch (err: unknown) {
if (err instanceof NotFoundError) {
console.error(`Sandbox ${asBold(sandboxID)} wasn't found`)
process.exit(1)
}
throw err
}
}
export const resumeCommand = new commander.Command('resume')
.description('resume paused sandbox')
.argument(
'<sandboxID>',
`resume the sandbox specified by ${asBold('<sandboxID>')}`
)
.alias('rs')
.action(async (sandboxID: string) => {
try {
const apiKey = ensureAPIKey()
await resumeSandbox(sandboxID, apiKey)
} catch (err: unknown) {
console.error(err)
process.exit(1)
}
})
@@ -0,0 +1,77 @@
import { wait } from '../../utils/wait'
import { asBold } from '../../utils/format'
import { Sandbox } from 'e2b'
import { ensureAPIKey } from 'src/api'
export function formatEnum(e: { [key: string]: string }) {
return Object.values(e)
.map((level) => asBold(level))
.join(', ')
}
export enum Format {
JSON = 'json',
PRETTY = 'pretty',
}
const maxRuntime = 24 * 60 * 60 * 1000 // 24 hours in milliseconds
export function waitForSandboxEnd(sandboxID: string) {
let running = true
async function monitor() {
const startTime = new Date().getTime()
// eslint-disable-next-line no-constant-condition
while (true) {
const currentTime = new Date().getTime()
const elapsedTime = currentTime - startTime // Time elapsed in milliseconds
// Check if 24 hours (in milliseconds) have passed
if (elapsedTime >= maxRuntime) {
break
}
running = await isRunning(sandboxID)
if (!running) {
break
}
await wait(5000)
}
}
monitor()
return () => running
}
export async function isRunning(sandboxID: string) {
try {
const apiKey = ensureAPIKey()
const info = await Sandbox.getInfo(sandboxID, {
apiKey,
})
return info.state === 'running'
} catch (err) {
console.error(`Failed to check sandbox status: ${err}`)
return false
}
}
export function parseMetadata(metadataRaw?: string) {
let metadata: Record<string, string> | undefined = undefined
if (metadataRaw && metadataRaw.length > 0) {
const parsedMetadata: Record<string, string> = {}
metadataRaw.split(',').map((pair: string) => {
const [key, value] = pair.split('=')
if (key && value) {
parsedMetadata[key.trim()] = value.trim()
}
})
metadata = parsedMetadata
}
return metadata
}
@@ -0,0 +1,37 @@
import * as boxen from 'boxen'
import * as commander from 'commander'
import { asBold, asPrimary } from '../../utils/format'
export const buildCommand = new commander.Command('build')
.description('Deprecated: use `e2b template create` instead.')
.argument('[template]', 'unused')
.allowUnknownOption(true)
.alias('bd')
.action(async () => {
const deprecationMessage = `${asBold('DEPRECATION WARNING')}
This is the v1 build system which is now deprecated.
Please migrate to the new build system v2.
Migration guide: ${asPrimary('https://e2b.dev/docs/template/migration-v2')}`
const deprecationWarning = boxen.default(deprecationMessage, {
padding: {
bottom: 0,
top: 0,
left: 2,
right: 2,
},
margin: {
top: 1,
bottom: 1,
left: 0,
right: 0,
},
borderColor: 'yellow',
borderStyle: 'round',
})
console.log(deprecationWarning)
process.exit(1)
})
@@ -0,0 +1,212 @@
import * as boxen from 'boxen'
import * as commander from 'commander'
import { defaultBuildLogger, Template, TemplateClass } from 'e2b'
import { connectionConfig, ensureAPIKey } from 'src/api'
import {
defaultDockerfileName,
fallbackDockerfileName,
} from 'src/docker/constants'
import { parsePositiveInt, pathOption } from 'src/options'
import { validateTemplateName } from 'src/utils/templateName'
import { getRoot } from 'src/utils/filesystem'
import {
asFormattedSandboxTemplate,
asLocal,
asLocalRelative,
asPrimary,
asPython,
asTypescript,
withDelimiter,
} from '../../utils/format'
import { getDockerfile } from './dockerfile'
export const createCommand = new commander.Command('create')
.description(
'build Dockerfile as a Sandbox template. This command reads a Dockerfile and builds it directly.'
)
.argument(
'<template-name>',
'template name to create or rebuild. The template name must be lowercase and contain only letters, numbers, dashes and underscores.'
)
.addOption(pathOption)
.option(
'-d, --dockerfile <file>',
`specify path to Dockerfile. By default E2B tries to find ${asLocal(
defaultDockerfileName
)} or ${asLocal(fallbackDockerfileName)} in root directory.`
)
.option(
'-c, --cmd <start-command>',
'specify command that will be executed when the sandbox is started.'
)
.option(
'--ready-cmd <ready-command>',
'specify command that will need to exit 0 for the template to be ready.'
)
.option(
'--cpu-count <cpu-count>',
'specify the number of CPUs that will be used to run the sandbox. The default value is 2.',
parsePositiveInt('CPU count')
)
.option(
'--memory-mb <memory-mb>',
'specify the amount of memory in megabytes that will be used to run the sandbox. Must be an even number. The default value is 512.',
parsePositiveInt('Memory in megabytes')
)
.option('--no-cache', 'skip cache when building the template.')
.alias('ct')
.action(
async (
templateName: string,
opts: {
path?: string
dockerfile?: string
cmd?: string
readyCmd?: string
cpuCount?: number
memoryMb?: number
noCache?: boolean
}
) => {
try {
process.stdout.write('\n')
// Validate and normalize template name
try {
templateName = validateTemplateName(templateName)
} catch (err) {
console.error(
`Template name ${asLocal(templateName)} is not valid. ${
err instanceof Error ? err.message : String(err)
}`
)
process.exit(1)
}
// Validate memory
if (opts.memoryMb && opts.memoryMb % 2 !== 0) {
console.error(
`The memory in megabytes must be an even number. You provided ${asLocal(
opts.memoryMb.toFixed(0)
)}.`
)
process.exit(1)
}
const root = getRoot(opts.path)
// Use options directly
const dockerfile = opts.dockerfile
const startCmd = opts.cmd
const readyCmd = opts.readyCmd
const cpuCount = opts.cpuCount
const memoryMB = opts.memoryMb
// Get Dockerfile content
const { dockerfileContent, dockerfileRelativePath } = getDockerfile(
root,
dockerfile
)
console.log(
`Found ${asLocalRelative(
dockerfileRelativePath
)} that will be used to build the sandbox template.`
)
// Initialize template builder with file context and parse Dockerfile
const baseTemplate = Template({
fileContextPath: root,
}).fromDockerfile(dockerfileContent)
// Apply start/ready commands if provided
let finalTemplate: TemplateClass = baseTemplate
if (startCmd && readyCmd) {
finalTemplate = baseTemplate.setStartCmd(startCmd, readyCmd)
} else if (readyCmd) {
finalTemplate = baseTemplate.setReadyCmd(readyCmd)
} else if (startCmd) {
console.error('Both start and ready commands must be provided.')
process.exit(1)
}
console.log('\nBuilding sandbox template...\n')
// Prepare API credentials for SDK
const apiKey = ensureAPIKey()
const domain = connectionConfig.domain
// Build the template using SDK
try {
await Template.build(finalTemplate, {
alias: templateName,
cpuCount: cpuCount,
memoryMB: memoryMB,
skipCache: opts.noCache,
apiKey: apiKey,
domain: domain,
onBuildLogs: defaultBuildLogger(),
})
} catch (error) {
console.error('\n❌ Template build failed.')
if (error instanceof Error) {
console.error('Error:', error.message)
}
process.exit(1)
}
// Display success message with examples
const pythonExample = asPython(`from e2b import Sandbox, AsyncSandbox
# Create sync sandbox
sandbox = Sandbox.create("${templateName}")
# Create async sandbox
sandbox = await AsyncSandbox.create("${templateName}")`)
const typescriptExample = asTypescript(`import { Sandbox } from 'e2b'
// Create sandbox
const sandbox = await Sandbox.create('${templateName}')`)
const examplesMessage = `You can now use the template to create custom sandboxes.\nLearn more on ${asPrimary(
'https://e2b.dev/docs'
)}`
const exampleHeader = boxen.default(examplesMessage, {
padding: {
bottom: 1,
top: 1,
left: 2,
right: 2,
},
margin: {
top: 1,
bottom: 1,
left: 0,
right: 0,
},
fullscreen(width) {
return [width, 0]
},
float: 'left',
})
const exampleUsage = `${withDelimiter(
pythonExample,
'Python SDK'
)}\n${withDelimiter(typescriptExample, 'JS SDK', true)}`
console.log(
`\n✅ Building sandbox template ${asFormattedSandboxTemplate({
templateID: templateName,
})} finished.\n${exampleHeader}\n${exampleUsage}\n`
)
process.exit(0)
} catch (err: any) {
console.error(err)
process.exit(1)
}
}
)
@@ -0,0 +1,187 @@
import * as commander from 'commander'
import * as chalk from 'chalk'
import * as fs from 'fs'
import {
asBold,
asFormattedError,
asFormattedSandboxTemplate,
asLocal,
asLocalRelative,
} from 'src/utils/format'
import {
configOption,
pathOption,
selectMultipleOption,
teamOption,
} from 'src/options'
import {
E2BConfig,
configName,
deleteConfig,
getConfigPath,
loadConfig,
} from 'src/config'
import { getRoot } from 'src/utils/filesystem'
import { listSandboxTemplates } from './list'
import { getPromptTemplates } from 'src/utils/templatePrompt'
import { confirm } from 'src/utils/confirm'
import { client, resolveTeamId } from 'src/api'
import { handleE2BRequestError } from '../../utils/errors'
async function deleteTemplate(templateID: string) {
const res = await client.api.DELETE('/templates/{templateID}', {
params: {
path: {
templateID,
},
},
})
handleE2BRequestError(res, 'Error deleting sandbox template')
return
}
export const deleteCommand = new commander.Command('delete')
.description(`delete sandbox template and ${asLocal(configName)} config`)
.argument(
'[template]',
`specify ${asBold('[template]')} to delete it. If you dont specify ${asBold(
'[template]'
)} the command will try to delete sandbox template defined by ${asLocal(
'e2b.toml'
)}.`
)
.addOption(pathOption)
.addOption(configOption)
.addOption(selectMultipleOption)
.addOption(teamOption)
.alias('dl')
.option('-y, --yes', 'skip manual delete confirmation')
.action(
async (
template,
opts: {
path?: string
config?: string
yes?: boolean
select?: boolean
team?: string
}
) => {
try {
let teamId = opts.team
const root = getRoot(opts.path)
const templates: (Pick<E2BConfig, 'template_id'> & {
configPath?: string
})[] = []
if (template) {
templates.push({
template_id: template,
})
} else if (opts.select) {
teamId = resolveTeamId(teamId)
const allTemplates = await listSandboxTemplates({
teamID: teamId,
})
const selectedTemplates = await getPromptTemplates(
allTemplates,
'Select sandbox templates to delete'
)
templates.push(
...selectedTemplates.map((e) => ({
template_id: e.templateID,
...e,
}))
)
if (!templates || templates.length === 0) {
console.log('No sandbox templates selected')
return
}
} else {
const configPath = getConfigPath(root, opts.config)
const config = fs.existsSync(configPath)
? await loadConfig(configPath)
: undefined
if (!config) {
console.log(
`No ${asLocal(configName)} found in ${asLocalRelative(
root
)}. Specify sandbox template with ${asBold(
'[template]'
)} argument or use interactive mode with ${asBold('-s')} flag.`
)
return
}
templates.push({
...config,
configPath,
})
}
if (!templates || templates.length === 0) {
console.log(
`No sandbox templates selected. Specify sandbox template with ${asBold(
'[template]'
)} argument or use interactive mode with ${asBold('-s')} flag.`
)
return
}
console.log(
chalk.default.red(
chalk.default.underline('\nSandbox templates to delete')
)
)
templates.forEach((e) =>
console.log(
asFormattedSandboxTemplate(
{ ...e, templateID: e.template_id },
e.configPath
)
)
)
process.stdout.write('\n')
if (!opts.yes) {
const confirmed = await confirm(
`Do you really want to delete ${
templates.length === 1 ? 'this template' : 'these templates'
}?`
)
if (!confirmed) {
console.log('Canceled')
return
}
}
await Promise.all(
templates.map(async (e) => {
console.log(
`- Deleting sandbox template ${asFormattedSandboxTemplate(
{ ...e, templateID: e.template_id },
e.configPath
)}`
)
await deleteTemplate(e.template_id)
if (e.configPath) {
await deleteConfig(e.configPath)
}
})
)
process.stdout.write('\n')
} catch (err: any) {
console.error(asFormattedError(err.message))
process.exit(1)
}
}
)
@@ -0,0 +1,71 @@
import * as fs from 'fs'
import * as path from 'path'
import {
defaultDockerfileName,
fallbackDockerfileName,
} from 'src/docker/constants'
import { asBold, asLocalRelative } from '../../utils/format'
function loadFile(filePath: string) {
if (!fs.existsSync(filePath)) {
return undefined
}
return fs.readFileSync(filePath, 'utf-8')
}
export function getDockerfile(root: string, file?: string) {
if (file) {
const dockerfilePath = path.join(root, file)
const dockerfileContent = loadFile(dockerfilePath)
const dockerfileRelativePath = path.relative(root, dockerfilePath)
if (dockerfileContent === undefined) {
throw new Error(
`No ${asLocalRelative(
dockerfileRelativePath
)} found in the root directory.`
)
}
return {
dockerfilePath,
dockerfileContent,
dockerfileRelativePath,
}
}
let dockerfilePath = path.join(root, defaultDockerfileName)
let dockerfileContent = loadFile(dockerfilePath)
const defaultDockerfileRelativePath = path.relative(root, dockerfilePath)
let dockerfileRelativePath = defaultDockerfileRelativePath
if (dockerfileContent !== undefined) {
return {
dockerfilePath,
dockerfileContent,
dockerfileRelativePath,
}
}
dockerfilePath = path.join(root, fallbackDockerfileName)
dockerfileContent = loadFile(dockerfilePath)
const fallbackDockerfileRelativeName = path.relative(root, dockerfilePath)
dockerfileRelativePath = fallbackDockerfileRelativeName
if (dockerfileContent !== undefined) {
return {
dockerfilePath,
dockerfileContent,
dockerfileRelativePath,
}
}
throw new Error(
`No ${asLocalRelative(defaultDockerfileRelativePath)} or ${asLocalRelative(
fallbackDockerfileRelativeName
)} found in the root directory (${root}). You can specify a custom Dockerfile with ${asBold(
'--dockerfile <file>'
)} option.`
)
}
@@ -0,0 +1,24 @@
import * as fs from 'fs'
import * as path from 'path'
/**
* Write content to a file, creating directories if needed
*/
export async function writeFileContent(
filePath: string,
content: string
): Promise<void> {
if (fs.existsSync(filePath)) {
throw new Error(
`File ${filePath} already exists. Aborting to avoid overwrite.`
)
}
// Ensure directory exists
const dir = path.dirname(filePath)
if (!fs.existsSync(dir)) {
await fs.promises.mkdir(dir, { recursive: true })
}
await fs.promises.writeFile(filePath, content)
}
@@ -0,0 +1,233 @@
import { Template, TemplateClass } from 'e2b'
import * as fs from 'fs'
import HandlebarsLib from 'handlebars'
import * as path from 'path'
import {
GeneratedFiles,
Language,
TemplateJSON,
TemplateWithStepsJSON,
} from './types'
class Handlebars {
private handlebars: typeof HandlebarsLib
constructor() {
const handlebars = HandlebarsLib.create()
handlebars.registerHelper('eq', function (a: any, b: any, options: any) {
if (a === b) {
// @ts-ignore - this context is provided by Handlebars
return options.fn(this)
}
return ''
})
handlebars.registerHelper('escapeQuotes', function (str) {
return str ? str.replace(/'/g, "\\'") : str
})
handlebars.registerHelper('escapeDoubleQuotes', function (str) {
return str ? str.replace(/"/g, '\\"') : str
})
this.handlebars = handlebars
}
compile(template: string) {
return this.handlebars.compile(template)
}
}
interface HandlebarStep {
type: string
args?: string[]
envVars?: Record<string, string>
src?: string
dest?: string
}
/**
* Transform template data for Handlebars
*/
export async function transformTemplateData(
template: TemplateClass
): Promise<TemplateJSON & { steps: HandlebarStep[] }> {
// Extract JSON structure from parsed template
const jsonString = await Template.toJSON(template, false)
const json = JSON.parse(jsonString) as TemplateWithStepsJSON
const transformedSteps: HandlebarStep[] = []
for (const step of json.steps) {
switch (step.type) {
case 'ENV': {
// Keep all environment variables from one ENV instruction together
const envVars: Record<string, string> = {}
for (let i = 0; i < step.args.length; i += 2) {
if (i + 1 < step.args.length) {
envVars[step.args[i]] = step.args[i + 1]
}
}
transformedSteps.push({
type: 'ENV',
envVars,
})
break
}
case 'COPY': {
if (step.args.length >= 2) {
const src = step.args[0]
let dest = step.args[1]
if (!dest || dest === '') {
dest = '.'
}
transformedSteps.push({
type: 'COPY',
src,
dest,
})
}
break
}
default:
transformedSteps.push({
type: step.type,
args: step.args,
})
}
}
return {
...json,
steps: transformedSteps,
}
}
/**
* Convert the template to TypeScript code using Handlebars
*/
export async function generateTypeScriptCode(
template: TemplateClass,
name: string,
cpuCount?: number,
memoryMB?: number
): Promise<{ templateContent: string; buildContent: string }> {
const hb = new Handlebars()
const transformedData = await transformTemplateData(template)
// Load and compile templates
// In dist, templates are at dist/templates/, __dirname is dist/
const templatesDir = path.join(__dirname, 'templates')
const templateSource = fs.readFileSync(
path.join(templatesDir, 'typescript-template.hbs'),
'utf8'
)
const buildSource = fs.readFileSync(
path.join(templatesDir, 'typescript-build.hbs'),
'utf8'
)
const generateTemplateSource = hb.compile(templateSource)
const generateBuildSource = hb.compile(buildSource)
// Generate content
const templateData = {
...transformedData,
}
const templateContent = generateTemplateSource(templateData)
const buildContent = generateBuildSource({
name,
cpuCount,
memoryMB,
})
return {
templateContent: templateContent.trim(),
buildContent: buildContent.trim(),
}
}
/**
* Convert the template to Python code using Handlebars
*/
export async function generatePythonCode(
template: TemplateClass,
name: string,
cpuCount?: number,
memoryMB?: number,
isAsync: boolean = false
): Promise<{ templateContent: string; buildContent: string }> {
const hb = new Handlebars()
const transformedData = await transformTemplateData(template)
// Load and compile templates
// In dist, templates are at dist/templates/, __dirname is dist/
const templatesDir = path.join(__dirname, 'templates')
const templateSource = fs.readFileSync(
path.join(templatesDir, 'python-template.hbs'),
'utf8'
)
const buildSource = fs.readFileSync(
path.join(templatesDir, `python-build-${isAsync ? 'async' : 'sync'}.hbs`),
'utf8'
)
const generateTemplateSource = hb.compile(templateSource)
const generateBuildSource = hb.compile(buildSource)
// Generate content
const templateContent = generateTemplateSource({
...transformedData,
isAsync,
})
const buildContent = generateBuildSource({
name,
cpuCount,
memoryMB,
})
return {
templateContent: templateContent.trim(),
buildContent: buildContent.trim(),
}
}
/**
* Generate README.md content using Handlebars
*/
export async function generateReadmeContent(
name: string,
templateDir: string,
generatedFiles: GeneratedFiles
): Promise<string> {
const hb = new Handlebars()
// Load and compile README template
const templatesDir = path.join(__dirname, 'templates')
const readmeSource = fs.readFileSync(
path.join(templatesDir, 'readme.hbs'),
'utf8'
)
const generateReadmeSource = hb.compile(readmeSource)
// Prepare template data
const templateData = {
name,
templateDir,
templateFile: generatedFiles.templateFile,
buildDevFile: generatedFiles.buildDevFile,
buildProdFile: generatedFiles.buildProdFile,
isTypeScript: generatedFiles.language === Language.TypeScript,
isPython:
generatedFiles.language === Language.PythonSync ||
generatedFiles.language === Language.PythonAsync,
isPythonSync: generatedFiles.language === Language.PythonSync,
isPythonAsync: generatedFiles.language === Language.PythonAsync,
}
return generateReadmeSource(templateData).trim()
}
@@ -0,0 +1,4 @@
// Re-export all the public APIs from the lib modules
export * from './types'
export * from './template-generator'
export * from './file-utils'
@@ -0,0 +1,93 @@
import * as path from 'path'
import { asLocalRelative, asPrimary } from '../../../utils/format'
import { GeneratedFiles, Language, languageDisplay } from './types'
import { generatePythonCode, generateTypeScriptCode } from './handlebars'
import { writeFileContent } from './file-utils'
import { TemplateClass } from 'e2b'
/**
* Generate and write template files for a given language
*/
export async function generateAndWriteTemplateFiles(
root: string,
name: string,
language: Language,
template: TemplateClass,
cpuCount?: number,
memoryMB?: number
): Promise<GeneratedFiles> {
switch (language) {
case Language.TypeScript: {
const { templateContent, buildContent: buildDevContent } =
await generateTypeScriptCode(
template,
`${name}-dev`,
cpuCount,
memoryMB
)
const { buildContent: buildProdContent } = await generateTypeScriptCode(
template,
name,
cpuCount,
memoryMB
)
const templateFile = 'template.ts'
const buildDevFile = 'build.dev.ts'
const buildProdFile = 'build.prod.ts'
await writeFileContent(path.join(root, templateFile), templateContent)
await writeFileContent(path.join(root, buildDevFile), buildDevContent)
await writeFileContent(path.join(root, buildProdFile), buildProdContent)
console.log(
`\n✅ Generated ${asPrimary(
languageDisplay[Language.TypeScript]
)} template files:`
)
console.log(` ${asLocalRelative(templateFile)}`)
console.log(` ${asLocalRelative(buildDevFile)}`)
console.log(` ${asLocalRelative(buildProdFile)}`)
return { templateFile, buildDevFile, buildProdFile, language }
}
case Language.PythonSync:
case Language.PythonAsync: {
const isAsync = language === Language.PythonAsync
const { templateContent, buildContent: buildDevContent } =
await generatePythonCode(
template,
`${name}-dev`,
cpuCount,
memoryMB,
isAsync
)
const { buildContent: buildProdContent } = await generatePythonCode(
template,
name,
cpuCount,
memoryMB,
isAsync
)
const templateFile = 'template.py'
const buildDevFile = 'build_dev.py'
const buildProdFile = 'build_prod.py'
await writeFileContent(path.join(root, templateFile), templateContent)
await writeFileContent(path.join(root, buildDevFile), buildDevContent)
await writeFileContent(path.join(root, buildProdFile), buildProdContent)
console.log(
`\n✅ Generated ${asPrimary(languageDisplay[language])} template files:`
)
console.log(` ${asLocalRelative(templateFile)}`)
console.log(` ${asLocalRelative(buildDevFile)}`)
console.log(` ${asLocalRelative(buildProdFile)}`)
return { templateFile, buildDevFile, buildProdFile, language }
}
default:
throw new Error('Unsupported language')
}
}
@@ -0,0 +1,35 @@
export enum Language {
TypeScript = 'typescript',
PythonSync = 'python-sync',
PythonAsync = 'python-async',
}
export const languageDisplay = {
[Language.TypeScript]: 'TypeScript',
[Language.PythonSync]: 'Python (sync)',
[Language.PythonAsync]: 'Python (async)',
}
export interface TemplateJSON {
fromImage?: string
fromTemplate?: string
startCmd?: string
readyCmd?: string
force: boolean
}
export interface TemplateWithStepsJSON extends TemplateJSON {
steps: Array<{
type: string
args: string[]
filesHash?: string
force?: boolean
}>
}
export interface GeneratedFiles {
templateFile: string
buildDevFile: string
buildProdFile: string
language: Language
}
@@ -0,0 +1,21 @@
import * as commander from 'commander'
import { createCommand } from './create'
import { buildCommand } from './build'
import { deleteCommand } from './delete'
import { initCommand } from './init'
import { listCommand } from './list'
import { migrateCommand } from './migrate'
import { publishCommand, unPublishCommand } from './publish'
export const templateCommand = new commander.Command('template')
.description('manage sandbox templates')
.alias('tpl')
.addCommand(createCommand)
.addCommand(buildCommand, { hidden: true })
.addCommand(listCommand)
.addCommand(initCommand)
.addCommand(deleteCommand)
.addCommand(publishCommand)
.addCommand(unPublishCommand)
.addCommand(migrateCommand)
+312
View File
@@ -0,0 +1,312 @@
import { input, select } from '@inquirer/prompts'
import PackageJson from '@npmcli/package-json'
import * as commander from 'commander'
import { Template } from 'e2b'
import * as fs from 'fs'
import * as path from 'path'
import { pathOption } from 'src/options'
import { getRoot } from 'src/utils/filesystem'
import { asPrimary } from 'src/utils/format'
import { validateTemplateName } from 'src/utils/templateName'
import {
generateAndWriteTemplateFiles,
GeneratedFiles,
Language,
languageDisplay,
} from './generators'
import { generateReadmeContent } from './generators/handlebars'
const DEFAULT_TEMPLATE_NAME = 'my-template'
/**
* Generate template files using shared template generation logic
*/
async function generateTemplateFiles(
root: string,
name: string,
language: Language,
cpuCount?: number,
memoryMB?: number
): Promise<GeneratedFiles> {
const template = Template().fromBaseImage().runCmd('echo Hello World E2B!')
return generateAndWriteTemplateFiles(
root,
name,
language,
template,
cpuCount,
memoryMB
)
}
/**
* Add build scripts to Makefile if it exists or create a new one
*/
async function addMakefileScripts(
root: string,
files: GeneratedFiles,
templateDirName: string
): Promise<void> {
try {
const makefileName = 'Makefile'
const makefileExists = fs.existsSync(path.join(root, makefileName))
let cdPrefix = ''
if (makefileExists) {
cdPrefix = `cd ${templateDirName} && `
}
const makefileContent = `
.PHONY: e2b:build:dev
e2b:build:dev:
\t${cdPrefix}python ${files.buildDevFile}
.PHONY: e2b:build:prod
e2b:build:prod:
\t${cdPrefix}python ${files.buildProdFile}
`
if (makefileExists) {
const makefilePath = path.join(root, makefileName)
await fs.promises.appendFile(makefilePath, '\n' + makefileContent, 'utf8')
} else {
// Create a basic Makefile if it doesn't exist
const makefilePath = path.join(root, templateDirName, makefileName)
await fs.promises.writeFile(makefilePath, makefileContent, 'utf8')
}
console.log('\n📝 Added build scripts to Makefile:')
console.log(
` ${asPrimary('make e2b:build:dev')} - Build development template`
)
console.log(
` ${asPrimary('make e2b:build:prod')} - Build production template`
)
} catch (err) {
console.warn(
'\n⚠️ Could not add scripts to Makefile:',
err instanceof Error ? err.message : err
)
}
}
/**
* Add build scripts to package.json if it exists or create a new one
*/
async function addPackageJsonScripts(
root: string,
files: GeneratedFiles,
templateDirName: string
): Promise<void> {
try {
let cdPrefix = ''
let pkgJson: PackageJson
try {
// The library expects the directory path, not the full file path
pkgJson = await PackageJson.load(root)
cdPrefix = `cd ${templateDirName} && `
} catch (error) {
// Handle the case where package.json does not exist
const createRoot = path.join(root, templateDirName)
pkgJson = await PackageJson.create(createRoot)
}
pkgJson.update({
scripts: {
...pkgJson.content.scripts,
'e2b:build:dev': `${cdPrefix}npx tsx ${files.buildDevFile}`,
'e2b:build:prod': `${cdPrefix}npx tsx ${files.buildProdFile}`,
},
})
// Save the changes
await pkgJson.save()
console.log('\n📝 Added build scripts to package.json:')
console.log(
` ${asPrimary('npm run e2b:build:dev')} - Build development template`
)
console.log(
` ${asPrimary('npm run e2b:build:prod')} - Build production template`
)
} catch (err) {
console.warn(
'\n⚠️ Could not add scripts to package.json:',
err instanceof Error ? err.message : err
)
}
}
export const initCommand = new commander.Command('init')
.description('initialize a new sandbox template using the SDK')
.addOption(pathOption)
.option('-n, --name <name>', 'template name', (value) => {
try {
return validateTemplateName(value)
} catch (err) {
throw new commander.InvalidArgumentError(
err instanceof Error ? err.message : String(err)
)
}
})
.option(
'-l, --language <language>',
`target language: ${Object.values(Language).join(', ')}`,
(value) => {
if (!Object.values(Language).includes(value as Language)) {
throw new commander.InvalidArgumentError(
`Invalid language. Must be one of: ${Object.values(Language).join(
', '
)}`
)
}
return value as Language
}
)
.alias('it')
.action(
async (opts: { path?: string; name?: string; language?: Language }) => {
try {
process.stdout.write('\n')
const root = getRoot(opts.path)
console.log('🚀 Initializing Sandbox Template...\n')
// Step 1: Get template name (from CLI or prompt)
let templateName: string = opts.name ?? DEFAULT_TEMPLATE_NAME
if (opts.name === undefined) {
templateName = await input({
message: 'Enter template name:',
default: DEFAULT_TEMPLATE_NAME,
validate: (input: string) => {
try {
validateTemplateName(input)
} catch (err) {
return err instanceof Error ? err.message : err
}
return true
},
})
}
templateName = validateTemplateName(templateName)
console.log(`Using template name: ${templateName}`)
// Step 2: Get language (from CLI or prompt)
let language: Language
if (opts.language) {
language = opts.language
console.log(`Using language: ${languageDisplay[language]}`)
} else {
language = await select({
message: 'Select target language for template files:',
choices: [
{
name: languageDisplay[Language.TypeScript],
value: Language.TypeScript,
description:
'Generate .ts files for JavaScript/TypeScript projects',
},
{
name: languageDisplay[Language.PythonSync],
value: Language.PythonSync,
description: 'Generate synchronous Python template files',
},
{
name: languageDisplay[Language.PythonAsync],
value: Language.PythonAsync,
description: 'Generate asynchronous Python template files',
},
],
default: Language.TypeScript,
})
}
// Step 3: Create template directory - fail if it already exists
const templateDirName = templateName
const templateDir = path.join(root, templateDirName)
if (fs.existsSync(templateDir)) {
throw new Error(
`Directory '${templateDirName}' already exists. Please choose a different template name or remove the existing directory.`
)
}
await fs.promises.mkdir(templateDir, { recursive: true })
// Step 4: Generate template and build files in the template directory
const generatedFiles = await generateTemplateFiles(
templateDir,
templateName,
language
)
// Step 5: Add scripts
switch (language) {
case Language.TypeScript:
await addPackageJsonScripts(root, generatedFiles, templateDirName)
break
case Language.PythonAsync:
case Language.PythonSync:
await addMakefileScripts(root, generatedFiles, templateDirName)
break
default:
throw new Error('Unsupported language for scripts')
}
// Step 6: Create README.md
const readmeContent = await generateReadmeContent(
templateName,
templateDirName,
generatedFiles
)
const readmeFilePath = path.join(templateDir, 'README.md')
await fs.promises.writeFile(readmeFilePath, readmeContent, 'utf8')
console.log('\n🎉 Template initialized successfully!')
console.log(
`\nTemplate created in: ${asPrimary(`./${templateDirName}/`)}`
)
console.log('\n🔨 To get started with your template:')
switch (language) {
case Language.TypeScript:
console.log(
` ${asPrimary('npm install e2b')} (install e2b dependency)`
)
console.log(
` ${asPrimary('npm run e2b:build:dev')} (for development)`
)
console.log(
` ${asPrimary('npm run e2b:build:prod')} (for production)`
)
break
case Language.PythonAsync:
case Language.PythonSync:
console.log(
` ${asPrimary('pip install e2b')} (install e2b dependency)`
)
console.log(
` ${asPrimary('make e2b:build:dev')} (for development)`
)
console.log(
` ${asPrimary('make e2b:build:prod')} (for production)`
)
break
default:
throw new Error('Unsupported language for instructions')
}
console.log(
`\nLearn more about Sandbox Templates: ${asPrimary(
'https://e2b.dev/docs'
)}\n`
)
} catch (err: any) {
console.error(err)
process.exit(1)
}
}
)
+127
View File
@@ -0,0 +1,127 @@
import * as tablePrinter from 'console-table-printer'
import * as commander from 'commander'
import * as e2b from 'e2b'
import { listAliases } from '../../utils/format'
import { sortTemplatesAliases } from 'src/utils/templateSort'
import { client, ensureAPIKey, resolveTeamId } from 'src/api'
import { teamOption } from '../../options'
import { handleE2BRequestError } from '../../utils/errors'
export const listCommand = new commander.Command('list')
.description('list sandbox templates')
.alias('ls')
.addOption(teamOption)
.option('-f, --format <format>', 'output format, eg. json, pretty')
.action(async (opts: { team: string; format: string }) => {
try {
const format = opts.format || 'pretty'
ensureAPIKey()
process.stdout.write('\n')
const templates = await listSandboxTemplates({
teamID: resolveTeamId(opts.team),
})
for (const template of templates) {
sortTemplatesAliases(template.aliases)
}
if (format === 'pretty') {
renderTable(templates)
} else if (format === 'json') {
console.log(JSON.stringify(templates, null, 2))
} else {
console.error(`Unsupported output format: ${format}`)
process.exit(1)
}
} catch (err: any) {
console.error(err)
process.exit(1)
}
})
function renderTable(templates: e2b.components['schemas']['Template'][]) {
if (!templates?.length) {
console.log('No templates found.')
return
}
const table = new tablePrinter.Table({
title: 'Sandbox templates',
columns: [
{ name: 'visibility', alignment: 'left', title: 'Access' },
{ name: 'templateID', alignment: 'left', title: 'Template ID' },
{
name: 'aliases',
alignment: 'left',
title: 'Template Name',
color: 'orange',
maxLen: 20,
},
{ name: 'cpuCount', alignment: 'right', title: 'vCPUs' },
{ name: 'memoryMB', alignment: 'right', title: 'RAM MiB' },
{ name: 'createdBy', alignment: 'right', title: 'Created by' },
{ name: 'createdAt', alignment: 'right', title: 'Created at' },
{ name: 'diskSizeMB', alignment: 'right', title: 'Disk size MiB' },
{ name: 'envdVersion', alignment: 'right', title: 'Envd version' },
],
disabledColumns: [
'public',
'buildID',
'buildCount',
'lastSpawnedAt',
'spawnCount',
'updatedAt',
],
rows: templates.map((template) => ({
...template,
visibility: template.public ? 'Public' : 'Private',
aliases: listAliases(template.aliases),
createdBy: template.createdBy?.email,
createdAt: new Date(template.createdAt).toLocaleDateString(),
})),
style: {
headerTop: {
left: '',
right: '',
mid: '',
other: '',
},
headerBottom: {
left: '',
right: '',
mid: '',
other: '',
},
tableBottom: {
left: '',
right: '',
mid: '',
other: '',
},
vertical: '',
},
colorMap: {
orange: '\x1b[38;5;216m',
},
})
table.printTable()
process.stdout.write('\n')
}
export async function listSandboxTemplates({
teamID,
}: {
teamID?: string
}): Promise<e2b.components['schemas']['Template'][]> {
const templates = await client.api.GET('/templates', {
params: {
query: { teamID },
},
})
handleE2BRequestError(templates, 'Error getting templates')
return templates.data
}
@@ -0,0 +1,317 @@
import { select } from '@inquirer/prompts'
import * as commander from 'commander'
import { Template, TemplateBuilder, TemplateClass } from 'e2b'
import * as fs from 'fs'
import * as path from 'path'
import { E2BConfig, getConfigPath, loadConfig } from '../../config'
import { defaultDockerfileName } from '../../docker/constants'
import { configOption, parsePositiveInt, pathOption } from '../../options'
import { getRoot } from '../../utils/filesystem'
import { asLocal, asLocalRelative, asPrimary } from '../../utils/format'
import { getDockerfile } from './dockerfile'
import { validateTemplateName } from '../../utils/templateName'
import {
generateAndWriteTemplateFiles,
Language,
languageDisplay,
} from './generators'
/**
* Migrate Dockerfile to a specific target language using SDK
*/
async function migrateToLanguage(
root: string,
config: E2BConfig,
dockerfileContent: string,
language: Language,
nameOverride?: string
): Promise<void> {
// Initialize template with file context
const template = Template({
fileContextPath: root,
})
// Parse Dockerfile using SDK
let baseTemplate: TemplateBuilder
try {
baseTemplate = template.fromDockerfile(dockerfileContent)
} catch (error) {
console.warn(
"\n⚠️ Unfortunately, we weren't able to fully convert the template to the new SDK format."
)
console.warn(
'\nPlease build the Docker image manually, push it to a repository of your choice, and then reference it.'
)
console.warn("\nHere's an example of how to build the Docker image:")
console.warn(
` ${asPrimary(
'docker build -f e2b.Dockerfile --platform linux/amd64 -t your-image-tag .'
)}`
)
console.warn(
'\nAfter building and pushing your image to a repository of your choice, update the generated template files to use the actual image tag.'
)
if (error instanceof Error) {
console.warn('\nCause:', error.message)
}
baseTemplate = template.fromImage('my-custom-image')
}
// Apply config start/ready commands
let parsedTemplate: TemplateClass = baseTemplate
if (config.start_cmd) {
parsedTemplate = baseTemplate.setStartCmd(
config.start_cmd,
config.ready_cmd || 'sleep 20'
)
} else if (config.ready_cmd) {
parsedTemplate = baseTemplate.setReadyCmd(config.ready_cmd)
}
const name = nameOverride || config.template_name || config.template_id
if (!name) {
throw new Error('Template name or ID is required')
}
// Generate code for the target language using shared functionality
await generateAndWriteTemplateFiles(
root,
name,
language,
parsedTemplate,
config.cpu_count,
config.memory_mb
)
}
export const migrateCommand = new commander.Command('migrate')
.description(
`migrate ${asLocal('e2b.Dockerfile')} and ${asLocal(
'e2b.toml'
)} to new Template SDK format`
)
.option(
'-d, --dockerfile <file>',
`specify path to Dockerfile. Defaults to ${asLocal('e2b.Dockerfile')}`
)
.addOption(configOption)
.option(
'-n, --name <name>',
'override the template name used in the generated files. Defaults to the template name or ID from the config file.',
(value) => {
try {
return validateTemplateName(value)
} catch (err) {
throw new commander.InvalidArgumentError(
err instanceof Error ? err.message : String(err)
)
}
}
)
.option(
'-c, --cmd <start-command>',
'override the command that will be executed when the sandbox is started.'
)
.option(
'--ready-cmd <ready-command>',
'override the command that will need to exit 0 for the template to be ready.'
)
.option(
'--cpu-count <cpu-count>',
'override the number of CPUs that will be used to run the sandbox.',
parsePositiveInt('CPU count')
)
.option(
'--memory-mb <memory-mb>',
'override the amount of memory in megabytes that will be used to run the sandbox. Must be an even number.',
parsePositiveInt('Memory in megabytes')
)
.option(
'-l, --language <language>',
`specify target language: ${Object.values(Language).join(', ')}`,
(value) => {
if (!Object.values(Language).includes(value as Language)) {
throw new commander.InvalidArgumentError(
`Invalid language. Must be one of: ${Object.values(Language).join(
', '
)}`
)
}
return value as Language
}
)
.addOption(pathOption)
.action(
async (opts: {
dockerfile?: string
config?: string
path?: string
language?: Language
name?: string
cmd?: string
readyCmd?: string
cpuCount?: number
memoryMb?: number
}) => {
let success = false
try {
console.log('\n🔄 Migrating template configuration to SDK format...\n')
// Validate memory override
if (opts.memoryMb && opts.memoryMb % 2 !== 0) {
throw new Error(
`The memory in megabytes must be an even number. You provided ${asLocal(
opts.memoryMb.toFixed(0)
)}.`
)
}
const root = getRoot(opts.path)
const configPath = getConfigPath(root, opts.config)
const { dockerfileContent, dockerfilePath, dockerfileRelativePath } =
getDockerfile(root, opts.dockerfile)
let config: E2BConfig = {
template_id: 'name-your-template',
dockerfile: defaultDockerfileName,
}
// Validate config file exists
if (fs.existsSync(configPath)) {
config = await loadConfig(configPath)
} else {
console.error(
`Config file ${asLocalRelative(
path.relative(root, configPath)
)} not found. Using defaults.`
)
}
// Apply command-line overrides on top of the loaded config
if (opts.cmd !== undefined) {
config.start_cmd = opts.cmd
}
if (opts.readyCmd !== undefined) {
config.ready_cmd = opts.readyCmd
}
if (opts.cpuCount !== undefined) {
config.cpu_count = opts.cpuCount
}
if (opts.memoryMb !== undefined) {
config.memory_mb = opts.memoryMb
}
// Determine target language
let language: Language
if (opts.language) {
language = opts.language
console.log(`Using language: ${asPrimary(languageDisplay[language])}`)
} else {
// Prompt for language selection
language = await select({
message: 'Select target language for Template SDK:',
choices: [
{
name: languageDisplay[Language.TypeScript],
value: Language.TypeScript,
description:
'Generate .ts files for JavaScript/TypeScript projects',
},
{
name: languageDisplay[Language.PythonSync],
value: Language.PythonSync,
description: 'Generate synchronous Python template files',
},
{
name: languageDisplay[Language.PythonAsync],
value: Language.PythonAsync,
description: 'Generate asynchronous Python template files',
},
],
default: Language.TypeScript,
})
}
// Perform migration
await migrateToLanguage(
root,
config,
dockerfileContent,
language,
opts.name
)
// Rename old files to .old extensions
const oldFilesRenamed: { oldPath: string; newPath: string }[] = []
// Rename Dockerfile if it exists
if (fs.existsSync(dockerfilePath)) {
const oldDockerfilePath = `${dockerfilePath}.old`
fs.renameSync(dockerfilePath, oldDockerfilePath)
oldFilesRenamed.push({
oldPath: dockerfileRelativePath,
newPath: path.relative(root, oldDockerfilePath),
})
}
// Rename e2b.toml if it exists
if (fs.existsSync(configPath)) {
const oldConfigPath = `${configPath}.old`
fs.renameSync(configPath, oldConfigPath)
oldFilesRenamed.push({
oldPath: path.relative(root, configPath),
newPath: path.relative(root, oldConfigPath),
})
}
if (oldFilesRenamed.length > 0) {
console.log('\n📁 Old template files no longer needed:')
oldFilesRenamed.forEach((file) => {
console.log(
` ${asLocalRelative(file.oldPath)}${asLocalRelative(file.newPath)}`
)
})
}
console.log('\n🎉 Migration completed successfully!')
console.log('\n🔨 To get started with your template:')
if (language === Language.TypeScript) {
console.log(
` ${asPrimary('npm install e2b')} (install e2b dependency)`
)
console.log(
` ${asPrimary('npx tsx build.dev.ts')} (run development build)`
)
console.log(
` ${asPrimary('npx tsx build.prod.ts')} (run production build)`
)
} else {
console.log(
` ${asPrimary('pip install e2b')} (install e2b dependency)`
)
console.log(
` ${asPrimary('python build_dev.py')} (run development build)`
)
console.log(
` ${asPrimary('python build_prod.py')} (run production build)`
)
}
console.log(
`\nLearn more about Template SDK: ${asPrimary(
'https://e2b.dev/docs'
)}\n`
)
success = true
} catch (err: any) {
console.error(`Migration failed: ${err.message}`)
process.exit(1)
}
if (success) {
process.exit(0)
}
}
)
@@ -0,0 +1,232 @@
import * as commander from 'commander'
import * as chalk from 'chalk'
import * as fs from 'fs'
import {
asBold,
asFormattedError,
asFormattedSandboxTemplate,
asLocal,
asLocalRelative,
} from 'src/utils/format'
import {
configOption,
pathOption,
selectMultipleOption,
teamOption,
} from 'src/options'
import { configName, E2BConfig, getConfigPath, loadConfig } from 'src/config'
import { getRoot } from 'src/utils/filesystem'
import { listSandboxTemplates } from './list'
import { getPromptTemplates } from 'src/utils/templatePrompt'
import { confirm } from 'src/utils/confirm'
import { client, resolveTeamId } from 'src/api'
import { handleE2BRequestError } from '../../utils/errors'
async function publishTemplate(templateID: string, publish: boolean) {
const res = await client.api.PATCH('/v2/templates/{templateID}', {
params: {
path: {
templateID,
},
},
body: {
public: publish,
},
})
handleE2BRequestError(
res,
`Error ${publish ? 'publishing' : 'unpublishing'} sandbox template`
)
return res.data?.names ?? []
}
async function templateAction(
publish: boolean,
template: string,
opts: {
path?: string
config?: string
yes?: boolean
select?: boolean
team?: string
}
) {
try {
let teamId = opts.team
const root = getRoot(opts.path)
const templates: (Pick<E2BConfig, 'template_id'> & {
configPath?: string
})[] = []
if (template) {
templates.push({
template_id: template,
})
} else if (opts.select) {
teamId = resolveTeamId(teamId)
const allTemplates = await listSandboxTemplates({
teamID: teamId,
})
const filteredTemplates = allTemplates.filter(
(e) => !e.public === publish
)
if (filteredTemplates.length === 0) {
console.log(
`No sandbox templates available ${
publish ? 'to publish' : 'to unpublish'
} found`
)
return
}
const selectedTemplates = await getPromptTemplates(
filteredTemplates,
`Select sandbox templates to ${publish ? 'publish' : 'unpublish'}`
)
templates.push(
...selectedTemplates.map((e) => ({
template_id: e.templateID,
...e,
}))
)
if (!templates || templates.length === 0) {
console.log('No sandbox templates selected')
return
}
} else {
const configPath = getConfigPath(root, opts.config)
const config = fs.existsSync(configPath)
? await loadConfig(configPath)
: undefined
if (!config) {
console.log(
`No ${asLocal(configName)} found in ${asLocalRelative(
root
)}. Specify sandbox template with ${asBold(
'[template]'
)} argument or use interactive mode with ${asBold('-s')} flag.`
)
return
}
templates.push({
...config,
configPath,
})
}
if (!templates || templates.length === 0) {
console.log(
`No sandbox templates selected. Specify sandbox template with ${asBold(
'[template]'
)} argument or use interactive mode with ${asBold('-s')} flag.`
)
return
}
console.log(
chalk.default.underline(
`Sandbox templates to ${publish ? 'publish' : 'unpublish'}`
)
)
templates.forEach((e) =>
console.log(
asFormattedSandboxTemplate(
{ ...e, templateID: e.template_id },
e.configPath
)
)
)
process.stdout.write('\n')
if (!opts.yes) {
const confirmed = await confirm(
`Do you really want to ${publish ? 'publish' : 'unpublish'} ${
templates.length === 1 ? 'this template' : 'these templates'
}?\n⚠️ This will make the ${
templates.length === 1 ? 'template' : 'templates'
} ${
publish
? 'public to everyone outside your team'
: 'private to your team'
}`
)
if (!confirmed) {
console.log('Canceled')
return
}
}
await Promise.all(
templates.map(async (e) => {
console.log(
`- ${
publish ? 'Publishing' : 'Unpublishing'
} sandbox template ${asFormattedSandboxTemplate(
{ ...e, templateID: e.template_id },
e.configPath
)}`
)
const names = await publishTemplate(e.template_id, publish)
if (publish && names.length > 0) {
console.log(` Published as: ${asBold(names.join(', '))}`)
}
})
)
process.stdout.write('\n')
} catch (err: any) {
console.error(asFormattedError(err.message))
process.exit(1)
}
}
export const publishCommand = new commander.Command('publish')
.description('publish sandbox template')
.argument(
'[template]',
`specify ${asBold(
'[template]'
)} to publish it. If you dont specify ${asBold(
'[template]'
)} the command will try to publish sandbox template defined by ${asLocal(
'e2b.toml'
)}.`
)
.addOption(pathOption)
.addOption(configOption)
.addOption(selectMultipleOption)
.addOption(teamOption)
.alias('pb')
.option('-y, --yes', 'skip manual publish confirmation')
.action(templateAction.bind(null, true))
export const unPublishCommand = new commander.Command('unpublish')
.description('unpublish sandbox template')
.argument(
'[template]',
`specify ${asBold(
'[template]'
)} to unpublish it. If you don't specify ${asBold(
'[template]'
)} the command will try to unpublish sandbox template defined by ${asLocal(
'e2b.toml'
)}.`
)
.addOption(pathOption)
.addOption(configOption)
.addOption(selectMultipleOption)
.addOption(teamOption)
.alias('upb')
.option('-y, --yes', 'skip manual unpublish confirmation')
.action(templateAction.bind(null, false))
+129
View File
@@ -0,0 +1,129 @@
import * as yup from 'yup'
import * as toml from '@iarna/toml'
import * as fsPromise from 'fs/promises'
import * as fs from 'fs'
import * as path from 'path'
import { asFormattedSandboxTemplate, asLocalRelative } from 'src/utils/format'
export const configName = 'e2b.toml'
function getConfigHeader(config: E2BConfig) {
return `# This is a config for E2B sandbox template.
# You can use template ID (${config.template_id}) ${
config.template_name ? `or template name (${config.template_name}) ` : ''
}to create a sandbox:
# Python SDK
# from e2b import Sandbox, AsyncSandbox
# sandbox = Sandbox.create("${
config.template_name || config.template_id
}") # Sync sandbox
# sandbox = await AsyncSandbox.create("${
config.template_name || config.template_id
}") # Async sandbox
# JS SDK
# import { Sandbox } from 'e2b'
# const sandbox = await Sandbox.create('${
config.template_name || config.template_id
}')
`
}
export const configSchema = yup.object({
template_id: yup.string().required(),
template_name: yup.string().optional(),
dockerfile: yup.string().required(),
start_cmd: yup.string().optional(),
ready_cmd: yup.string().optional(),
cpu_count: yup.number().integer().min(1).optional(),
memory_mb: yup.number().integer().min(128).optional(),
team_id: yup.string().optional(),
})
export type E2BConfig = yup.InferType<typeof configSchema>
interface Migration {
from: string
to: string
}
// List of name migrations from old config format to new one.
// We need to keep this list to be able to migrate old configs to new format.
const migrations: Migration[] = [
{
from: 'id',
to: 'template_id',
},
{
from: 'name',
to: 'template_name',
},
]
function applyMigrations(config: toml.JsonMap, migrations: Migration[]) {
for (const migration of migrations) {
const from = migration.from
const to = migration.to
if (config[from]) {
config[to] = config[from]
delete config[from]
}
}
return config
}
export async function loadConfig(configPath: string) {
const tomlRaw = await fsPromise.readFile(configPath, 'utf-8')
const config = toml.parse(tomlRaw)
const migratedConfig = applyMigrations(config, migrations)
return (await configSchema.validate(migratedConfig)) as E2BConfig
}
export async function saveConfig(
configPath: string,
config: E2BConfig,
overwrite?: boolean
) {
try {
if (!overwrite) {
const configExists = fs.existsSync(configPath)
if (configExists) {
throw new Error(
`Config already exists on path ${asLocalRelative(configPath)}`
)
}
}
const validatedConfig: any = await configSchema.validate(config, {
stripUnknown: true,
})
const tomlRaw = toml.stringify(validatedConfig)
await fsPromise.writeFile(configPath, getConfigHeader(config) + tomlRaw)
} catch (err: any) {
throw new Error(
`E2B sandbox template config ${asFormattedSandboxTemplate(
{
templateID: config.template_id,
},
configPath
)} cannot be saved: ${err.message}`
)
}
}
export async function deleteConfig(configPath: string) {
await fsPromise.unlink(configPath)
}
export function getConfigPath(root: string, configPath?: string) {
if (configPath && path.isAbsolute(configPath)) return configPath
return path.join(root, configPath || configName)
}
+2
View File
@@ -0,0 +1,2 @@
export const defaultDockerfileName = 'e2b.Dockerfile'
export const fallbackDockerfileName = 'Dockerfile'
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env -S node --enable-source-maps
import simpleUpdateNotifier from 'simple-update-notifier'
import * as commander from 'commander'
import * as packageJSON from '../package.json'
import { program } from './commands'
import { commands2md } from './utils/commands2md'
export const pkg = packageJSON
const updateCheck = simpleUpdateNotifier({
pkg,
updateCheckInterval: 1000 * 60 * 60 * 8, // 8 hours
}).catch((e) => {
if (process.env.DEBUG) {
console.error('Update check failed:', e)
}
})
const prog = program.version(
packageJSON.version,
undefined,
'display E2B CLI version'
)
if (process.env.NODE_ENV === 'development') {
prog
.addOption(new commander.Option('-cmd2md').hideHelp())
.on('option:-cmd2md', () => {
commands2md(program.commands as any)
process.exit(0)
})
}
async function main() {
await prog.parseAsync()
await updateCheck
}
main()
+43
View File
@@ -0,0 +1,43 @@
import * as commander from 'commander'
import { asBold, asLocal } from './utils/format'
/**
* Parse a CLI option as a positive integer, rejecting non-numeric values so
* they don't silently become NaN.
*/
export function parsePositiveInt(label: string): (value: string) => number {
return (value) => {
const parsed = Number(value)
if (!Number.isInteger(parsed) || parsed < 1) {
throw new commander.InvalidArgumentError(
`${label} must be a positive integer. You provided ${asLocal(value)}.`
)
}
return parsed
}
}
export const pathOption = new commander.Option(
'-p, --path <path>',
`change root directory where command is executed to ${asBold(
'<path>'
)} directory`
)
export const configOption = new commander.Option(
'--config <e2b-toml>',
`specify path to the E2B config toml. By default E2B tries to find ${asBold(
'./e2b.toml'
)} in root directory. We recommend using the new build system (https://e2b.dev/docs/template/defining-template) that does not use config files.`
)
export const selectMultipleOption = new commander.Option(
'-s, --select',
'select sandbox template from interactive list'
)
export const teamOption = new commander.Option(
'-t, --team <team-id>',
'specify the team ID that the operation will be associated with. You can find team ID in the team settings in the E2B dashboard (https://e2b.dev/dashboard?tab=team).'
)
@@ -0,0 +1,21 @@
import asyncio
from e2b import AsyncTemplate, default_build_logger
from template import template
async def main():
await AsyncTemplate.build(
template,
"{{name}}",
{{#if cpuCount}}
cpu_count={{cpuCount}},
{{/if}}
{{#if memoryMB}}
memory_mb={{memoryMB}},
{{/if}}
on_build_logs=default_build_logger(),
)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,16 @@
from e2b import Template, default_build_logger
from template import template
if __name__ == "__main__":
Template.build(
template,
"{{name}}",
{{#if cpuCount}}
cpu_count={{cpuCount}},
{{/if}}
{{#if memoryMB}}
memory_mb={{memoryMB}},
{{/if}}
on_build_logs=default_build_logger(),
)
@@ -0,0 +1,36 @@
from e2b import {{#if isAsync}}AsyncTemplate{{else}}Template{{/if}}
template = (
{{#if isAsync}}AsyncTemplate{{else}}Template{{/if}}()
{{#if fromImage}}
.from_image("{{{fromImage}}}")
{{/if}}
{{#each steps}}
{{#eq type "WORKDIR"}}
.set_workdir("{{{args.[0]}}}")
{{/eq}}
{{#eq type "USER"}}
.set_user("{{{args.[0]}}}")
{{/eq}}
{{#eq type "ENV"}}
.set_envs({
{{#each envVars}}
"{{{@key}}}": "{{{this}}}",
{{/each}}
})
{{/eq}}
{{#eq type "RUN"}}
.run_cmd("{{{args.[0]}}}")
{{/eq}}
{{#eq type "COPY"}}
.copy("{{{src}}}", "{{{dest}}}")
{{/eq}}
{{/each}}
{{#if startCmd}}
{{#if readyCmd}}
.set_start_cmd("sudo {{{escapeDoubleQuotes startCmd}}}", "{{{escapeDoubleQuotes readyCmd}}}")
{{/if}}
{{else if readyCmd}}
.set_ready_cmd("sudo {{{escapeDoubleQuotes readyCmd}}}")
{{/if}}
)
+99
View File
@@ -0,0 +1,99 @@
# {{name}} - E2B Sandbox Template
This is an E2B sandbox template that allows you to run code in a controlled environment.
## 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))
{{#if isTypeScript}}- Node.js and npm/yarn (or similar) installed{{else if isPython}}- Python installed{{/if}}
## Configuration
1. Create a `.env` file in your project root or set the environment variable:
```
E2B_API_KEY=your_api_key_here
```
## Installing Dependencies
```bash
{{#if isTypeScript}}
npm install e2b
{{else if isPython}}
pip install e2b
{{/if}}
```
## Building the Template
```bash
{{#if isTypeScript}}
# For development
npm run e2b:build:dev
# For production
npm run e2b:build:prod
{{else if isPython}}
# For development
make e2b:build:dev
# For production
make e2b:build:prod
{{/if}}
```
## Using the Template in a Sandbox
Once your template is built, you can use it in your E2B sandbox:
{{#if isTypeScript}}
```typescript
import { Sandbox } from 'e2b'
// Create a new sandbox instance
const sandbox = await Sandbox.create('{{name}}')
// Your sandbox is ready to use!
console.log('Sandbox created successfully')
```
{{else if isPythonSync}}
```python
from e2b import Sandbox
# Create a new sandbox instance
sandbox = Sandbox.create('{{name}}')
# Your sandbox is ready to use!
print('Sandbox created successfully')
```
{{else if isPythonAsync}}
```python
from e2b import AsyncSandbox
import asyncio
async def main():
# Create a new sandbox instance
sandbox = await AsyncSandbox.create('{{name}}')
# Your sandbox is ready to use!
print('Sandbox created successfully')
# Run the async function
asyncio.run(main())
```
{{/if}}
## Template Structure
- `{{templateFile}}` - Defines the sandbox template configuration
- `{{buildDevFile}}` - Builds the template for development
- `{{buildProdFile}}` - Builds the template for production
## Next Steps
1. Customize the template in `{{templateFile}}` to fit your needs
2. Build the template using one of the methods above
3. Use the template in your E2B sandbox code
4. Check out the [E2B documentation](https://e2b.dev/docs) for more advanced usage
@@ -0,0 +1,16 @@
import { Template, defaultBuildLogger } from 'e2b'
import { template } from './template'
async function main() {
await Template.build(template, '{{name}}', {
{{#if cpuCount}}
cpuCount: {{cpuCount}},
{{/if}}
{{#if memoryMB}}
memoryMB: {{memoryMB}},
{{/if}}
onBuildLogs: defaultBuildLogger(),
});
}
main().catch(console.error);
@@ -0,0 +1,34 @@
import { Template } from 'e2b'
export const template = Template()
{{#if fromImage}}
.fromImage('{{{fromImage}}}')
{{/if}}
{{#each steps}}
{{#eq type "WORKDIR"}}
.setWorkdir('{{{args.[0]}}}')
{{/eq}}
{{#eq type "USER"}}
.setUser('{{{args.[0]}}}')
{{/eq}}
{{#eq type "ENV"}}
.setEnvs({
{{#each envVars}}
'{{{@key}}}': '{{{this}}}',
{{/each}}
})
{{/eq}}
{{#eq type "RUN"}}
.runCmd('{{{args.[0]}}}')
{{/eq}}
{{#eq type "COPY"}}
.copy('{{{src}}}', '{{{dest}}}')
{{/eq}}
{{/each}}
{{#if startCmd}}
{{#if readyCmd}}
.setStartCmd('sudo {{{escapeQuotes startCmd}}}', '{{{escapeQuotes readyCmd}}}')
{{/if}}
{{else if readyCmd}}
.setReadyCmd('sudo {{{escapeQuotes readyCmd}}}')
{{/if}}
+108
View File
@@ -0,0 +1,108 @@
import * as e2b from 'e2b'
const FLUSH_INPUT_INTERVAL_MS = 10
function getStdoutSize() {
return {
cols: process.stdout.columns,
rows: process.stdout.rows,
}
}
export async function spawnConnectedTerminal(sandbox: e2b.Sandbox) {
// Clear local terminal emulator before starting terminal
// process.stdout.write('\x1b[2J\x1b[0f')
process.stdin.setRawMode(true)
process.stdout.setEncoding('utf-8')
const terminalSession = await sandbox.pty.create({
onData: (data) => {
process.stdout.write(data)
},
...getStdoutSize(),
timeoutMs: 0,
})
const inputQueue = new BatchedQueue<Buffer>(async (batch) => {
const combined = Buffer.concat(batch)
await sandbox.pty.sendInput(terminalSession.pid, combined)
}, FLUSH_INPUT_INTERVAL_MS)
const resizeListener = process.stdout.on('resize', () =>
sandbox.pty.resize(terminalSession.pid, getStdoutSize())
)
const stdinListener = process.stdin.on('data', (data) => {
inputQueue.push(data)
})
inputQueue.start()
// Wait for terminal session to finish
try {
await terminalSession.wait()
} catch (err: any) {
if (err instanceof e2b.CommandExitError) {
if (err.exitCode === -1 && err.error === 'signal: killed') {
return
}
if (err.exitCode === 130) {
console.warn('Terminal session was killed by user')
return
}
}
throw err
} finally {
// Cleanup
process.stdout.write('\n')
resizeListener.destroy()
stdinListener.destroy()
await inputQueue.stop()
process.stdin.setRawMode(false)
}
}
class BatchedQueue<T> {
private queue: T[] = []
private isFlushing = false
private intervalId?: NodeJS.Timeout
constructor(
private flushHandler: (batch: T[]) => Promise<void>,
private flushIntervalMs: number
) {}
push(item: T) {
this.queue.push(item)
}
start() {
this.intervalId = setInterval(async () => {
if (this.isFlushing) return
this.isFlushing = true
await this.flush()
this.isFlushing = false
}, this.flushIntervalMs)
}
async stop() {
if (this.intervalId) {
clearInterval(this.intervalId)
this.intervalId = undefined
}
await this.flush()
}
private async flush() {
if (this.queue.length === 0) return
const batch = this.queue.splice(0, this.queue.length)
try {
await this.flushHandler(batch)
} catch (err) {
console.error('Error sending input:', err)
}
}
}
+105
View File
@@ -0,0 +1,105 @@
import * as os from 'os'
import * as path from 'path'
import * as fs from 'fs'
/**
* User configuration stored in ~/.e2b/config.json
*/
export interface UserIdentity {
email: string
}
export interface UserOAuth {
token_endpoint: string
revoke_endpoint: string
client_id: string
}
export interface UserTokens {
access_token: string
refresh_token: string
}
export interface UserConfig {
version: 1
identity: UserIdentity
oauth: UserOAuth
tokens: UserTokens
last_refresh: string
teamName: string
teamId: string
teamApiKey: string
dockerProxySet?: boolean
}
type UnknownRecord = Record<string, unknown>
export const USER_CONFIG_PATH = path.join(os.homedir(), '.e2b', 'config.json') // TODO: Keep in Keychain
export const DEPRECATED_USER_CONFIG_MESSAGE =
'Your CLI authentication config is deprecated. You have been signed out. Please run `e2b auth login` again.'
export const DOCS_BASE =
process.env.E2B_DOCS_BASE ||
`https://${process.env.E2B_DOMAIN || 'e2b.dev'}/docs`
export const DASHBOARD_BASE =
process.env.E2B_DASHBOARD_BASE ||
`https://${process.env.E2B_DOMAIN || 'e2b.dev'}/dashboard`
export const SANDBOX_INSPECT_URL = (sandboxId: string) =>
`${DASHBOARD_BASE}/inspect/sandbox/${sandboxId}`
export function getUserConfig(): UserConfig | null {
if (!fs.existsSync(USER_CONFIG_PATH)) return null
const config = JSON.parse(fs.readFileSync(USER_CONFIG_PATH, 'utf8'))
if (!isUserConfig(config)) {
fs.unlinkSync(USER_CONFIG_PATH)
console.error(DEPRECATED_USER_CONFIG_MESSAGE)
return null
}
return config
}
function isObject(value: unknown): value is UnknownRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function isString(value: unknown): value is string {
return typeof value === 'string'
}
function isUserConfig(config: unknown): config is UserConfig {
if (!isObject(config)) return false
if (config.version !== 1) return false
return (
isObject(config.identity) &&
isString(config.identity.email) &&
isObject(config.oauth) &&
isString(config.oauth.token_endpoint) &&
isString(config.oauth.revoke_endpoint) &&
isString(config.oauth.client_id) &&
isObject(config.tokens) &&
isString(config.tokens.access_token) &&
isString(config.tokens.refresh_token)
)
}
export function getConfigRefreshTimestamp(): string {
return new Date().toISOString()
}
/**
* Write user config to disk with restrictive file permissions.
* The config directory is restricted to the owner and the config file is
* written as owner-readable/writable only because it contains credentials.
*/
export function writeUserConfig(configPath: string, config: UserConfig): void {
const dir = path.dirname(configPath)
fs.mkdirSync(dir, { recursive: true, mode: 0o700 })
fs.chmodSync(dir, 0o700)
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 })
fs.chmodSync(configPath, 0o600)
}
+85
View File
@@ -0,0 +1,85 @@
import { Command } from 'commander'
import fs from 'fs'
import json2md from 'json2md'
import path from 'path'
/**
* Converts command objects to Markdown documentation.
* This function takes an array of command objects and generates a structured
* Markdown document describing each command, its usage, options, and subcommands.
* @returns A string containing the entire markdown documentation for all commands.
*/
export function commands2md(commands: Command[]): void {
const outputDir = 'sdk_ref'
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true })
}
function commandToMd(
command: any,
parentName: string = ''
): [string, string] {
const commandName = command.name() as string
const fullName = parentName ? `${parentName} ${commandName}` : commandName
const mdStructure = [
{ h2: `e2b ${fullName}` },
{ p: command.description() },
{ h3: 'Usage' },
{
code: {
language: 'bash',
content: `e2b ${fullName} ${command.usage()}`,
},
},
...(command.options.length > 0
? [
{ h3: 'Options' },
{
ul: command.options.map(
(y: any) =>
`\`${y.flags}: ${y.description} ${
y.defaultValue !== undefined
? `[default: ${y.defaultValue}]`
: ''
}\``
),
},
]
: []),
]
let mdContent = json2md(mdStructure)
// Process subcommands
command.commands.forEach((subcommand: any) => {
const [, subMdContent] = commandToMd(subcommand, fullName)
mdContent += subMdContent + '\n\n'
})
// Clean the mdContent from terminal colors and escape HTML characters
mdContent = mdContent
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/\[1m/g, '')
.replace(/\[22m/g, '')
.replace(/\[34m/g, '')
.replace(/\[39m/g, '')
.replace(/\[38;2;255;183;102m/g, '')
return [fullName, mdContent]
}
commands.forEach((command: any) => {
try {
const [commandName, mdContent] = commandToMd(command)
const fileName = `${commandName}.md`
const filePath = path.join(outputDir, fileName)
fs.writeFileSync(filePath, mdContent)
console.log(`Generated documentation for ${commandName} at ${filePath}`)
} catch (error) {
console.error(`Error processing command: ${command.name()}`)
console.error(error)
}
})
}
+13
View File
@@ -0,0 +1,13 @@
export async function confirm(text: string, defaultAnswer = false) {
const inquirer = await import('inquirer')
const confirmAnswers = await inquirer.default.prompt([
{
name: 'confirm',
type: 'confirm',
default: defaultAnswer,
message: text,
},
])
return confirmAnswers['confirm'] as boolean
}
+72
View File
@@ -0,0 +1,72 @@
import status from 'statuses'
/**
* Thrown when a request to E2B API occurs.
*/
export class E2BRequestError extends Error {
constructor(message: any) {
super(message)
this.name = 'E2BRequestError'
}
}
type E2BResponseError = { code?: number; message?: string }
type E2BResponse<TData> =
| {
data: TData
error?: undefined
}
| {
data?: undefined
error: E2BResponseError
}
function throwE2BRequestError(error: E2BResponseError, errMsg?: string): never {
let message: string
const code = error.code ?? 0
switch (code) {
case 400:
message = 'bad request'
break
case 401:
message = 'unauthorized'
break
case 403:
message = 'forbidden'
break
case 404:
message = 'not found'
break
case 500:
message = 'internal server error'
break
default:
message = status.message[code] || 'unknown error'
break
}
throw new E2BRequestError(
`${errMsg && `${errMsg}: `}[${code}] ${message && `${message}: `}${
error.message ?? 'no message'
}`
)
}
export function handleE2BRequestError(
res: { error: E2BResponseError },
errMsg?: string
): never
export function handleE2BRequestError<TData>(
res: E2BResponse<TData>,
errMsg?: string
): asserts res is { data: TData; error?: undefined }
export function handleE2BRequestError(
res: E2BResponse<unknown>,
errMsg?: string
) {
if (!res.error) {
return
}
throwE2BRequestError(res.error, errMsg)
}
+12
View File
@@ -0,0 +1,12 @@
import * as path from 'path'
export function getRoot(templatePath?: string) {
const defaultPath = process.cwd()
if (!templatePath) return defaultPath
if (path.isAbsolute(templatePath)) return templatePath
return path.resolve(defaultPath, templatePath)
}
export function cwdRelative(absolutePath: string) {
return path.relative(process.cwd(), absolutePath)
}

Some files were not shown because too many files have changed in this diff Show More