From 8fb6fa72472d8152fee264064e8626f0fb8b1855 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 20:12:37 +0000 Subject: [PATCH 01/27] feat(ticket-021): add docker compose full stack + fix launch-editor.sh for Linux Co-Authored-By: Claude Sonnet 4.6 --- docker/docker-compose.full.yml | 67 ++++++++++++++++++++++++++++++++++ launch-editor.sh | 35 +++++++++++++++--- 2 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 docker/docker-compose.full.yml diff --git a/docker/docker-compose.full.yml b/docker/docker-compose.full.yml new file mode 100644 index 00000000..fbc9e9db --- /dev/null +++ b/docker/docker-compose.full.yml @@ -0,0 +1,67 @@ +version: "3.9" + +services: + occ-gateway: + image: openclaw/pod:latest + restart: unless-stopped + ports: + - "127.0.0.1:18789:18789" + environment: + DATABASE_URL: postgresql://openclaw:occdev@occ-postgres:5432/openclaw + REDIS_URL: redis://occ-redis:6379 + GATEWAY_PORT: "18789" + volumes: + - ${OPENCLAW_DATA_DIR:-~/.openclaw}:/root/.openclaw + networks: + - occ-network + depends_on: + occ-postgres: + condition: service_healthy + occ-redis: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:18789/health || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + occ-postgres: + image: postgres:16-alpine + restart: unless-stopped + ports: + - "127.0.0.1:5432:5432" + environment: + POSTGRES_PASSWORD: occdev + POSTGRES_DB: openclaw + POSTGRES_USER: openclaw + volumes: + - occ-postgres-data:/var/lib/postgresql/data + networks: + - occ-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U openclaw"] + interval: 5s + timeout: 3s + retries: 5 + + occ-redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - "127.0.0.1:6379:6379" + networks: + - occ-network + healthcheck: + test: ["CMD-SHELL", "redis-cli ping"] + interval: 5s + timeout: 3s + retries: 5 + +networks: + occ-network: + driver: bridge + +volumes: + occ-openclaw-data: + occ-postgres-data: diff --git a/launch-editor.sh b/launch-editor.sh index 72ac52f2..be88c691 100755 --- a/launch-editor.sh +++ b/launch-editor.sh @@ -3,11 +3,36 @@ set -e ROOT="$(cd "$(dirname "$0")" && pwd)" -export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" -[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" +# Determine how to activate the correct Node.js version. +# +# Priority: +# 1. Inside a Docker container -> use system node (skip nvm entirely) +# 2. nvm is available -> use nvm as before +# 3. System node is available -> use system node directly +# 4. Nothing found -> print helpful error and exit 1 + +if [ -f "/.dockerenv" ]; then + # Running inside a Docker container; rely on system node. + if ! command -v node >/dev/null 2>&1; then + echo "ERROR: Running inside Docker but no system node found." >&2 + echo "Make sure your Docker image includes Node.js." >&2 + exit 1 + fi + echo "Docker environment detected, using system node ($(node --version))." +else + export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + if [ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh" 2>/dev/null; then + # nvm is available – use the version pinned in .nvmrc / .node-version. + nvm use + elif command -v node >/dev/null 2>&1; then + echo "nvm not found; using system node ($(node --version))." + else + echo "ERROR: No suitable Node.js runtime found." >&2 + echo "Please install nvm (https://github.com/nvm-sh/nvm) or Node.js directly," >&2 + echo "then re-run this script." >&2 + exit 1 + fi +fi cd "$ROOT/apps/editor" -nvm use exec ./scripts/code.sh "$@" - -cd apps/editor && VSCODE_SKIP_PRELAUNCH=1 ./scripts/code.sh \ No newline at end of file From 3d93fb3dfbf2302076a0a335c765e4a2c9c50871 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 20:13:56 +0000 Subject: [PATCH 02/27] feat(ticket-021): implement docker bootstrap wizard UI + engine in home.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add panel-bootstrap-choice: two-card setup choice (Docker Recommended / Local Advanced) - Add panel-docker-path: configurable data directory with ~/Desktop/occ shortcut note - Add panel-docker-doctor: live dependency checklist (OS, docker/podman, daemon, port, compose) - Add panel-docker-provision: streaming log panel for docker compose up -d - Add detectDockerEnvironment(): platform-aware check for docker/podman + daemon + port + compose - Add runDockerProvision(): pulls images, runs compose up, polls /health, writes openclaw.json - Add runDockerTeardown(): docker compose down for cancel/reset flows - Add createDesktopShortcut(): symlink ~/Desktop/occ → data dir (win: .lnk via PowerShell) - Add getDefaultOpenClawDataPath(): ~/.openclaw (mac/linux) or %APPDATA%\openclaw (win) - Wire dockerGetDefaultPath, dockerRunDoctor, dockerProvision, dockerCancel message handlers Co-Authored-By: Claude Sonnet 4.6 --- .../extensions/openclaw/src/panels/home.ts | 1156 +++++++++++++++++ 1 file changed, 1156 insertions(+) diff --git a/apps/editor/extensions/openclaw/src/panels/home.ts b/apps/editor/extensions/openclaw/src/panels/home.ts index def263ff..bec30688 100644 --- a/apps/editor/extensions/openclaw/src/panels/home.ts +++ b/apps/editor/extensions/openclaw/src/panels/home.ts @@ -256,6 +256,35 @@ export class HomePanel { if (args && args.length > 0) { void vscode.commands.executeCommand('void.openChatWithMessage', args[0], 'agent'); } + } else if (msg.command === 'dockerGetDefaultPath') { + try { + this._panel.webview.postMessage({ type: 'dockerDefaultPath', path: HomePanel.getDefaultOpenClawDataPath() }); + } catch { /* non-fatal */ } + } else if (msg.command === 'dockerRunDoctor') { + const dataPath = msg.dataPath as string || HomePanel.getDefaultOpenClawDataPath(); + const post = (m: object) => { try { this._panel.webview.postMessage(m); } catch {} }; + // Show spinner on all items first + post({ type: 'doctorUpdate', items: [ + { label: 'Detecting operating system…', status: 'pending' }, + { label: 'Looking for Docker or Podman…', status: 'pending' }, + ], allPassed: false, canRetry: false }); + const result = await HomePanel.detectDockerEnvironment(process.platform); + post({ type: 'doctorUpdate', ...result }); + // Store runtime for provisioning + (this as any)._dockerRuntime = result.runtime ?? 'docker'; + (this as any)._dockerDataPath = dataPath; + } else if (msg.command === 'dockerProvision') { + const dataPath = (msg.dataPath as string) || (this as any)._dockerDataPath || HomePanel.getDefaultOpenClawDataPath(); + const runtime: 'docker' | 'podman' = (this as any)._dockerRuntime ?? 'docker'; + const post = (m: object) => { try { this._panel.webview.postMessage(m); } catch {} }; + void HomePanel.runDockerProvision(post, dataPath, this._extensionUri.fsPath, runtime) + .then(() => { + // Re-check if openclaw is now configured + setTimeout(() => void this._update(), 2000); + }); + } else if (msg.command === 'dockerCancel') { + const runtime: 'docker' | 'podman' = (this as any)._dockerRuntime ?? 'docker'; + void HomePanel.runDockerTeardown(this._extensionUri.fsPath, runtime); } else if (msg.command === 'chooseHostType') { const t = msg.hostType as string; // Best-effort: close files from the other host's dir (non-blocking). @@ -1336,6 +1365,846 @@ The binary is already downloaded — do NOT re-download or compile anything.`; } +<<<<<<< HEAD +======= + private _getSetupHtml( + isInstalled: boolean, + iconUri: string, + occUser: { email: string; picture: string | null; balance_usd: number; api_keys?: { moltpilotKey?: string; occKey?: string } | null } | null = null + ): string { + // Render user area statically (avoids JS innerHTML escaping issues) + let userAreaHtml: string; + if (!occUser) { + userAreaHtml = ``; + } else { + const initial = (occUser.email || '?')[0].toUpperCase(); + const safeEmail = occUser.email.replace(/"/g, '"').replace(/` + : initial; + userAreaHtml = ` +
+ +
+
+
${avatarImg}
+
${safeEmail}
+
+ +
+ +
+
`; + } + + const providers = [ + { id: 'anthropic', label: 'Anthropic Claude', hint: 'console.anthropic.com/settings/keys', placeholder: 'sk-ant-...' }, + { id: 'openai', label: 'OpenAI', hint: 'platform.openai.com/api-keys', placeholder: 'sk-...' }, + { id: 'openrouter', label: 'OpenRouter', hint: 'openrouter.ai/settings/keys', placeholder: 'sk-or-...' }, + { id: 'gemini', label: 'Google Gemini', hint: 'aistudio.google.com/apikey', placeholder: 'AIza...' }, + ]; + + const providerCards = providers.map(p => + `` + ).join('\n '); + + return ` + + + + + + + + +
${userAreaHtml}
+ + + +
Set up OpenClaw
+
Follow the steps below to get started
+ + +
+
+
${isInstalled ? '✓' : '1'}
+
Install
OpenClaw
+
+
+
2
+
Configure
AI Model
+
+
+
3
+
Ready
+
+
+ + +
+
How would you like to set up OpenClaw?
+
Choose your installation method. Docker is recommended for a consistent, isolated environment.
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
Working
+ + + +
+ + + + + + +`; + } + +>>>>>>> 44f23a3 (feat(ticket-021): implement docker bootstrap wizard UI + engine in home.ts) private _getWizardHtml(iconUri: string, occUser: { email: string; picture: string | null; balance_usd: number; api_keys?: { moltpilotKey?: string; occKey?: string } | null } | null = null): string { // Render user area statically (avoids JS innerHTML escaping issues) let userAreaHtml: string; @@ -1949,4 +2818,291 @@ The binary is already downloaded — do NOT re-download or compile anything.`; private _buildExecEnv(): Record { return this._host.buildExecEnv(); } + + // ── Docker Bootstrap ────────────────────────────────────────────────────────── + + /** Returns the default ~/.openclaw data directory for the current OS. */ + public static getDefaultOpenClawDataPath(): string { + const platform = process.platform; + if (platform === 'win32') { + return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'openclaw'); + } + return path.join(os.homedir(), '.openclaw'); + } + + /** + * Creates a shortcut/symlink at ~/Desktop/occ → dataPath. + * On Windows creates a .lnk shortcut via PowerShell. On unix creates a symlink. + * Non-fatal: logs errors but never throws. + */ + public static async createDesktopShortcut(dataPath: string): Promise { + try { + const desktopDir = path.join(os.homedir(), 'Desktop'); + if (!fs.existsSync(desktopDir)) return; // No Desktop folder (headless/server) + const occDir = path.join(desktopDir, 'occ'); + if (process.platform === 'win32') { + // Create a Windows shortcut via PowerShell + const script = `$ws = New-Object -ComObject WScript.Shell; $s = $ws.CreateShortcut('${occDir}.lnk'); $s.TargetPath = '${dataPath}'; $s.Save()`; + await new Promise(resolve => cp.exec(`powershell -NoProfile -Command "${script}"`, () => resolve())); + } else { + // Unix symlink + if (fs.existsSync(occDir) || fs.lstatSync(occDir).isSymbolicLink()) { + fs.unlinkSync(occDir); + } + fs.symlinkSync(dataPath, occDir, 'dir'); + } + writeLog(`[docker-bootstrap] Desktop shortcut created: ${occDir} → ${dataPath}\n`); + } catch (e) { + writeLog(`[docker-bootstrap] Desktop shortcut creation skipped: ${e}\n`); + } + } + + /** + * Detects Docker (or Podman) availability and daemon status. + * Returns an array of checklist items for display. + */ + public static async detectDockerEnvironment(platform: string): Promise<{ + items: Array<{ label: string; detail?: string; status: 'ok' | 'fail' | 'warn' | 'pending' }>; + allPassed: boolean; + canRetry: boolean; + guide?: string; + runtime?: 'docker' | 'podman'; + }> { + const items: Array<{ label: string; detail?: string; status: 'ok' | 'fail' | 'warn' | 'pending' }> = []; + let allPassed = true; + let guide: string | undefined; + let runtime: 'docker' | 'podman' | undefined; + + // 1. OS detection + const osLabel = platform === 'darwin' ? 'macOS' : platform === 'win32' ? 'Windows' : `Linux (${process.arch})`; + items.push({ label: `Operating System: ${osLabel}`, status: 'ok' }); + + // 2. Docker CLI check + const dockerVersion = await new Promise(resolve => { + cp.exec('docker --version', { timeout: 5000 }, (err, stdout) => + resolve(err ? null : (stdout || '').trim()) + ); + }); + + if (dockerVersion) { + runtime = 'docker'; + items.push({ label: 'Docker CLI found', detail: dockerVersion, status: 'ok' }); + } else { + // Try podman + const podmanVersion = await new Promise(resolve => { + cp.exec('podman --version', { timeout: 5000 }, (err, stdout) => + resolve(err ? null : (stdout || '').trim()) + ); + }); + if (podmanVersion) { + runtime = 'podman'; + items.push({ label: 'Podman CLI found', detail: podmanVersion, status: 'ok' }); + } else { + allPassed = false; + items.push({ label: 'Docker or Podman not found', status: 'fail' }); + // Provide platform-specific install guide + if (platform === 'win32') { + guide = '📥 Install Docker Desktop for Windows
Download from docs.docker.com

After installation: restart your computer, then open Docker Desktop and ensure it is running.'; + } else if (platform === 'darwin') { + guide = '📥 Install Docker Desktop for macOS
Download from docs.docker.com

After installation: open Docker Desktop from Applications and wait for the whale icon to appear in the menu bar.'; + } else { + guide = '📥 Install Docker Engine on Linux
sudo apt-get update && sudo apt-get install -y docker.io docker-compose-v2
sudo systemctl start docker && sudo systemctl enable docker
sudo usermod -aG docker $USER (then log out and back in)

Or install Podman: sudo apt-get install -y podman'; + } + return { items, allPassed: false, canRetry: true, guide, runtime }; + } + } + + // 3. Daemon running check + const cliCmd = runtime === 'podman' ? 'podman' : 'docker'; + const daemonRunning = await new Promise(resolve => { + cp.exec(`${cliCmd} info`, { timeout: 8000 }, err => resolve(!err)); + }); + + if (daemonRunning) { + items.push({ label: `${runtime === 'podman' ? 'Podman' : 'Docker'} daemon is running`, status: 'ok' }); + } else { + allPassed = false; + const startMsg = platform === 'linux' + ? 'Start Docker: sudo systemctl start docker' + : `Open Docker Desktop and wait for it to start (look for the ${runtime === 'docker' ? '🐋' : ''} icon in the system tray)`; + items.push({ label: `${runtime === 'podman' ? 'Podman' : 'Docker'} daemon is not running`, detail: 'Start the daemon then retry', status: 'fail' }); + guide = `⚠️ ${runtime === 'podman' ? 'Podman' : 'Docker'} daemon not accessible.
${startMsg}`; + return { items, allPassed: false, canRetry: true, guide, runtime }; + } + + // 4. Port 18789 availability + const portFree = await new Promise(resolve => { + const net = require('net') as typeof import('net'); + const srv = net.createServer(); + srv.listen(18789, '127.0.0.1', () => { srv.close(() => resolve(true)); }); + srv.on('error', () => resolve(false)); + }); + + if (portFree) { + items.push({ label: 'Port 18789 is available', status: 'ok' }); + } else { + items.push({ label: 'Port 18789 is already in use', detail: 'Another process may be using this port', status: 'warn' }); + // Warn but don't block — Docker might already be running a previous OCC instance + } + + // 5. docker compose available + const composeAvail = await new Promise(resolve => { + cp.exec(`${cliCmd} compose version`, { timeout: 5000 }, err => { + if (!err) { resolve(true); return; } + // Fallback: docker-compose v1 standalone + cp.exec('docker-compose --version', { timeout: 5000 }, err2 => resolve(!err2)); + }); + }); + + if (composeAvail) { + items.push({ label: 'Docker Compose available', status: 'ok' }); + } else { + allPassed = false; + items.push({ label: 'Docker Compose not found', detail: 'Install docker-compose-plugin or docker-compose-v2', status: 'fail' }); + guide = platform === 'linux' + ? '📥 Install Docker Compose on Linux
sudo apt-get install -y docker-compose-v2
or
sudo apt-get install -y docker-compose' + : 'Docker Compose should be included with Docker Desktop. Please reinstall Docker Desktop.'; + } + + return { items, allPassed, canRetry: !allPassed, guide, runtime }; + } + + /** + * Runs `docker compose up -d` using the bundled compose file and streams output to the panel. + * Writes a .env file with OPENCLAW_DATA_DIR before running. + */ + public static async runDockerProvision( + post: (msg: object) => void, + dataPath: string, + extensionPath: string, + runtime: 'docker' | 'podman' = 'docker', + ): Promise { + const tee = (text: string) => { post({ type: 'provisionLog', text }); writeLog(text); }; + const composeFile = path.join(extensionPath, '..', '..', '..', '..', 'docker', 'docker-compose.full.yml'); + + // Resolve real compose file path (handle symlinks/relative) + let resolvedCompose = composeFile; + try { resolvedCompose = fs.realpathSync(composeFile); } catch { /* use original */ } + + if (!fs.existsSync(resolvedCompose)) { + // Fallback: look relative to extension directory + const altCompose = path.join(extensionPath, 'docker', 'docker-compose.full.yml'); + if (fs.existsSync(altCompose)) resolvedCompose = altCompose; + else { + post({ type: 'provisionStatus', text: '❌ Compose file not found. Cannot provision.', done: true, ok: false }); + return; + } + } + + // Expand dataPath (~/ prefix) + const expandedDataPath = dataPath.startsWith('~/') + ? path.join(os.homedir(), dataPath.slice(2)) + : dataPath; + + // Ensure data directory exists + try { fs.mkdirSync(expandedDataPath, { recursive: true }); } catch { /* non-fatal */ } + + // Write .env file alongside compose + const envFile = path.join(path.dirname(resolvedCompose), '.env'); + try { fs.writeFileSync(envFile, `OPENCLAW_DATA_DIR=${expandedDataPath}\n`, 'utf8'); } catch { /* non-fatal */ } + + tee(`▶ Using compose file: ${resolvedCompose}\n`); + tee(`▶ Data directory: ${expandedDataPath}\n`); + tee(`▶ Runtime: ${runtime}\n\n`); + + post({ type: 'provisionStatus', text: 'Pulling images (this may take a few minutes)…' }); + + const cliCmd = runtime === 'podman' ? 'podman' : 'docker'; + const env = { ...process.env, OPENCLAW_DATA_DIR: expandedDataPath }; + + // Pull images first + const pullResult = await new Promise(resolve => { + const child = cp.spawn(cliCmd, ['compose', '-f', resolvedCompose, 'pull'], { + env, stdio: ['ignore', 'pipe', 'pipe'], + }); + child.stdout?.on('data', (d: Buffer) => tee(d.toString())); + child.stderr?.on('data', (d: Buffer) => tee(d.toString())); + child.on('close', code => resolve(code ?? 1)); + child.on('error', err => { tee(`\nError: ${err.message}\n`); resolve(1); }); + }); + + if (pullResult !== 0) { + tee('\n⚠️ Image pull had warnings (may be ok if images are cached)\n'); + } + + tee('\n▶ Starting services…\n'); + post({ type: 'provisionStatus', text: 'Starting containers…' }); + + const upResult = await new Promise(resolve => { + const child = cp.spawn(cliCmd, ['compose', '-f', resolvedCompose, 'up', '-d', '--remove-orphans'], { + env, stdio: ['ignore', 'pipe', 'pipe'], + }); + child.stdout?.on('data', (d: Buffer) => tee(d.toString())); + child.stderr?.on('data', (d: Buffer) => tee(d.toString())); + child.on('close', code => resolve(code ?? 1)); + child.on('error', err => { tee(`\nError: ${err.message}\n`); resolve(1); }); + }); + + if (upResult !== 0) { + tee('\n❌ docker compose up failed.\n'); + post({ type: 'provisionStatus', text: '❌ Failed to start containers. See log above.', done: true, ok: false }); + return; + } + + tee('\n✅ Containers started. Waiting for gateway health…\n'); + post({ type: 'provisionStatus', text: 'Waiting for gateway to become healthy…' }); + + // Poll health for up to 60s + const gatewayUrl = 'http://127.0.0.1:18789/health'; + let healthy = false; + for (let i = 0; i < 30; i++) { + await new Promise(r => setTimeout(r, 2000)); + try { + const resp = await fetch(gatewayUrl); + if (resp.ok) { healthy = true; break; } + } catch { /* not ready yet */ } + tee(i % 5 === 0 ? `⏳ Waiting for gateway… (${i * 2}s)\n` : ''); + } + + if (!healthy) { + tee('\n⚠️ Gateway did not respond on /health within 60s. Containers may still be starting.\n'); + } else { + tee('\n✅ Gateway is healthy!\n'); + } + + // Write openclaw.json with gateway config if it doesn't already have one + const openclawJson = path.join(expandedDataPath, 'openclaw.json'); + if (!fs.existsSync(openclawJson)) { + try { + fs.writeFileSync(openclawJson, JSON.stringify({ + gateway: { host: '127.0.0.1', port: 18789 }, + }, null, 2), 'utf8'); + tee('✅ Created openclaw.json with gateway config\n'); + } catch (e) { + tee(`⚠️ Could not write openclaw.json: ${e}\n`); + } + } + + // Create Desktop shortcut + await HomePanel.createDesktopShortcut(expandedDataPath); + + post({ type: 'provisionStatus', text: healthy ? '✅ Docker environment is ready!' : '⚠️ Containers started (gateway health check timed out)', done: true, ok: true }); + } + + /** + * Tears down the Docker environment: `docker compose down`. + */ + public static async runDockerTeardown(extensionPath: string, runtime: 'docker' | 'podman' = 'docker'): Promise { + const composeFile = path.join(extensionPath, '..', '..', '..', '..', 'docker', 'docker-compose.full.yml'); + let resolvedCompose = composeFile; + try { resolvedCompose = fs.realpathSync(composeFile); } catch { /* use original */ } + if (!fs.existsSync(resolvedCompose)) return; + + const cliCmd = runtime === 'podman' ? 'podman' : 'docker'; + await new Promise(resolve => { + cp.spawn(cliCmd, ['compose', '-f', resolvedCompose, 'down'], { + stdio: 'ignore', + }).on('close', () => resolve()).on('error', () => resolve()); + }); + } } From 11bd9605072e297126b499a0e653cc6a110f2386 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 20:14:54 +0000 Subject: [PATCH 03/27] chore(ticket-021): update prd.md with completed task statuses (Tasks 1-5) Co-Authored-By: Claude Sonnet 4.6 --- .../ticket-021-docker-bootstrap-setup/prd.md | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 .tickets/ticket-021-docker-bootstrap-setup/prd.md diff --git a/.tickets/ticket-021-docker-bootstrap-setup/prd.md b/.tickets/ticket-021-docker-bootstrap-setup/prd.md new file mode 100644 index 00000000..d6e6f0af --- /dev/null +++ b/.tickets/ticket-021-docker-bootstrap-setup/prd.md @@ -0,0 +1,233 @@ +# PRD: Ticket 021 - Docker Bootstrap Setup (One-Click Containerized Environment) + +## 1. Problem Statement + +New users of OCCode face significant friction when setting up the full local development environment. They must manually install Node.js, PostgreSQL, Redis, the OpenClaw gateway, and configure environment variables. Even with the Developer Quickstart guide, this process is error-prone and intimidating for non-technical users. We need a **bootstrap application** that offers a **Docker-based setup** as the primary installation path: one click, and the entire stack is provisioned in isolated, consistent containers. This Docker setup should be presented as an option alongside a "Local Setup" advanced option when users first launch the app. The Docker flow must automatically detect the user's platform, verify Docker availability, and if needed, guide them to install Docker. Once Docker is present, it should pull the necessary images, create volumes, initialize configuration, start services, and seamlessly take the user to the OpenClaw dashboard ready for use. + +## 2. Proposed Solution + +Implement a **Bootstrap Wizard** in the OCCode Home panel that runs on first launch (or via a "Reset Setup" command). The wizard presents two primary options: + +- **Docker Setup (Recommended)** — provisions everything in Docker containers +- **Local Setup (Advanced)** — manual installation for developers who prefer their own environment + +### Docker Setup Flow + +1. **Platform Detection & Docker Check** + - Detect OS: Windows, macOS, or Linux + - Check if Docker is installed and running: + - Windows: Check for Docker Desktop + WSL2 integration + - macOS: Check for Docker Desktop + - Linux: Check for `dockerd` service or `docker` CLI + - If Docker not detected: + - Show clear instructions with links to download Docker Desktop (Windows/macOS) or install Docker Engine (Linux) + - Provide a "I've installed Docker, retry" button after user confirms + - If Docker detected but not running, prompt to start Docker Desktop + +2. **Docker Environment Provisioning** + - Use a `docker-compose.full.yml` (or generate dynamically) that defines: + - `openclaw-gateway` service (official `openclaw/gateway` image) + - `postgres` service (PostgreSQL 16) + - `redis` service (optional caching) + - `backend` service (OCC backend API at `occ.mba.sh` or local mock for dev) + - Pull images (show progress) + - Create named volumes for persistence: + - `openclaw_data`: for `~/.openclaw` inside container + - `postgres_data`: database storage + - Initialize PostgreSQL if empty (run migrations automatically) + - Seed initial data (admin user, credits) + +3. **Configuration & Connection** + - Write `openclaw.json` in host user directory (`~/.openclaw`) to point gateway to Docker network: + ```json + { + "gateway": { + "customBaseUrl": "http://localhost:3001" // backend API + } + } + ``` + - Ensure extension's `globalState` is configured to use local Docker-based backend (or auto-detect) + - Wait for all services to become healthy (`docker compose ps` check) + - Verify gateway is running: `openclaw gateway status` + +4. **User Onboarding Completion** + - Mark setup as complete in `globalState` (so wizard doesn't show again) + - Transition Home panel to the "Dashboard" view showing: + - Gateway status: Running + - Balance (if authenticated) + - Quick links: "Open Dashboard", "Start Chatting", "Manage Account" + - Optionally auto-open browser to OpenClaw dashboard (`http://localhost:3000` or similar) + +### Fallback & Error Handling + +- If any step fails (Docker errors, port conflicts, network issues): + - Show a detailed error card with "Retry" and "Show logs" buttons + - Offer "Switch to Local Setup" as fallback + - Log full error to developer console and allow copying to clipboard +- If user cancels mid-flow, clean up partially created containers/volumes or leave them for retry (idempotent) + +### Local Setup Option + +- Provide a condensed version of the Developer Quickstart (ticket-020) for users who want to run services directly on host +- Include link to full `DEVELOPERS.md` for detailed instructions +- Still automated where possible (scripts to install Node, DB, etc.) but more manual intervention required + +## 3. Acceptance Criteria + +- On first launch (or via explicit "Setup" action), the Home panel shows a Bootstrap Wizard with two clear options: "Docker Setup (Recommended)" and "Local Setup (Advanced)" +- Docker Setup button initiates the provisioning flow +- The app correctly detects Docker presence on Windows (Docker Desktop), macOS (Docker Desktop), and Linux (docker CLI/daemon) +- If Docker is missing, the wizard shows platform-specific instructions and download links, with a retry button +- When Docker is present, the wizard: + - Pulls all required images (with visible progress indicator) + - Creates `docker-compose` network and volumes + - Starts all services and waits for health (gateway returns 200 on `/health`) + - Creates or updates `~/.openclaw/openclaw.json` with correct gateway configuration + - Confirms gateway status is "Running" +- After successful Docker setup, the Home panel switches to the Dashboard view showing the OpenClaw agent status and balance +- The entire flow is fully automated after Docker is confirmed; user only clicks buttons and watches progress +- Errors are captured and presented with actionable recovery options; no silent failures +- The wizard can be re-run (e.g., from a "Reset Setup" command) to tear down and recreate the environment from scratch +- The setup is idempotent: running it multiple times does not create duplicate containers or corrupt data +- All Docker resources (containers, networks, volumes) are named with a clear prefix like `occ-` to avoid collisions + +## 4. Technical Considerations + +- **Docker Compose**: Use a version-compatible `docker-compose.yml` (v3.8+) that works with Docker Desktop and Docker Engine. Define services, networks, volumes, healthchecks. +- **Platform-specific detection**: + - Windows: Check registry or process `Docker Desktop.exe`; also check WSL2 integration via `wsl -l -v` if needed + - macOS: Check `docker version` and `osascript` to see if Docker Desktop app is running + - Linux: `systemctl is-active docker` or `docker info` +- **Privilege escalation**: Starting Docker on Windows/macOS may require user to unlock Docker Desktop (it runs as a privileged service but UI may be locked). Provide instructions: "Please open Docker Desktop and click Start" +- **Port conflicts**: If ports 3000, 3001, etc. are already in use, either choose alternate ports via environment variables or fail with clear message to free ports +- **Resource requirements**: Docker setup needs ~2GB RAM and 10GB disk. Warn user if system resources are low. +- **Volume naming**: Use `occ-openclaw-data`, `occ-postgres-data` to avoid conflicts with other projects +- **Container orchestration**: Use `docker-compose up -d` to start in detached mode; `docker-compose logs -f` to stream logs to the wizard UI (show real-time output) +- **Health checks**: Each service should have a healthcheck directive in compose file. Gateway: `openclaw gateway health` or `curl http://localhost:3000/health`. Backend: `GET /health`. +- **Configuration persistence**: The `openclaw.json` should be written to the host's `~/.openclaw/` so it survives container recreation. Inside gateway container, it will mount this volume. +- **Uninstall / Reset**: Provide a "Tear Down" button that runs `docker compose down -v` to remove containers and networks (optionally preserve volumes with `-v` flag off if user wants to keep data) +- **Telemetry (optional)**: Track adoption of Docker vs Local setup to inform product decisions + +## 5. Dependencies + +- Backend Docker image must exist (either build from `docker-compose.yml` in backend repo or use prebuilt `ghcr.io/openclaw/gateway:latest`) +- Docker Compose must be installed (v2+). On Windows/macOS, it's included with Docker Desktop. +- OpenClaw gateway Docker image tag should be version-pinned for stability + +## 6. Subtask Checklist + +- [x] Task 1: Design Docker Compose configuration + - **Problem**: Define all services needed for OCC full stack + - **Test**: `docker compose -f docker-compose.full.yml up` brings up all services without manual intervention + - **Subtasks**: + - [x] Subtask 1.1: Create `docker/docker-compose.full.yml` with services: + - `occ-gateway` (image: `openclaw/pod:latest`) + - `occ-postgres` (image: `postgres:16-alpine`, with volume, env `POSTGRES_PASSWORD`, `POSTGRES_DB=openclaw`) + - `occ-redis` (image: `redis:7-alpine`) + - [x] Subtask 1.2: Define networks: `occ-network` (bridge) + - [x] Subtask 1.3: Define volumes: + - `occ-openclaw-data` (bind-mount from `${OPENCLAW_DATA_DIR:-~/.openclaw}` to `/root/.openclaw`) + - `occ-postgres-data` (mount to `/var/lib/postgresql/data`) + - [x] Subtask 1.4: Add healthcheck to each service: + - Gateway: `curl -f http://localhost:18789/health` + - Postgres: `pg_isready -U openclaw` + - Redis: `redis-cli ping` + - [x] Subtask 1.5: Ensure service startup order: `depends_on` with condition `service_healthy` for gateway waiting for postgres and redis + +- [x] Task 2: Implement Docker detection module in extension + - **Problem**: Determine if Docker is available and running on the host + - **Test**: On Windows with Docker Desktop closed → "Docker not detected"; on Linux with docker running → "Docker ready" + - **Subtasks**: + - [x] Subtask 2.1: Write TypeScript function `detectDockerEnvironment()` in `home.ts` returning checklist items with status, allPassed, guide, runtime + - [x] Subtask 2.2: Platform-specific checks: + - All: try `docker --version` then `podman --version` as fallback + - Daemon: `docker info` / `podman info` — returns running=false if daemon not accessible + - Port 18789 availability check via net.createServer + - Compose: `docker compose version` then `docker-compose --version` fallback + - [x] Subtask 2.3: Return fail status if CLI exists but daemon not accessible + - [ ] Subtask 2.4: Cache detection result for a short period (5 minutes) to avoid repeated heavy checks + +- [x] Task 3: Create Bootstrap Wizard UI component + - **Problem**: Show setup options and progress to user + - **Test**: Home panel initially shows wizard; after completion, switches to dashboard + - **Subtasks**: + - [x] Subtask 3.1: Bootstrap wizard panels in `_getSetupHtml()`: + - `panel-bootstrap-choice`: Welcome with two cards (Docker Recommended / Local Advanced) + - `panel-docker-path`: Configurable data directory input with default per OS + - `panel-docker-doctor`: Live dependency checklist with spinner per item + - `panel-docker-provision`: Streaming log panel + status + actions + - [x] Subtask 3.2: Implement step navigation (forward, back, cancel) via `showBootstrapChoice`, `chooseLocal`, `chooseDocker`, `confirmDockerPath`, `dockerRetry`, `dockerCancel` + - [x] Subtask 3.3: Styled to match OCCode branding (red accent cards, dark panels, consistent fonts) + - [x] Subtask 3.4: Cancel button available; `dockerCancel` command runs compose down and returns to choice + +- [x] Task 4: Implement Docker provisioning engine (backend side) + - **Problem**: Execute Docker commands and stream output to UI + - **Test**: Clicking "Start Docker Setup" runs compose up and streams logs; UI shows each line + - **Subtasks**: + - [x] Subtask 4.1: `runDockerProvision()` static method in `HomePanel` spawns `docker compose up -d` with stdout/stderr streamed via `postMessage provisionLog` + - [x] Subtask 4.2: `runDockerTeardown()` runs `docker compose down` for cancel/reset + - [x] Subtask 4.3: Health check polling: every 2s for 60s, fetch `http://127.0.0.1:18789/health`; reports progress + - [x] Subtask 4.4: Non-zero exit from spawn aborts with error status sent to UI + - [x] Subtask 4.5: After healthy, writes `~/.openclaw/openclaw.json` with `{ gateway: { host: "127.0.0.1", port: 18789 } }` if not already present + - [ ] Subtask 4.6: `openclaw gateway status` verification — deferred (gateway runs inside container, not host CLI) + +- [x] Task 5: Platform-specific Docker installation guidance + - **Problem**: Users without Docker need clear instructions + - **Test**: On Windows with no Docker, wizard shows: "Download Docker Desktop for Windows" with link; macOS similar; Linux shows `apt-get install docker.io docker-compose` + - **Subtasks**: + - [x] Subtask 5.1: Windows: guide with Docker Desktop link in `detectDockerEnvironment()` when CLI not found + - [x] Subtask 5.2: macOS: guide with Docker Desktop link + - [x] Subtask 5.3: Linux: `apt-get install docker.io docker-compose-v2` + `systemctl` + `usermod -aG docker $USER` instructions; also mentions Podman as alternative + - [x] Subtask 5.4: "↻ Retry Check" button shown when doctor detects a failure + +- [ ] Task 6: Local Setup option integration + - **Problem**: Provide alternative for developers who don't want Docker + - **Test**: Clicking "Local Setup" opens a webview or panel with step-by-step instructions and possibly automated scripts + - **Subtasks**: + - [ ] Subtask 6.1: Create `LocalSetupGuide` component that displays the Developer Quickstart (ticket-020)文档 in condensed form + - [ ] Subtask 6.2: Offer buttons to run individual setup scripts: "Install OpenClaw CLI", "Start Database", "Run Backend", "Launch Editor" + - [ ] Subtask 6.3: Each button spawns a terminal process (or uses VS Code terminal API) to execute commands, streaming output to panel + - [ ] Subtask 6.4: After all steps complete, "Go to Dashboard" appears + +- [ ] Task 7: Reset and teardown functionality + - **Problem**: User may want to start over or uninstall + - **Test**: "Reset Setup" command tears down Docker environment and returns to wizard Step 0; also clears `~/.openclaw` optionally + - **Subtasks**: + - [ ] Subtask 7.1: Add command `occ.setup.reset` that: + - If Docker environment exists: `docker compose -f down -v` (with confirmation) + - Remove `~/.openclaw/openclaw.json` (or backup) + - Reset `globalState` flag `setupCompleted = false` + - Reopen Home panel to wizard Step 0 + - [ ] Subtask 7.2: In wizard, always show "Cancel / Reset" button in top-right; on click, show confirmation dialog with options: "Cancel and keep data" vs "Reset and delete everything" + - [ ] Subtask 7.3: If user chooses full reset, also delete Docker volumes: `docker volume rm occ-openclaw-data occ-postgres-data` (after compose down) + +- [ ] Task 8: Testing (unit + integration) + - **Problem**: Ensure setup flow works across platforms and handles failures gracefully + - **Test**: Automated and manual tests cover detection, provisioning, errors, reset + - **Subtasks**: + - [ ] Subtask 8.1: Unit tests for `detectDocker()` mocking platform and docker CLI responses + - [ ] Subtask 8.2: Integration test with a Docker-in-Docker (DinD) container or local Docker daemon: + - Simulate full wizard flow: detection → compose up → health → completion + - Verify containers are running: `docker ps` shows `occ-` services + - Verify gateway responds on `http://localhost:3000/health` + - [ ] Subtask 8.3: Test failure scenarios: Docker not installed, compose file invalid, port conflict, image pull failure + - [ ] Subtable 8.4: Test cancellation mid-flow: ensure containers are cleaned up (or left in known state) + - [ ] Subtask 8.5: Test reset flow: after reset, wizard shows again and can re-provision cleanly + +- [ ] Task 9: Documentation and user guidance + - **Problem**: Users need to understand what's happening during setup + - **Test**: Documentation explains Docker setup, requirements, troubleshooting + - **Subtasks**: + - [ ] Subtask 9.1: Add section to `README.md` and `docs/setup.md` describing Docker-based installation + - [ ] Subtask 9.2: Include system requirements: Docker Desktop 4.0+, 4GB RAM, 10GB disk, internet for initial image pull + - [ ] Subtask 9.3: Troubleshooting guide: common issues (Docker not starting, permission denied, WSL2 not installed on Windows, port conflicts) with solutions + - [ ] Subtask 9.4: Mention that Local Setup is available for advanced users and link to `DEVELOPERS.md` + +- [ ] Task 10: Accessibility and polish + - **Problem**: Wizard should be usable by all + - **Test**: Screen reader announces steps; keyboard navigation works; colors have contrast + - **Subtasks**: + - [ ] Subtask 10.1: Ensure all buttons have accessible labels; progress announcements via `aria-live` + - [ ] Subtask 10.2: Allow keyboard-only navigation (tab order, Enter to activate) + - [ ] Subtask 10.3: Use high-contrast colors; test with OS accessibility settings + - [ ] Subtask 10.4: Provide "Skip Docker Setup" link at every step in case user wants to exit From 4e17d9535996ef41e9c619c7934e38ad7b4b9408 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 26 Mar 2026 20:21:28 +0000 Subject: [PATCH 04/27] feat(ticket-021): co-locate .openclaw inside OCC install dir (~/Desktop/occ/.openclaw) - getDefaultOpenClawDataPath() now returns ~/Desktop/occ/.openclaw on all platforms instead of platform-specific ~/.openclaw / %APPDATA%\openclaw - HTML path input placeholder updated to ~/Desktop/occ/.openclaw - docker-compose.full.yml: remove ~ fallback (extension always writes .env with expanded path) - createDesktopShortcut(): skip symlink creation when dataPath is already inside ~/Desktop/occ/ (data is co-located, no symlink needed); fix lstat race on missing dir Co-Authored-By: Claude Sonnet 4.6 --- .../extensions/openclaw/src/panels/home.ts | 38 +++++++++++-------- docker/docker-compose.full.yml | 2 +- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/apps/editor/extensions/openclaw/src/panels/home.ts b/apps/editor/extensions/openclaw/src/panels/home.ts index bec30688..da208b89 100644 --- a/apps/editor/extensions/openclaw/src/panels/home.ts +++ b/apps/editor/extensions/openclaw/src/panels/home.ts @@ -1734,7 +1734,7 @@ The binary is already downloaded — do NOT re-download or compile anything.`;
OpenClaw Data Directory
Where should OpenClaw store its data on your machine? This folder will be mounted into the Docker container.
- +
A shortcut will also be created at ~/Desktop/occ