@devfellowship/sandbox-cli
v1.2.0
Published
CLI for managing DevFellowship sandbox environments
Downloads
29
Maintainers
Readme
@devfellowship/sandbox-cli
CLI for managing DevFellowship preview sandbox environments. It's a thin Commander-based wrapper over the dfl-sandbox-manager daemon HTTP API — provision a per-branch preview of any DFL app, run migrations, tail container logs, SSH into the app container from your editor, and tear it down when you're done.
Install
As a one-off (recommended — avoids binary-name conflicts with other tools that ship a sandbox command):
npx @devfellowship/sandbox-cli <command>Globally:
npm i -g @devfellowship/sandbox-cli
sandbox <command>The installed binary is named
sandbox. If you already have another tool on your$PATHthat uses the same name, prefernpxor alias it.
Requires Node.js >=20.
Configuration
Resolution precedence (first one wins): env var → ~/.config/sandbox-cli/config.json → built-in default. The CLI also reads .env.local/.env from the repo root when run inside a checkout (legacy fallback for CI scripts).
| Key | Env var | Default | Purpose |
|---|---|---|---|
| daemonUrl | DAEMON_URL | https://sandbox-api.preview.devfellowship.com | Base URL of the sandbox-manager daemon. As of v1.2.0 the default points at production — set to http://localhost:3500 for local dev. |
| sandboxToken | SANDBOX_TOKEN | (empty) | Bearer token sent as Authorization: Bearer <token>. Required for everything except health. See Token resolution below. |
| computePublicIp | COMPUTE_PUBLIC_IP | (empty) | Public IP of the compute host (used by connect/legacy SSH paths) |
| sshKeyPath | SSH_KEY_PATH | ./creds/sandbox-admin | Private key for the legacy ssh helper (connect only — logs/status are now HTTP-only) |
| sshUser | SSH_USER | root | User for the legacy ssh helper |
| previewDomain | PREVIEW_DOMAIN | preview.devfellowship.com | Domain under which sandboxes are exposed |
Persisted config is written to ~/.config/sandbox-cli/config.json (mode 0600; directory 0700).
sandbox config — manage persisted config (v1.2.0)
# Set values that survive across shells
sandbox config set daemonUrl https://sandbox-api.preview.devfellowship.com
sandbox config set sandboxToken dfl_sk_…
# Show the effective config (token is masked to first 8 chars)
sandbox config show
sandbox config show --jsonToken resolution (v1.2.0)
When the CLI needs a SANDBOX_TOKEN it tries, in order:
- Environment variable
SANDBOX_TOKEN. sandboxTokenfrom~/.config/sandbox-cli/config.json.- Infisical auto-fetch — if
INFISICAL_MACHINE_TOKENis set,GET {INFISICAL_API_URL}/api/v3/secrets/raw/SANDBOX_TOKEN?secretPath=/apps/paperclip&environment=prod. The fetched value is cached in process memory only, never persisted.
If none resolve a token, the CLI fails loud with the precedence list — it does not silently fall back to unauthenticated calls.
Remote use
# One-time setup
sandbox config set sandboxToken dfl_sk_…
sandbox listWithout a token, authenticated routes (list, provision, destroy, migrate, rebuild, refetch, exec, cp, env, sql, logs, status, secrets, functions) return 401 UNAUTHORIZED. health works unauthenticated.
Commands
health — check daemon status
npx @devfellowship/sandbox-cli healthPings GET /health on the daemon and prints uptime and active sandbox count.
list (alias ls) — list active sandboxes
npx @devfellowship/sandbox-cli list
npx @devfellowship/sandbox-cli ls --jsonprovision — create a sandbox for a branch
# Uses the daemon's default dev command and port
npx @devfellowship/sandbox-cli provision dfl-learn feat/my-branch
# Custom dev command and port
npx @devfellowship/sandbox-cli provision dfl-learn feat/my-branch \
--dev-command "npm run dev -- --host 0.0.0.0 --port 5173" \
--port 5173Accepts a bare repo name (dfl-learn), org/repo, or a full GitHub URL.
destroy — tear down a sandbox by branch
npx @devfellowship/sandbox-cli destroy feat/my-branch
npx @devfellowship/sandbox-cli destroy feat/my-branch -y # skip confirmationlogs — tail container logs (HTTP-only as of v1.2.0)
# Last 100 lines from the app container
npx @devfellowship/sandbox-cli logs my-sandbox-slug
# Specific service (app / rest / auth)
npx @devfellowship/sandbox-cli logs my-sandbox-slug rest --tail 500
# Follow mode (polls /v1/sandboxes/:slug/logs?since=<iso> every 2s)
npx @devfellowship/sandbox-cli logs my-sandbox-slug -ftail — alias for logs --follow (v1.2.0)
npx @devfellowship/sandbox-cli tail my-sandbox-slug reststatus — sandbox status (HTTP-only as of v1.2.0)
npx @devfellowship/sandbox-cli status my-sandbox-slug # registry only
npx @devfellowship/sandbox-cli status my-sandbox-slug -v # + containers + 20-line logs of app/rest/authexec — run a command inside the app container (v1.2.0)
npx @devfellowship/sandbox-cli exec my-sandbox-slug cat /app/package.json
npx @devfellowship/sandbox-cli exec my-sandbox-slug --timeout 60000 npm run typecheckArgument-mode only: shell metachars (>, <, |, backtick, $()) are rejected client- and server-side. Upload a script via cp and exec it instead. Exit code mirrors the remote command. Requires write scope.
cp — copy files between local and a sandbox (v1.2.0)
# Upload — must target /app/<…> (use --unsafe-path to override)
npx @devfellowship/sandbox-cli cp ./local.txt my-sandbox:/app/foo.txt
# Download
npx @devfellowship/sandbox-cli cp my-sandbox:/app/package.json ./package.json
# Outside /app/ (rare — kept around for support flows)
npx @devfellowship/sandbox-cli cp ./prod.crt my-sandbox:/etc/ssl/prod.crt --unsafe-path10 MB cap. Path traversal (..) is rejected on both client and server.
env — print scrubbed env (v1.2.0)
npx @devfellowship/sandbox-cli env my-sandbox-slug
npx @devfellowship/sandbox-cli env my-sandbox-slug --jsonDaemon allowlists VITE_*, NEXT_PUBLIC_*, SUPABASE_*, POWERSYNC_*, NODE_ENV, PORT, then strips anything ending in _SECRET/_TOKEN/_KEY (except _PUBLIC_KEY/_ANON_KEY). SUPABASE_SERVICE_ROLE_KEY is never returned.
sql — run SQL via PostgREST roles (v1.2.0)
# Anonymous SELECT (read scope)
npx @devfellowship/sandbox-cli sql my-sandbox --query 'select id, email from public.users limit 5'
# Authenticated user role
npx @devfellowship/sandbox-cli sql my-sandbox --role authenticated --file ./check.sql
# service_role (admin-equivalent — write scope required)
npx @devfellowship/sandbox-cli sql my-sandbox --role service_role --query 'select count(*) from auth.users'
# JSON output for scripting
npx @devfellowship/sandbox-cli sql my-sandbox --query 'select 1' --jsonThe first SQL token decides the required scope: SELECT/WITH need read; anything else (or --role service_role) needs write.
migrate — run SQL against a sandbox DB
# Inline SQL
npx @devfellowship/sandbox-cli migrate my-sandbox-slug --sql "create schema foo;"
# From a file
npx @devfellowship/sandbox-cli migrate my-sandbox-slug --file ./supabase/migrations/0001.sql
# With a source label for audit
npx @devfellowship/sandbox-cli migrate my-sandbox-slug --file 0001.sql --source claude-mainmigrations <slug> (sibling command) prints the migration log for a sandbox.
rebuild — rebuild the app container (fresh npm ci)
npx @devfellowship/sandbox-cli rebuild my-sandbox-slugrefetch — git fetch + reset --hard the current branch in the app container
npx @devfellowship/sandbox-cli refetch my-sandbox-slugfunctions — deploy and list edge functions (v1.1.0)
Manage Supabase-style edge functions on a sandbox without destroying
and re-provisioning. Functions land at /functions/v1/<name> on the
sandbox's preview URL.
# Auto-deploy from repo-contract.yaml (running app-<slug> container)
npx @devfellowship/sandbox-cli functions deploy my-sandbox-slug
# Explicit GitHub source — public or private (uses GH_TOKEN / `gh auth token`)
npx @devfellowship/sandbox-cli functions deploy my-sandbox-slug \
github:devfellowship/dfl-schema:supabase/functions/dfl-lesson-studio-shotstack-proxy
# A specific ref (branch/tag/sha):
npx @devfellowship/sandbox-cli functions deploy my-sandbox-slug \
github:devfellowship/dfl-schema@some-branch:supabase/functions/foo
# Local directory
npx @devfellowship/sandbox-cli functions deploy my-sandbox-slug ./supabase/functions/my-fn
# Override the deployed name (defaults to last path segment)
npx @devfellowship/sandbox-cli functions deploy my-sandbox-slug ./local/fn --name renamed-fn
# List what's deployed
npx @devfellowship/sandbox-cli functions list my-sandbox-slugIf the function imports from a sibling _shared/ directory (the
dfl-schema convention), the CLI auto-includes it in the upload.
secrets — manage sandbox-scoped Infisical secrets (v1.1.0)
Slug-scoped wrapper over Infisical. Values are never displayed —
the CLI only confirms which keys exist and which scope they come from
(shared, app, sandbox). Writes always land at
/sandboxes/<slug>/; the daemon refuses to touch other paths.
# List keys visible to the sandbox (key + scope, masked status)
npx @devfellowship/sandbox-cli secrets list my-sandbox-slug
# KEY SCOPE STATUS
# SENTRY_DSN shared ••••
# SHOTSTACK_API_KEY app ••••
# DEBUG_FORCE_RENDER_FAIL sandbox ••••
# Confirm one key — exits 0 if set, 1 if missing. NEVER prints the value.
npx @devfellowship/sandbox-cli secrets get my-sandbox-slug SHOTSTACK_API_KEY
# SHOTSTACK_API_KEY (set, scope: app)
# Set one or more keys — auto-restarts the edge container so the new
# env is picked up on the next request.
npx @devfellowship/sandbox-cli secrets set my-sandbox-slug \
SHOTSTACK_API_KEY=ssk_test_… \
SHOTSTACK_ENV=stage
# Delete a key from /sandboxes/<slug>/. Refuses to delete inherited
# keys (those that come from /shared/ or /apps/<repo>/).
npx @devfellowship/sandbox-cli secrets delete my-sandbox-slug SHOTSTACK_API_KEYKeys matching production patterns are rejected by the daemon:
PROD_*, *PRODUCTION*, CLOUDFLARE_API_TOKEN, SSH_KEY*,
INFISICAL_*. The daemon's Infisical token is scoped to the
sandbox env only — production-tier secrets are unreachable from
the CLI by design.
connect — SSH config + open Cursor / VS Code into the app container
# Auto-detect editor (cursor preferred, falls back to code), update ~/.ssh/config, open
npx @devfellowship/sandbox-cli connect my-sandbox-slug
# Explicit editor
npx @devfellowship/sandbox-cli connect my-sandbox-slug --cursor
npx @devfellowship/sandbox-cli connect my-sandbox-slug --code
# Just SSH into the container
npx @devfellowship/sandbox-cli connect my-sandbox-slug --ssh
# Update SSH config only (no editor/ssh)
npx @devfellowship/sandbox-cli connect my-sandbox-slug --config-onlyUpserts a block in ~/.ssh/config with a <slug>.sandbox host alias; it only touches blocks between its own markers so your other SSH entries are left alone.
Related
- Daemon + full architecture:
devfellowship/dfl-sandbox-manager - Sister CLI for Supabase migrations/seed:
@devfellowship/db
License
MIT © DevFellowship
