spawn/codesandbox/lib/common.sh
A 9336998168
fix(ux): add post-session summary to 10 exec-based cloud providers (#1056)
Users on exec-based clouds (Fly, Render, Koyeb, Northflank, Railway,
Modal, Daytona, E2B, CodeSandbox, GitHub Codespaces) got no warning
when their session ended that their service was still running and
incurring charges. This adds:

- _show_exec_post_session_summary() in shared/common.sh for non-SSH
  providers that use CLI exec commands instead of direct SSH
- SPAWN_DASHBOARD_URL for all 10 exec-based clouds so users get
  actionable dashboard links
- Post-session summary calls in each cloud's interactive_session()
- 33 new tests covering the exec post-session summary feature

Agent: ux-engineer

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-14 00:38:10 -05:00

257 lines
9.2 KiB
Bash

#!/bin/bash
# Common bash functions for CodeSandbox spawn scripts
# Uses CodeSandbox SDK + CLI — https://codesandbox.io
# Sandboxes are Firecracker microVMs with ~2 second start times
# No SSH — uses CodeSandbox SDK for exec
# Bash safety flags
set -eo pipefail
# ============================================================
# Provider-agnostic functions
# ============================================================
# Source shared provider-agnostic functions (local or remote fallback)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
if [[ -n "${SCRIPT_DIR}" && -f "${SCRIPT_DIR}/../../shared/common.sh" ]]; then
source "${SCRIPT_DIR}/../../shared/common.sh"
else
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/shared/common.sh)"
fi
# Note: Provider-agnostic functions (logging, OAuth, browser, nc_listen) are now in shared/common.sh
# ============================================================
# CodeSandbox specific functions
# ============================================================
SPAWN_DASHBOARD_URL="https://codesandbox.io/dashboard"
ensure_codesandbox_cli() {
if ! command -v node &>/dev/null; then
log_step "Installing Node.js..."
if command -v curl &>/dev/null; then
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && apt-get install -y nodejs 2>/dev/null || {
log_error "Failed to install Node.js automatically"
log_error ""
log_error "Please install Node.js manually:"
log_error " Ubuntu/Debian: curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo bash - && sudo apt-get install -y nodejs"
log_error " macOS: brew install node"
log_error " Fedora/RHEL: sudo dnf install nodejs"
return 1
}
else
log_error "Node.js is required but not installed"
log_error "Install Node.js: https://nodejs.org/"
return 1
fi
fi
if ! npm list -g @codesandbox/sdk &>/dev/null; then
log_step "Installing CodeSandbox SDK/CLI..."
npm install -g @codesandbox/sdk 2>/dev/null || {
log_error "Failed to install CodeSandbox SDK"
log_error ""
log_error "Manual installation:"
log_error " npm install -g @codesandbox/sdk"
return 1
}
fi
log_info "CodeSandbox SDK/CLI ready"
}
test_codesandbox_token() {
# Test token by attempting to list sandboxes (lightweight API call)
local test_output
test_output=$(CSB_API_KEY="${CSB_API_KEY}" npx -y @codesandbox/sdk sandboxes list 2>&1)
local exit_code=$?
if [[ ${exit_code} -ne 0 ]]; then
if echo "${test_output}" | grep -qi "unauthorized\|invalid.*key\|authentication\|401"; then
log_error "Invalid API key"
log_error "How to fix:"
log_warn " 1. Get a new API key at: https://codesandbox.io/t/api"
log_warn " 2. Enable all scopes when creating the key"
log_warn " 3. Export it as: export CSB_API_KEY=your-key-here"
return 1
fi
fi
return 0
}
ensure_codesandbox_token() {
ensure_api_token_with_provider \
"CodeSandbox" \
"CSB_API_KEY" \
"${HOME}/.config/spawn/codesandbox.json" \
"https://codesandbox.io/t/api" \
"test_codesandbox_token"
}
get_server_name() {
get_resource_name "CODESANDBOX_SANDBOX_NAME" "Enter sandbox name: "
}
# Run a JS snippet that uses the CodeSandbox SDK.
# The snippet receives `sdk` (authenticated SDK instance) in scope.
# All extra env vars are forwarded so callers can pass data safely.
# Usage: _csb_sdk_eval 'await sdk.sandboxes.list()'
_csb_sdk_eval() {
local js_body="${1}"
node -e "
const { CodeSandbox } = require('@codesandbox/sdk');
const sdk = new CodeSandbox(process.env.CSB_API_KEY);
(async () => {
try { ${js_body} }
catch (e) { console.error('ERROR:', e.message); process.exit(1); }
})();
"
}
# Connect to an existing sandbox, run a command, stream output.
# Receives sandbox ID via _CSB_SB_ID env var, command via _CSB_CMD.
_csb_run_cmd() {
_csb_sdk_eval "
const sb = await sdk.sandboxes.get(process.env._CSB_SB_ID);
const c = await sb.connect();
const r = await c.commands.run(process.env._CSB_CMD);
if (r.output) process.stdout.write(r.output);
if (r.stderr) process.stderr.write(r.stderr);
process.exit(r.exitCode || 0);
"
}
# Invoke Node.js script to create sandbox via SDK
# SECURITY: Pass name and template via environment variables to prevent injection
_invoke_codesandbox_create() {
local name="${1}"
local template="${2:-base}"
CSB_API_KEY="${CSB_API_KEY}" _CSB_NAME="${name}" _CSB_TEMPLATE="${template}" \
_csb_sdk_eval "
const sb = await sdk.sandboxes.create({
name: process.env._CSB_NAME,
template: process.env._CSB_TEMPLATE || 'base'
});
console.log(sb.id);
" 2>&1
}
create_server() {
local name="${1}"
local template="${CODESANDBOX_TEMPLATE:-base}"
log_step "Creating CodeSandbox sandbox '${name}'..."
local output
output=$(_invoke_codesandbox_create "${name}" "${template}")
local exit_code=$?
if [[ ${exit_code} -ne 0 ]] || [[ -z "${output}" ]] || [[ "${output}" =~ ERROR ]]; then
log_error "Failed to create sandbox"
log_error ""
if [[ -n "${output}" ]]; then
log_error "Error details: ${output}"
fi
log_error ""
log_error "Possible causes:"
log_error " - Invalid API key or expired authentication"
log_error " - Insufficient quota or credits (check: https://codesandbox.io/settings)"
log_error " - Network connectivity issues"
log_error " - Invalid sandbox name or template"
return 1
fi
CODESANDBOX_SANDBOX_ID="${output}"
export CODESANDBOX_SANDBOX_ID
log_info "Sandbox created: ID=${CODESANDBOX_SANDBOX_ID}"
}
wait_for_cloud_init() {
log_step "Installing tools in sandbox..."
# CodeSandbox comes with Node.js pre-installed
run_server "curl -fsSL https://bun.sh/install | bash" >/dev/null 2>&1 || true
run_server 'echo "export PATH=\"\${HOME}/.bun/bin:\${PATH}\"" >> ~/.bashrc' >/dev/null 2>&1 || true
log_info "Tools installed"
}
# Validate CodeSandbox sandbox ID format
validate_sandbox_id() {
local sid="${1}"
if [[ ! "${sid}" =~ ^[a-zA-Z0-9_-]+$ ]]; then
log_error "Invalid CODESANDBOX_SANDBOX_ID format: expected alphanumeric with dashes/underscores"
return 1
fi
}
# Execute command via CodeSandbox SDK
run_server() {
local cmd="${1}"
validate_sandbox_id "${CODESANDBOX_SANDBOX_ID}" || return 1
# SECURITY: Pass sandbox ID and command via environment variables to prevent injection
CSB_API_KEY="${CSB_API_KEY}" _CSB_SB_ID="${CODESANDBOX_SANDBOX_ID}" _CSB_CMD="${cmd}" _csb_run_cmd
}
upload_file() {
local local_path="${1}"
local remote_path="${2}"
# SECURITY: Strict allowlist validation — only safe path characters
if [[ ! "${remote_path}" =~ ^[a-zA-Z0-9/_.~-]+$ ]]; then
log_error "Invalid remote path (must contain only alphanumeric, /, _, ., ~, -): ${remote_path}"
return 1
fi
local content
content=$(base64 -w0 "${local_path}" 2>/dev/null || base64 "${local_path}")
# SECURITY: Use SDK filesystem API via env vars — no shell interpolation
validate_sandbox_id "${CODESANDBOX_SANDBOX_ID}" || return 1
CSB_API_KEY="${CSB_API_KEY}" _CSB_SB_ID="${CODESANDBOX_SANDBOX_ID}" \
_CSB_REMOTE_PATH="${remote_path}" _CSB_CONTENT="${content}" \
_csb_sdk_eval "
const fs = require('fs');
const path = require('path');
const sb = await sdk.sandboxes.get(process.env._CSB_SB_ID);
const c = await sb.connect();
const remotePath = process.env._CSB_REMOTE_PATH;
const dir = path.dirname(remotePath);
if (dir !== '.' && dir !== '/') {
await c.commands.run('mkdir -p ' + dir);
}
const content = Buffer.from(process.env._CSB_CONTENT, 'base64');
await c.fs.writeFile(remotePath, content);
"
}
interactive_session() {
log_info "Starting interactive session..."
log_step "For a full terminal, open your sandbox at: https://codesandbox.io/dashboard"
local session_exit=0
run_server "$1" || session_exit=$?
SERVER_NAME="${CODESANDBOX_SANDBOX_ID:-}" _show_exec_post_session_summary
return "${session_exit}"
}
destroy_server() {
local sandbox_id="${1:-${CODESANDBOX_SANDBOX_ID}}"
validate_sandbox_id "${sandbox_id}" || return 1
log_step "Shutting down sandbox..."
# SECURITY: Pass sandbox ID via environment variable
CSB_API_KEY="${CSB_API_KEY}" _CSB_SB_ID="${sandbox_id}" \
_csb_sdk_eval "
await sdk.sandboxes.shutdown(process.env._CSB_SB_ID);
console.log('Sandbox shut down');
" 2>/dev/null || true
log_info "Sandbox shut down"
}
list_servers() {
CSB_API_KEY="${CSB_API_KEY}" \
_csb_sdk_eval "
const sbs = await sdk.sandboxes.list();
sbs.forEach(sb => console.log(sb.id + ' ' + (sb.name || 'unnamed')));
" 2>/dev/null || echo "No sandboxes found"
}