From 13e97e2c71254de7a60afb5495b21064c4bc4241 Mon Sep 17 00:00:00 2001 From: simosmik Date: Tue, 14 Apr 2026 15:18:02 +0000 Subject: [PATCH] feat: adding docker sandbox environments --- README.de.md | 11 + README.ja.md | 11 + README.ko.md | 14 +- README.md | 48 ++-- README.ru.md | 13 +- README.zh-CN.md | 14 +- docker/README.md | 173 +++++++++----- docker/claude-code/Dockerfile | 4 +- docker/codex/Dockerfile | 4 +- docker/gemini/Dockerfile | 4 +- docker/shared/install-cloudcli.sh | 11 +- docker/shared/start-cloudcli.sh | 9 +- server/cli.js | 365 +++++++++++++++++++++++++++++- 13 files changed, 578 insertions(+), 103 deletions(-) diff --git a/README.de.md b/README.de.md index 4e163781..4e354020 100644 --- a/README.de.md +++ b/README.de.md @@ -76,6 +76,8 @@ Der schnellste Einstieg – keine lokale Einrichtung erforderlich. Erhalte eine ### Self-Hosted (Open Source) +#### npm + CloudCLI UI sofort mit **npx** ausprobieren (erfordert **Node.js** v22+): ```bash @@ -93,6 +95,15 @@ cloudcli Die **[Dokumentation →](https://cloudcli.ai/docs)** enthält weitere Konfigurationsoptionen, PM2, Remote-Server-Einrichtung und mehr. +#### Docker Sandboxes (Experimentell) + +Agents in isolierten Sandboxes mit Hypervisor-Isolation ausführen. Standardmäßig wird Claude Code gestartet. Erfordert die [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/). + +``` +npx @cloudcli-ai/cloudcli sandbox ~/my-project +``` + +Unterstützt Claude Code, Codex und Gemini CLI. Weitere Details in der [Sandbox-Dokumentation](docker/). --- diff --git a/README.ja.md b/README.ja.md index eada6dec..dfca8cda 100644 --- a/README.ja.md +++ b/README.ja.md @@ -72,6 +72,8 @@ ### セルフホスト(オープンソース) +#### npm + **npx** で今すぐ CloudCLI UI を試せます(**Node.js** v22+ が必要): ```bash @@ -89,6 +91,15 @@ cloudcli より詳細な設定オプション、PM2、リモートサーバー設定などについては **[ドキュメントはこちら →](https://cloudcli.ai/docs)** を参照してください。 +#### Docker Sandboxes(実験的) + +ハイパーバイザーレベルの分離でエージェントをサンドボックスで実行します。デフォルトでは Claude Code が起動します。[`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/) が必要です。 + +``` +npx @cloudcli-ai/cloudcli sandbox ~/my-project +``` + +Claude Code、Codex、Gemini CLI に対応。詳細は[サンドボックスのドキュメント](docker/)をご覧ください。 --- diff --git a/README.ko.md b/README.ko.md index ed8ddd9f..6cee2adf 100644 --- a/README.ko.md +++ b/README.ko.md @@ -72,6 +72,8 @@ ### 셀프 호스트 (오픈 소스) +#### npm + **npx**로 즉시 CloudCLI UI를 실행하세요 (Node.js v22+ 필요): ```bash @@ -87,7 +89,17 @@ cloudcli `http://localhost:3001`을 열면 기존 세션이 자동으로 발견됩니다. -자세한 구성 옵션, PM2, 원격 서버 설정 등은 **[문서 →](https://cloudcli.ai/docs)**를 참고하세요 +자세한 구성 옵션, PM2, 원격 서버 설정 등은 **[문서 →](https://cloudcli.ai/docs)**를 참고하세요. + +#### Docker Sandboxes (실험적) + +하이퍼바이저 수준 격리로 에이전트를 샌드박스에서 실행합니다. 기본 에이전트는 Claude Code입니다. [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/)가 필요합니다. + +``` +npx @cloudcli-ai/cloudcli sandbox ~/my-project +``` + +Claude Code, Codex, Gemini CLI를 지원합니다. 자세한 내용은 [샌드박스 문서](docker/)를 참고하세요. --- diff --git a/README.md b/README.md index 751f1659..986505b3 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ The fastest way to get started — no local setup required. Get a fully managed, ### Self-Hosted (Open source) +#### npm + Try CloudCLI UI instantly with **npx** (requires **Node.js** v22+): ``` @@ -91,33 +93,41 @@ cloudcli Open `http://localhost:3001` — all your existing sessions are discovered automatically. -Visit the **[documentation →](https://cloudcli.ai/docs)** for more full configuration options, PM2, remote server setup and more +Visit the **[documentation →](https://cloudcli.ai/docs)** for full configuration options, PM2, remote server setup and more. + +#### Docker Sandboxes (Experimental) + +Run agents in isolated sandboxes with hypervisor-level isolation. Starts Claude Code by default. Requires the [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/). + +``` +npx @cloudcli-ai/cloudcli sandbox ~/my-project +``` + +Supports Claude Code, Codex, and Gemini CLI. See the [sandbox docs](docker/) for setup and advanced options. --- ## Which option is right for you? -CloudCLI UI is the open source UI layer that powers CloudCLI Cloud. You can self-host it on your own machine, or use CloudCLI Cloud which builds on top of it with a full managed cloud environment, team features, and deeper integrations. +CloudCLI UI is the open source UI layer that powers CloudCLI Cloud. You can self-host it on your own machine, run it in a Docker sandbox for isolation, or use CloudCLI Cloud for a fully managed environment. -| | CloudCLI UI (Self-hosted) | CloudCLI Cloud | -|---|---|---| -| **Best for** | Developers who want a full UI for local agent sessions on their own machine | Teams and developers who want agents running in the cloud, accessible from anywhere | -| **How you access it** | Browser via `[yourip]:port` | Browser, any IDE, REST API, n8n | -| **Setup** | `npx @cloudcli-ai/cloudcli` | No setup required | -| **Machine needs to stay on** | Yes | No | -| **Mobile access** | Any browser on your network | Any device, native app coming | -| **Sessions available** | All sessions auto-discovered from `~/.claude` | All sessions within your cloud environment | -| **Agents supported** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI | -| **File explorer and Git** | Yes, built into the UI | Yes, built into the UI | -| **MCP configuration** | Managed via UI, synced with your local `~/.claude` config | Managed via UI | -| **IDE access** | Your local IDE | Any IDE connected to your cloud environment | -| **REST API** | Yes | Yes | -| **n8n node** | No | Yes | -| **Team sharing** | No | Yes | -| **Platform cost** | Free, open source | Starts at $7/month | +| | Self-Hosted (npm) | Self-Hosted (Docker Sandbox) *(Experimental)* | CloudCLI Cloud | +|---|---|---|---| +| **Best for** | Local agent sessions on your own machine | Isolated agents with web/mobile IDE | Teams who want agents in the cloud | +| **How you access it** | Browser via `[yourip]:port` | Browser via `localhost:port` | Browser, any IDE, REST API, n8n | +| **Setup** | `npx @cloudcli-ai/cloudcli` | `npx @cloudcli-ai/cloudcli sandbox ~/project` | No setup required | +| **Isolation** | Runs on your host | Hypervisor-level sandbox (microVM) | Full cloud isolation | +| **Machine needs to stay on** | Yes | Yes | No | +| **Mobile access** | Any browser on your network | Any browser on your network | Any device, native app coming | +| **Agents supported** | Claude Code, Cursor CLI, Codex, Gemini CLI | Claude Code, Codex, Gemini CLI | Claude Code, Cursor CLI, Codex, Gemini CLI | +| **File explorer and Git** | Yes | Yes | Yes | +| **MCP configuration** | Synced with `~/.claude` | Managed via UI | Managed via UI | +| **REST API** | Yes | Yes | Yes | +| **Team sharing** | No | No | Yes | +| **Platform cost** | Free, open source | Free, open source | Starts at $7/month | -> Both options use your own AI subscriptions (Claude, Cursor, etc.) — CloudCLI provides the environment, not the AI. +> All options use your own AI subscriptions (Claude, Cursor, etc.) — CloudCLI provides the environment, not the AI. --- diff --git a/README.ru.md b/README.ru.md index c7561ba6..55b936a8 100644 --- a/README.ru.md +++ b/README.ru.md @@ -76,6 +76,8 @@ ### Self-Hosted (Open source) +#### npm + Попробовать CloudCLI UI можно сразу через **npx** (требуется **Node.js** v22+): ```bash @@ -91,8 +93,17 @@ cloudcli Откройте `http://localhost:3001` — все ваши существующие сессии будут обнаружены автоматически. -Посетите **[документацию →](https://cloudcli.ai/docs)**, чтобы узнать про дополнительные варианты конфигурации, PM2, настройку удалённого сервера и многое другое +Посетите **[документацию →](https://cloudcli.ai/docs)**, чтобы узнать про дополнительные варианты конфигурации, PM2, настройку удалённого сервера и многое другое. +#### Docker Sandboxes (Экспериментально) + +Запускайте агентов в изолированных песочницах с гипервизорной изоляцией. По умолчанию запускается Claude Code. Требуется [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/). + +``` +npx @cloudcli-ai/cloudcli sandbox ~/my-project +``` + +Поддерживаются Claude Code, Codex и Gemini CLI. Подробнее в [документации sandbox](docker/). --- diff --git a/README.zh-CN.md b/README.zh-CN.md index 690af7d8..3f18d3cd 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -72,6 +72,8 @@ ### 自托管(开源) +#### npm + 启动 CloudCLI UI,只需一行 `npx`(需要 Node.js v22+): ```bash @@ -87,7 +89,17 @@ cloudcli 打开 `http://localhost:3001`,系统会自动发现所有现有会话。 -更多配置选项、PM2、远程服务器设置等,请参阅 **[文档 →](https://cloudcli.ai/docs)** +更多配置选项、PM2、远程服务器设置等,请参阅 **[文档 →](https://cloudcli.ai/docs)**。 + +#### Docker Sandboxes(实验性) + +在隔离的沙箱中运行代理,具有虚拟机管理程序级别的隔离。默认启动 Claude Code。需要 [`sbx` CLI](https://docs.docker.com/ai/sandboxes/get-started/)。 + +``` +npx @cloudcli-ai/cloudcli sandbox ~/my-project +``` + +支持 Claude Code、Codex 和 Gemini CLI。详情请参阅 [沙箱文档](docker/)。 --- diff --git a/docker/README.md b/docker/README.md index 780fcf93..c093eccb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,89 +1,146 @@ -# CloudCLI — Docker Sandbox Templates + + -Run AI coding agents with a full web IDE inside [Docker Sandboxes](https://docs.docker.com/ai/sandboxes/). +# Sandboxed coding agents with a web & mobile IDE (CloudCLI) -Instead of a terminal-only experience, get a browser-based interface with chat, file explorer, git panel, shell, and MCP configuration — all running safely inside an isolated sandbox. +[Docker Sandbox](https://docs.docker.com/ai/sandboxes/) templates that add [CloudCLI](https://cloudcli.ai) on top of Claude Code, Codex, and Gemini CLI. You get a full web and mobile IDE accessible from any browser on any device. -## Available Templates +## Get started -| Template | Base Image | Agent | -|----------|-----------|-------| -| `cloudcli-ai/sandbox:claude-code` | `docker/sandbox-templates:claude-code` | Claude Code | -| `cloudcli-ai/sandbox:codex` | `docker/sandbox-templates:codex` | OpenAI Codex | -| `cloudcli-ai/sandbox:gemini` | `docker/sandbox-templates:gemini` | Gemini CLI | +### 1. Install the sbx CLI -## Quick Start +Docker Sandboxes run agents in isolated microVMs. Install the `sbx` CLI: -### 1. Start a sandbox with the template +- **macOS**: `brew install docker/tap/sbx` +- **Windows**: `winget install -h Docker.sbx` +- **Linux**: `sudo apt-get install docker-sbx` + +Full instructions: [docs.docker.com/ai/sandboxes/get-started](https://docs.docker.com/ai/sandboxes/get-started/) + +### 2. Store your API key + +`sbx` manages credentials securely — your API key never enters the sandbox. Store it once: ```bash -sbx run --template docker.io/cloudcli-ai/sandbox:claude-code claude ~/my-project +sbx login +sbx secret set -g anthropic ``` -### 2. Forward the UI port +### 3. Launch Claude Code ```bash -sbx ports --publish 3001:3001 +npx @cloudcli-ai/cloudcli sandbox ~/my-project ``` -### 3. Open the browser +Open **http://localhost:3001**. Set a password on first visit. Start building. -``` -http://localhost:3001 +### Using a different agent + +Store the matching API key and pass `--agent`: + +```bash +# OpenAI Codex +sbx secret set -g openai +npx @cloudcli-ai/cloudcli sandbox ~/my-project --agent codex + +# Gemini CLI +sbx secret set -g google +npx @cloudcli-ai/cloudcli sandbox ~/my-project --agent gemini ``` -On first visit you'll set a password — this protects the UI if the port is ever exposed beyond localhost. +### Available templates -## What You Get +| Agent | Template | +|-------|----------| +| **Claude Code** (default) | `docker.io/cloudcliai/sandbox:claude-code` | +| OpenAI Codex | `docker.io/cloudcliai/sandbox:codex` | +| Gemini CLI | `docker.io/cloudcliai/sandbox:gemini` | -- **Chat** — Rich conversation UI with markdown rendering, code blocks, and message history -- **Files** — Visual file tree with syntax-highlighted editor -- **Git** — Diff viewer, staging, branch switching, and commit — all visual +These are used with `--template` when running `sbx` directly (see [Advanced usage](#advanced-usage)). + +## Managing sandboxes + +```bash +cloudcli sandbox ls # List all sandboxes +cloudcli sandbox stop my-project # Stop (preserves state) +cloudcli sandbox start my-project # Restart and re-launch web UI +cloudcli sandbox logs my-project # View server logs +cloudcli sandbox rm my-project # Remove everything +``` + +## What you get + +- **Chat** — Markdown rendering, code blocks, message history +- **Files** — File tree with syntax-highlighted editor +- **Git** — Diff viewer, staging, branch switching, commits - **Shell** — Built-in terminal emulator -- **MCP** — Configure Model Context Protocol servers through the UI +- **MCP** — Configure Model Context Protocol servers visually - **Mobile** — Works on tablet and phone browsers -## Building Locally - -All Dockerfiles share scripts from `shared/`. Build with the `docker/` directory as context: - -```bash -# Claude Code variant -docker build -f docker/claude-code/Dockerfile -t cloudcli-sandbox:claude-code docker/ - -# Codex variant -docker build -f docker/codex/Dockerfile -t cloudcli-sandbox:codex docker/ - -# Gemini variant -docker build -f docker/gemini/Dockerfile -t cloudcli-sandbox:gemini docker/ -``` - -## How It Works - -Each template extends Docker's official sandbox base image and adds: - -1. **Node.js 22** — Runtime for CloudCLI -2. **CloudCLI** — Installed globally via `npm install -g @cloudcli-ai/cloudcli` -3. **Auto-start** — The UI server starts in the background when the sandbox shell opens (port 3001) - -The agent (Claude Code, Codex, or Gemini) comes from the base image. CloudCLI connects to it and provides the web interface on top. +Your project directory is mounted bidirectionally — edits propagate in real time, both ways. ## Configuration -| Environment Variable | Default | Description | -|---------------------|---------|-------------| -| `SERVER_PORT` | `3001` | Port for the web UI | -| `HOST` | `0.0.0.0` | Bind address | -| `DATABASE_PATH` | `~/.cloudcli/auth.db` | SQLite database location | - -## Network Policies - -If your sandbox uses restricted network policies, allow the UI port: +Set variables at creation time with `--env`: ```bash -sbx policy allow network "localhost:3001" +npx @cloudcli-ai/cloudcli sandbox ~/my-project --env SERVER_PORT=8080 ``` +Or inside a running sandbox: + +```bash +sbx exec my-project bash -c 'echo "export SERVER_PORT=8080" >> /etc/sandbox-persistent.sh' +sbx exec my-project bash -c 'pkill -f "server/index.js"; . ~/.cloudcli-start.sh' +``` + +| Variable | Default | Description | +|----------|---------|-------------| +| `SERVER_PORT` | `3001` | Web UI port | +| `HOST` | `0.0.0.0` | Bind address (must be `0.0.0.0` for `sbx ports`) | +| `DATABASE_PATH` | `~/.cloudcli/auth.db` | SQLite database location | + +## Advanced usage + +For branch mode, multiple workspaces, memory limits, or the terminal agent experience, use `sbx` with the template: + +```bash +# Terminal agent + web UI +sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/my-project --name my-project +sbx ports my-project --publish 3001:3001 + +# Branch mode (Git worktree isolation) +sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/my-project --branch my-feature + +# Multiple workspaces +sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/project ~/shared-libs:ro + +# Pass a prompt directly +sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/my-project -- "Fix the auth bug" +``` + +CloudCLI auto-starts via `.bashrc` when using `sbx run`. + +Full options in the [Docker Sandboxes usage guide](https://docs.docker.com/ai/sandboxes/usage/). + +## Network policies + +Sandboxes restrict outbound access by default. To reach host services from inside the sandbox: + +```bash +sbx policy allow network localhost:11434 +# Inside the sandbox: curl http://host.docker.internal:11434 +``` + +The web UI itself doesn't need a policy — access it via `sbx ports`. + +## Links + +- [CloudCLI Cloud](https://cloudcli.ai) — fully managed, no setup required +- [Documentation](https://cloudcli.ai/docs) — full configuration guide +- [Discord](https://discord.gg/buxwujPNRE) — community support +- [GitHub](https://github.com/siteboon/claudecodeui) — source code and issues + ## License -These templates are free and open-source under the same license as CloudCLI (AGPL-3.0-or-later). +AGPL-3.0-or-later diff --git a/docker/claude-code/Dockerfile b/docker/claude-code/Dockerfile index ed04cf6b..8d108a80 100644 --- a/docker/claude-code/Dockerfile +++ b/docker/claude-code/Dockerfile @@ -5,7 +5,7 @@ COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh USER agent -RUN npm install -g @cloudcli-ai/cloudcli +RUN npm install -g @cloudcli-ai/cloudcli && cloudcli --version -COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh +COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc diff --git a/docker/codex/Dockerfile b/docker/codex/Dockerfile index 9948d0af..c1bc1807 100644 --- a/docker/codex/Dockerfile +++ b/docker/codex/Dockerfile @@ -5,7 +5,7 @@ COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh USER agent -RUN npm install -g @cloudcli-ai/cloudcli +RUN npm install -g @cloudcli-ai/cloudcli && cloudcli --version -COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh +COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc diff --git a/docker/gemini/Dockerfile b/docker/gemini/Dockerfile index ec7209f0..dc06db69 100644 --- a/docker/gemini/Dockerfile +++ b/docker/gemini/Dockerfile @@ -5,7 +5,7 @@ COPY shared/install-cloudcli.sh /tmp/install-cloudcli.sh RUN chmod +x /tmp/install-cloudcli.sh && /tmp/install-cloudcli.sh USER agent -RUN npm install -g @cloudcli-ai/cloudcli +RUN npm install -g @cloudcli-ai/cloudcli && cloudcli --version -COPY shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh +COPY --chown=agent:agent shared/start-cloudcli.sh /home/agent/.cloudcli-start.sh RUN echo '. ~/.cloudcli-start.sh' >> /home/agent/.bashrc diff --git a/docker/shared/install-cloudcli.sh b/docker/shared/install-cloudcli.sh index 0390383e..4b43fe22 100644 --- a/docker/shared/install-cloudcli.sh +++ b/docker/shared/install-cloudcli.sh @@ -1,13 +1,10 @@ #!/bin/bash set -e -# Install Node.js 22 LTS -curl -fsSL https://deb.nodesource.com/setup_22.x | bash - - -# Install Node.js + build tools needed for native modules (node-pty, better-sqlite3, bcrypt) -# Node.js + build tools for native modules + common dev tools -apt-get install -y --no-install-recommends \ - nodejs build-essential python3 python3-setuptools \ +# Install build tools needed for native modules (node-pty, better-sqlite3, bcrypt) +# Node.js is already provided by the sandbox base image +apt-get update && apt-get install -y --no-install-recommends \ + build-essential python3 python3-setuptools \ jq ripgrep sqlite3 zip unzip tree vim-tiny # Clean up apt cache to reduce image size diff --git a/docker/shared/start-cloudcli.sh b/docker/shared/start-cloudcli.sh index 145c35c5..82b2fdc2 100644 --- a/docker/shared/start-cloudcli.sh +++ b/docker/shared/start-cloudcli.sh @@ -4,19 +4,14 @@ # This script is sourced from ~/.bashrc on sandbox shell open. if ! pgrep -f "server/index.js" > /dev/null 2>&1; then - # Start the pre-installed version immediately nohup cloudcli start --port 3001 > /tmp/cloudcli-ui.log 2>&1 & disown - # Check for updates in the background (non-blocking) - nohup npm update -g @cloudcli-ai/cloudcli > /tmp/cloudcli-update.log 2>&1 & - disown - echo "" echo " CloudCLI is starting on port 3001..." echo "" - echo " To access the web UI, forward the port:" - echo " sbx ports \$(hostname) --publish 3001:3001" + echo " Forward the port from another terminal:" + echo " sbx ports --publish 3001:3001" echo "" echo " Then open: http://localhost:3001" echo "" diff --git a/server/cli.js b/server/cli.js index 39461673..dad74268 100755 --- a/server/cli.js +++ b/server/cli.js @@ -7,6 +7,7 @@ * Commands: * (no args) - Start the server (default) * start - Start the server + * sandbox - Manage Docker sandbox environments * status - Show configuration and data locations * help - Show help information * version - Show version information @@ -150,6 +151,7 @@ Usage: Commands: start Start the CloudCLI server (default) + sandbox Manage Docker sandbox environments status Show configuration and data locations update Update to the latest version help Show this help information @@ -164,8 +166,7 @@ Options: Examples: $ cloudcli # Start with defaults $ cloudcli --port 8080 # Start on port 8080 - $ cloudcli -p 3000 # Short form for port - $ cloudcli start --port 4000 # Explicit start command + $ cloudcli sandbox ~/my-project # Run in a Docker sandbox $ cloudcli status # Show configuration Environment Variables: @@ -244,6 +245,357 @@ async function updatePackage() { } } +// ── Sandbox command ───────────────────────────────────────── + +const SANDBOX_TEMPLATES = { + claude: 'docker.io/cloudcliai/sandbox:claude-code', + codex: 'docker.io/cloudcliai/sandbox:codex', + gemini: 'docker.io/cloudcliai/sandbox:gemini', +}; + +const SANDBOX_SECRETS = { + claude: 'anthropic', + codex: 'openai', + gemini: 'google', +}; + +function parseSandboxArgs(args) { + const result = { + subcommand: null, + workspace: null, + agent: 'claude', + name: null, + port: 3001, + template: null, + env: [], + }; + + const subcommands = ['ls', 'stop', 'start', 'rm', 'logs', 'help']; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (i === 0 && subcommands.includes(arg)) { + result.subcommand = arg; + } else if (arg === '--agent' || arg === '-a') { + result.agent = args[++i]; + } else if (arg === '--name' || arg === '-n') { + result.name = args[++i]; + } else if (arg === '--port') { + result.port = parseInt(args[++i], 10); + } else if (arg === '--template' || arg === '-t') { + result.template = args[++i]; + } else if (arg === '--env' || arg === '-e') { + result.env.push(args[++i]); + } else if (!arg.startsWith('-')) { + if (!result.subcommand) { + result.workspace = arg; + } else { + result.name = arg; // for stop/start/rm/logs + } + } + } + + // Default subcommand based on what we got + if (!result.subcommand) { + result.subcommand = 'create'; + } + + // Derive name from workspace path if not set + if (!result.name && result.workspace) { + result.name = path.basename(path.resolve(result.workspace.replace(/^~/, os.homedir()))); + } + + // Default template from agent + if (!result.template) { + result.template = SANDBOX_TEMPLATES[result.agent] || SANDBOX_TEMPLATES.claude; + } + + return result; +} + +function showSandboxHelp() { + console.log(` +${c.bright('CloudCLI Sandbox')} — Run CloudCLI inside Docker Sandboxes + +Usage: + cloudcli sandbox Create and start a sandbox + cloudcli sandbox [name] Manage sandboxes + +Subcommands: + ${c.bright('(default)')} Create a sandbox and start the web UI + ${c.bright('ls')} List all sandboxes + ${c.bright('start')} Restart a stopped sandbox and re-launch the web UI + ${c.bright('stop')} Stop a sandbox (preserves state) + ${c.bright('rm')} Remove a sandbox + ${c.bright('logs')} Show CloudCLI server logs + ${c.bright('help')} Show this help + +Options: + -a, --agent Agent to use: claude, codex, gemini (default: claude) + -n, --name Sandbox name (default: derived from workspace folder) + -t, --template Custom template image + -e, --env Set environment variable (repeatable) + --port Host port for the web UI (default: 3001) + +Examples: + $ cloudcli sandbox ~/my-project + $ cloudcli sandbox ~/my-project --agent codex --port 8080 + $ cloudcli sandbox ~/my-project --env SERVER_PORT=8080 --env HOST=0.0.0.0 + $ cloudcli sandbox ls + $ cloudcli sandbox stop my-project + $ cloudcli sandbox start my-project + $ cloudcli sandbox rm my-project + +Prerequisites: + 1. Install sbx CLI: https://docs.docker.com/ai/sandboxes/get-started/ + 2. Authenticate and store your API key: + sbx login + sbx secret set -g anthropic # for Claude + sbx secret set -g openai # for Codex + sbx secret set -g google # for Gemini + +Advanced usage: + For branch mode, multiple workspaces, memory limits, network policies, + or passing prompts to the agent, use sbx directly with the template: + + sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/my-project --branch my-feature + sbx run --template docker.io/cloudcliai/sandbox:claude-code claude ~/project ~/libs:ro --memory 8g + + Full Docker Sandboxes docs: https://docs.docker.com/ai/sandboxes/usage/ +`); +} + +async function sandboxCommand(args) { + const { execFileSync } = await import('child_process'); + + // Safe execution — uses execFileSync (no shell) to prevent injection + const sbx = (subcmd, opts = {}) => { + const result = execFileSync('sbx', subcmd, { + encoding: 'utf8', + stdio: opts.inherit ? 'inherit' : 'pipe', + }); + return result || ''; + }; + + const opts = parseSandboxArgs(args); + + if (opts.subcommand === 'help') { + showSandboxHelp(); + return; + } + + // Validate name (alphanumeric, hyphens, underscores only) + if (opts.name && !/^[\w-]+$/.test(opts.name)) { + console.error(`\n${c.error('❌')} Invalid sandbox name: ${opts.name}`); + console.log(` Names may only contain letters, numbers, hyphens, and underscores.\n`); + process.exit(1); + } + + // Check sbx is installed + try { + sbx(['version']); + } catch { + console.error(`\n${c.error('❌')} ${c.bright('sbx')} CLI not found.\n`); + console.log(` Install it from: ${c.info('https://docs.docker.com/ai/sandboxes/get-started/')}`); + console.log(` Then run: ${c.bright('sbx login')}`); + console.log(` And store your API key: ${c.bright('sbx secret set -g anthropic')}\n`); + process.exit(1); + } + + switch (opts.subcommand) { + + case 'ls': + sbx(['ls'], { inherit: true }); + break; + + case 'stop': + if (!opts.name) { + console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox stop \n`); + process.exit(1); + } + sbx(['stop', opts.name], { inherit: true }); + break; + + case 'rm': + if (!opts.name) { + console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox rm \n`); + process.exit(1); + } + sbx(['rm', opts.name], { inherit: true }); + break; + + case 'logs': + if (!opts.name) { + console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox logs \n`); + process.exit(1); + } + try { + sbx(['exec', opts.name, 'bash', '-c', 'cat /tmp/cloudcli-ui.log'], { inherit: true }); + } catch (e) { + console.error(`\n${c.error('❌')} Could not read logs: ${e.message || 'Is the sandbox running?'}\n`); + } + break; + + case 'start': { + if (!opts.name) { + console.error(`\n${c.error('❌')} Sandbox name required: cloudcli sandbox start \n`); + process.exit(1); + } + console.log(`\n${c.info('▶')} Starting sandbox ${c.bright(opts.name)}...`); + try { + sbx(['start', opts.name], { inherit: true }); + } catch { /* might already be running */ } + + console.log(`${c.info('▶')} Launching CloudCLI web server...`); + sbx(['exec', '-d', opts.name, 'bash', '-c', '. ~/.cloudcli-start.sh']); + + console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`); + try { + sbx(['ports', opts.name, '--publish', `${opts.port}:3001`]); + } catch (e) { + const msg = e.stdout || e.stderr || e.message || ''; + if (msg.includes('address already in use')) { + const altPort = opts.port + 1; + console.log(`${c.warn('⚠')} Port ${opts.port} in use, trying ${altPort}...`); + try { + sbx(['ports', opts.name, '--publish', `${altPort}:3001`]); + opts.port = altPort; + } catch { + console.error(`${c.error('❌')} Ports ${opts.port} and ${altPort} both in use. Use --port to specify a free port.`); + process.exit(1); + } + } else { + throw e; + } + } + + console.log(`\n${c.ok('✔')} ${c.bright('CloudCLI is ready!')}`); + console.log(` ${c.info('→')} ${c.bright(`http://localhost:${opts.port}`)}\n`); + break; + } + + case 'create': { + if (!opts.workspace) { + console.error(`\n${c.error('❌')} Workspace path required: cloudcli sandbox \n`); + console.log(` Example: ${c.bright('cloudcli sandbox ~/my-project')}\n`); + process.exit(1); + } + + const workspace = opts.workspace.startsWith('~') + ? opts.workspace.replace(/^~/, os.homedir()) + : path.resolve(opts.workspace); + + if (!fs.existsSync(workspace)) { + console.error(`\n${c.error('❌')} Workspace path not found: ${c.dim(workspace)}\n`); + process.exit(1); + } + + const secret = SANDBOX_SECRETS[opts.agent] || 'anthropic'; + + // Check if the required secret is stored + try { + const secretList = sbx(['secret', 'ls']); + if (!secretList.includes(secret)) { + console.error(`\n${c.error('❌')} No ${c.bright(secret)} API key found.\n`); + console.log(` Run: ${c.bright(`sbx secret set -g ${secret}`)}\n`); + process.exit(1); + } + } catch { /* sbx secret ls not available, skip check */ } + + console.log(`\n${c.bright('CloudCLI Sandbox')}`); + console.log(c.dim('─'.repeat(50))); + console.log(` Agent: ${c.info(opts.agent)} ${c.dim(`(${secret} credentials)`)}`); + console.log(` Workspace: ${c.dim(workspace)}`); + console.log(` Name: ${c.dim(opts.name)}`); + console.log(` Template: ${c.dim(opts.template)}`); + console.log(` Port: ${c.dim(String(opts.port))}`); + if (opts.env.length > 0) { + console.log(` Env: ${c.dim(opts.env.join(', '))}`); + } + console.log(c.dim('─'.repeat(50))); + + // Step 1: Create sandbox + console.log(`\n${c.info('▶')} Creating sandbox ${c.bright(opts.name)}...`); + try { + sbx( + ['create', '--template', opts.template, '--name', opts.name, opts.agent, workspace], + { inherit: true } + ); + } catch (e) { + const msg = e.stdout || e.stderr || e.message || ''; + if (msg.includes('already exists')) { + console.log(`${c.warn('⚠')} Sandbox ${c.bright(opts.name)} already exists. Starting it instead...\n`); + try { sbx(['start', opts.name]); } catch { /* may already be running */ } + } else { + throw e; + } + } + + // Step 2: Inject environment variables + if (opts.env.length > 0) { + console.log(`${c.info('▶')} Setting environment variables...`); + const exports = opts.env + .filter(e => /^\w+=.+$/.test(e)) + .map(e => `export ${e}`) + .join('\n'); + if (exports) { + sbx(['exec', opts.name, 'bash', '-c', `echo '${exports}' >> /etc/sandbox-persistent.sh`]); + } + const invalid = opts.env.filter(e => !/^\w+=.+$/.test(e)); + if (invalid.length > 0) { + console.log(`${c.warn('⚠')} Skipped invalid env vars: ${invalid.join(', ')} (expected KEY=VALUE)`); + } + } + + // Step 3: Start CloudCLI + console.log(`${c.info('▶')} Launching CloudCLI web server...`); + try { + sbx(['exec', '-d', opts.name, 'bash', '-c', '. ~/.cloudcli-start.sh']); + } catch (e) { + console.error(`${c.error('❌')} Failed to start CloudCLI: ${e.message}`); + process.exit(1); + } + + // Step 4: Forward port + console.log(`${c.info('▶')} Forwarding port ${opts.port} → 3001...`); + try { + sbx(['ports', opts.name, '--publish', `${opts.port}:3001`]); + } catch (e) { + const msg = e.stdout || e.stderr || e.message || ''; + if (msg.includes('address already in use')) { + const altPort = opts.port + 1; + console.log(`${c.warn('⚠')} Port ${opts.port} in use, trying ${altPort}...`); + try { + sbx(['ports', opts.name, '--publish', `${altPort}:3001`]); + opts.port = altPort; + } catch { + console.error(`${c.error('❌')} Ports ${opts.port} and ${altPort} both in use. Use --port to specify a free port.`); + process.exit(1); + } + } else { + throw e; + } + } + + // Done + console.log(`\n${c.ok('✔')} ${c.bright('CloudCLI is ready!')}`); + console.log(` ${c.info('→')} Open ${c.bright(`http://localhost:${opts.port}`)}`); + console.log(`\n${c.dim(' Manage with:')}`); + console.log(` ${c.dim('$')} cloudcli sandbox ls`); + console.log(` ${c.dim('$')} cloudcli sandbox stop ${opts.name}`); + console.log(` ${c.dim('$')} cloudcli sandbox start ${opts.name}`); + console.log(` ${c.dim('$')} cloudcli sandbox rm ${opts.name}\n`); + break; + } + + default: + showSandboxHelp(); + } +} + +// ── Server ────────────────────────────────────────────────── + // Start the server async function startServer() { // Check for updates silently on startup @@ -274,6 +626,10 @@ function parseArgs(args) { parsed.command = 'version'; } else if (!arg.startsWith('-')) { parsed.command = arg; + if (arg === 'sandbox') { + parsed.remainingArgs = args.slice(i + 1); + break; + } } } @@ -283,7 +639,7 @@ function parseArgs(args) { // Main CLI handler async function main() { const args = process.argv.slice(2); - const { command, options } = parseArgs(args); + const { command, options, remainingArgs } = parseArgs(args); // Apply CLI options to environment variables if (options.serverPort) { @@ -299,6 +655,9 @@ async function main() { case 'start': await startServer(); break; + case 'sandbox': + await sandboxCommand(remainingArgs || []); + break; case 'status': case 'info': showStatus();