From a023ede926d08848cf79f00c998f7feb2ec01862 Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Mon, 6 Apr 2026 17:31:37 -0600 Subject: [PATCH 1/4] Ability to run within docker container --- README.md | 29 ++++++- docker/Dockerfile | 13 +++ docker/docker-compose.claude.yml | 7 ++ docker/docker-compose.codex.yml | 7 ++ docker/docker-compose.cursor.yml | 7 ++ docker/docker-compose.kiro.yml | 7 ++ docker/docker-compose.opencode.yml | 7 ++ docker/docker-compose.yml | 14 +++ docs/ARCHITECTURE.md | 2 +- in-docker.sh | 134 +++++++++++++++++++++++++++++ src/server.js | 5 +- 11 files changed, 229 insertions(+), 3 deletions(-) create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.claude.yml create mode 100644 docker/docker-compose.codex.yml create mode 100644 docker/docker-compose.cursor.yml create mode 100644 docker/docker-compose.kiro.yml create mode 100644 docker/docker-compose.opencode.yml create mode 100644 docker/docker-compose.yml create mode 100755 in-docker.sh diff --git a/README.md b/README.md index 2956888..7d95e0c 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,26 @@ npm i -g codedash-app codedash run ``` +## Docker + +Docker files live under `docker/`. Use `./in-docker.sh` to select which host agent data sources should be mounted into the container. + +```bash +./in-docker.sh --claude --codex +./in-docker.sh --cursor -- up -d +./in-docker.sh --all -- up --build +``` + +Supported flags: +- `--claude` mounts `~/.claude` to `/root/.claude` +- `--codex` mounts `~/.codex` to `/root/.codex` +- `--cursor` mounts `~/.cursor` to `/root/.cursor` +- `--opencode` mounts `~/.local/share/opencode/opencode.db` +- `--kiro` mounts `~/Library/Application Support/kiro-cli/data.sqlite3` +- `--all` enables every supported mount + +The base container always listens on `0.0.0.0:3847`, so it can be reached via the host machine LAN IP when Docker publishes that port. + ## Supported Agents | Agent | Sessions | Preview | Search | Live Status | Convert | Handoff | Launch | @@ -72,6 +92,13 @@ codedash restart codedash stop ``` +Bind host can be configured with `CODEDASH_HOST`: + +```bash +codedash run +CODEDASH_HOST=0.0.0.0 codedash run +``` + **Keyboard Shortcuts**: `/` search, `j/k` navigate, `Enter` open, `x` star, `d` delete, `s` select, `g` group, `r` refresh, `Esc` close ## Data Sources @@ -84,7 +111,7 @@ codedash stop ~/Library/Application Support/kiro-cli/ Kiro CLI (SQLite) ``` -Zero dependencies. Everything runs on `localhost`. +Zero dependencies. By default everything runs on `localhost`. Set `CODEDASH_HOST=0.0.0.0` to listen on all interfaces, including your host machine LAN IP. ## Install Agents diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..ab7b5b8 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,13 @@ +FROM node:20-alpine + +RUN apk add --no-cache sqlite + +WORKDIR /app + +COPY package.json ./ +COPY bin ./bin +COPY src ./src + +EXPOSE 3847 + +CMD ["node", "bin/cli.js", "run", "--no-browser", "--port=3847"] diff --git a/docker/docker-compose.claude.yml b/docker/docker-compose.claude.yml new file mode 100644 index 0000000..14a8bb8 --- /dev/null +++ b/docker/docker-compose.claude.yml @@ -0,0 +1,7 @@ +services: + codedash: + volumes: + - type: bind + source: ${HOST_CLAUDE_DIR} + target: /root/.claude + read_only: true diff --git a/docker/docker-compose.codex.yml b/docker/docker-compose.codex.yml new file mode 100644 index 0000000..57e6203 --- /dev/null +++ b/docker/docker-compose.codex.yml @@ -0,0 +1,7 @@ +services: + codedash: + volumes: + - type: bind + source: ${HOST_CODEX_DIR} + target: /root/.codex + read_only: true diff --git a/docker/docker-compose.cursor.yml b/docker/docker-compose.cursor.yml new file mode 100644 index 0000000..fd4de5b --- /dev/null +++ b/docker/docker-compose.cursor.yml @@ -0,0 +1,7 @@ +services: + codedash: + volumes: + - type: bind + source: ${HOST_CURSOR_DIR} + target: /root/.cursor + read_only: true diff --git a/docker/docker-compose.kiro.yml b/docker/docker-compose.kiro.yml new file mode 100644 index 0000000..1b59471 --- /dev/null +++ b/docker/docker-compose.kiro.yml @@ -0,0 +1,7 @@ +services: + codedash: + volumes: + - type: bind + source: ${HOST_KIRO_DB} + target: /root/Library/Application Support/kiro-cli/data.sqlite3 + read_only: true diff --git a/docker/docker-compose.opencode.yml b/docker/docker-compose.opencode.yml new file mode 100644 index 0000000..c0bb138 --- /dev/null +++ b/docker/docker-compose.opencode.yml @@ -0,0 +1,7 @@ +services: + codedash: + volumes: + - type: bind + source: ${HOST_OPENCODE_DB} + target: /root/.local/share/opencode/opencode.db + read_only: true diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..de94762 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,14 @@ +services: + codedash: + image: ghcr.io/vakovalskii/codedash + build: + context: .. + dockerfile: docker/Dockerfile + container_name: codedash + restart: unless-stopped + environment: + HOME: /root + CODEDASH_HOST: 0.0.0.0 + CODEDASH_LOG: "1" + ports: + - "3847:3847" diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 1343794..548be2a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,7 +2,7 @@ ## Overview -CodeDash is a zero-dependency Node.js dashboard for AI coding agent sessions. Supports 6 agents: Claude Code, Claude Extension, Codex, Cursor, OpenCode, Kiro. Single process serves a web UI at `localhost:3847`. +CodeDash is a zero-dependency Node.js dashboard for AI coding agent sessions. Supports 6 agents: Claude Code, Claude Extension, Codex, Cursor, OpenCode, Kiro. Single process serves a web UI at `localhost:3847` by default, or another bind host via `CODEDASH_HOST`. ``` Browser (localhost:3847) Node.js Server diff --git a/in-docker.sh b/in-docker.sh new file mode 100755 index 0000000..f016b6c --- /dev/null +++ b/in-docker.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +DOCKER_DIR="$SCRIPT_DIR/docker" + +BASE_COMPOSE="$DOCKER_DIR/docker-compose.yml" + +USE_CLAUDE=0 +USE_CODEX=0 +USE_CURSOR=0 +USE_KIRO=0 +USE_OPENCODE=0 + +print_usage() { + cat <<'EOF' +Usage: ./in-docker.sh [options] [-- docker-compose-args...] + +Options: + --claude Mount ~/.claude + --codex Mount ~/.codex + --cursor Mount ~/.cursor + --kiro Mount ~/Library/Application Support/kiro-cli/data.sqlite3 + --opencode Mount ~/.local/share/opencode/opencode.db + --all Enable all supported mounts + -h, --help Show this help + +Examples: + ./in-docker.sh --claude --codex + ./in-docker.sh --all -- up --build + ./in-docker.sh --cursor -- up -d + +If no docker-compose args are provided, the script runs: + docker compose ... up --build +EOF +} + +require_path() { + label=$1 + path_value=$2 + if [ ! -e "$path_value" ]; then + printf '%s\n' "Missing $label path: $path_value" >&2 + exit 1 + fi +} + +while [ $# -gt 0 ]; do + case "$1" in + --claude) + USE_CLAUDE=1 + shift + ;; + --codex) + USE_CODEX=1 + shift + ;; + --cursor) + USE_CURSOR=1 + shift + ;; + --kiro) + USE_KIRO=1 + shift + ;; + --opencode) + USE_OPENCODE=1 + shift + ;; + --all) + USE_CLAUDE=1 + USE_CODEX=1 + USE_CURSOR=1 + USE_KIRO=1 + USE_OPENCODE=1 + shift + ;; + -h|--help) + print_usage + exit 0 + ;; + --) + shift + break + ;; + *) + break + ;; + esac +done + +compose_files=(-f "$BASE_COMPOSE") +compose_args=("$@") + +if [ "${#compose_args[@]}" -eq 0 ]; then + compose_args=(up --build) +fi + +if [ "$USE_CLAUDE" -eq 1 ]; then + HOST_CLAUDE_DIR="${HOST_CLAUDE_DIR:-$HOME/.claude}" + require_path "Claude" "$HOST_CLAUDE_DIR" + export HOST_CLAUDE_DIR + compose_files+=(-f "$DOCKER_DIR/docker-compose.claude.yml") +fi + +if [ "$USE_CODEX" -eq 1 ]; then + HOST_CODEX_DIR="${HOST_CODEX_DIR:-$HOME/.codex}" + require_path "Codex" "$HOST_CODEX_DIR" + export HOST_CODEX_DIR + compose_files+=(-f "$DOCKER_DIR/docker-compose.codex.yml") +fi + +if [ "$USE_CURSOR" -eq 1 ]; then + HOST_CURSOR_DIR="${HOST_CURSOR_DIR:-$HOME/.cursor}" + require_path "Cursor" "$HOST_CURSOR_DIR" + export HOST_CURSOR_DIR + compose_files+=(-f "$DOCKER_DIR/docker-compose.cursor.yml") +fi + +if [ "$USE_KIRO" -eq 1 ]; then + HOST_KIRO_DB="${HOST_KIRO_DB:-$HOME/Library/Application Support/kiro-cli/data.sqlite3}" + require_path "Kiro" "$HOST_KIRO_DB" + export HOST_KIRO_DB + compose_files+=(-f "$DOCKER_DIR/docker-compose.kiro.yml") +fi + +if [ "$USE_OPENCODE" -eq 1 ]; then + HOST_OPENCODE_DB="${HOST_OPENCODE_DB:-$HOME/.local/share/opencode/opencode.db}" + require_path "OpenCode" "$HOST_OPENCODE_DB" + export HOST_OPENCODE_DB + compose_files+=(-f "$DOCKER_DIR/docker-compose.opencode.yml") +fi + +exec docker compose "${compose_files[@]}" "${compose_args[@]}" diff --git a/src/server.js b/src/server.js index 7dfcd0c..4e28af8 100644 --- a/src/server.js +++ b/src/server.js @@ -12,6 +12,7 @@ const { getHTML } = require('./html'); // ── Logging ────────────────────────────────── const LOG_VERBOSE = process.env.CODEDASH_LOG !== '0'; +const DEFAULT_HOST = '127.0.0.1'; function log(tag, msg, data) { if (!LOG_VERBOSE && tag !== 'ERROR') return; @@ -26,6 +27,7 @@ function log(tag, msg, data) { } function startServer(port, openBrowser = true) { + const host = process.env.CODEDASH_HOST || DEFAULT_HOST; const server = http.createServer((req, res) => { const parsed = new URL(req.url, `http://localhost:${port}`); const pathname = parsed.pathname; @@ -370,9 +372,10 @@ function startServer(port, openBrowser = true) { } }); - server.listen(port, '127.0.0.1', () => { + server.listen(port, host, () => { console.log(''); console.log(' \x1b[36m\x1b[1mcodedash\x1b[0m — Claude & Codex Sessions Dashboard'); + console.log(` \x1b[2mbind ${host}:${port}\x1b[0m`); console.log(` \x1b[2mhttp://localhost:${port}\x1b[0m`); console.log(' \x1b[2mPress Ctrl+C to stop\x1b[0m'); console.log(''); From 0839ed61ba476f423d7d765e4323af9b9d5f8f70 Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Mon, 6 Apr 2026 18:50:21 -0600 Subject: [PATCH 2/4] copy to clipboard in insecure environment --- src/frontend/app.js | 62 +++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/src/frontend/app.js b/src/frontend/app.js index 3519af5..e0b7d8b 100644 --- a/src/frontend/app.js +++ b/src/frontend/app.js @@ -87,6 +87,43 @@ function showToast(msg) { setTimeout(() => el.classList.remove('show'), 2500); } +function fallbackCopyText(text) { + try { + var ta = document.createElement('textarea'); + ta.value = text; + ta.setAttribute('readonly', ''); + ta.style.position = 'fixed'; + ta.style.top = '-9999px'; + ta.style.left = '-9999px'; + document.body.appendChild(ta); + ta.focus(); + ta.select(); + var ok = document.execCommand('copy'); + document.body.removeChild(ta); + return ok; + } catch (e) { + return false; + } +} + +function copyText(text, successMsg) { + var done = function() { + showToast(successMsg || ('Copied: ' + text)); + return true; + }; + var fail = function() { + if (fallbackCopyText(text)) return done(); + prompt('Copy this command:', text); + showToast(window.isSecureContext ? 'Clipboard copy failed' : 'Clipboard unavailable on non-secure origin'); + return false; + }; + + if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { + return navigator.clipboard.writeText(text).then(done).catch(fail); + } + return Promise.resolve(fail()); +} + function formatBytes(bytes) { if (!bytes || bytes < 1024) return (bytes || 0) + ' B'; if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'; @@ -1358,12 +1395,7 @@ function copyResume(sessionId, tool) { } else { cmd = 'claude --resume ' + sessionId; } - navigator.clipboard.writeText(cmd).then(function() { - showToast('Copied: ' + cmd); - }).catch(function() { - // Fallback - prompt('Copy this command:', cmd); - }); + copyText(cmd, 'Copied: ' + cmd); } function exportMd(sessionId, project) { @@ -2038,7 +2070,7 @@ function installAgent(agent) { var overlay = document.getElementById('confirmOverlay'); document.getElementById('confirmTitle').textContent = 'Install ' + info.name; - var html = '' + escHtml(info.cmd) + ''; + var html = '' + escHtml(info.cmd) + ''; if (info.alt) { html += 'or: ' + escHtml(info.alt) + '
'; } @@ -2048,9 +2080,7 @@ function installAgent(agent) { document.getElementById('confirmAction').textContent = 'Copy Install Command'; document.getElementById('confirmAction').className = 'launch-btn btn-primary'; document.getElementById('confirmAction').onclick = function() { - navigator.clipboard.writeText(info.cmd).then(function() { - showToast('Copied: ' + info.cmd); - }); + copyText(info.cmd, 'Copied: ' + info.cmd); closeConfirm(); }; if (overlay) overlay.style.display = 'flex'; @@ -2072,9 +2102,7 @@ function showExportDialog() { document.getElementById('confirmAction').textContent = 'Copy Export Command'; document.getElementById('confirmAction').className = 'launch-btn btn-primary'; document.getElementById('confirmAction').onclick = function() { - navigator.clipboard.writeText('codedash export').then(function() { - showToast('Copied: codedash export'); - }); + copyText('codedash export', 'Copied: codedash export'); closeConfirm(); }; if (overlay) overlay.style.display = 'flex'; @@ -2105,9 +2133,7 @@ async function checkForUpdates() { badge.classList.add('update-available'); badge.title = 'Click to copy update command'; badge.onclick = function() { - navigator.clipboard.writeText('npm i -g codedash-app@latest').then(function() { - showToast('Copied: npm i -g codedash-app@latest'); - }); + copyText('npm i -g codedash-app@latest', 'Copied: npm i -g codedash-app@latest'); }; } var banner = document.getElementById('updateBanner'); @@ -2123,9 +2149,7 @@ async function checkForUpdates() { function copyUpdate() { var cmd = 'codedash update && codedash restart'; - navigator.clipboard.writeText(cmd).then(function() { - showToast('Copied: ' + cmd + ' (run in terminal)'); - }); + copyText(cmd, 'Copied: ' + cmd + ' (run in terminal)'); } function dismissUpdate() { From 83e2a8bb60526820f7012923ab787249c8fa9cc8 Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Tue, 7 Apr 2026 00:09:49 -0600 Subject: [PATCH 3/4] code review comments addressed --- README.md | 1 + docker/docker-compose.claude.yml | 1 - docker/docker-compose.codex.yml | 1 - docker/docker-compose.cursor.yml | 1 - docker/docker-compose.kiro.yml | 1 - docker/docker-compose.opencode.yml | 1 - src/server.js | 29 +++++++++++++++++++++++++---- 7 files changed, 26 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7d95e0c..604ca9c 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Supported flags: - `--all` enables every supported mount The base container always listens on `0.0.0.0:3847`, so it can be reached via the host machine LAN IP when Docker publishes that port. +Mounted agent data sources are writable inside the container so CodeDash actions like delete, convert, and settings updates work the same way as a host run. ## Supported Agents diff --git a/docker/docker-compose.claude.yml b/docker/docker-compose.claude.yml index 14a8bb8..f6ffe98 100644 --- a/docker/docker-compose.claude.yml +++ b/docker/docker-compose.claude.yml @@ -4,4 +4,3 @@ services: - type: bind source: ${HOST_CLAUDE_DIR} target: /root/.claude - read_only: true diff --git a/docker/docker-compose.codex.yml b/docker/docker-compose.codex.yml index 57e6203..0158dd9 100644 --- a/docker/docker-compose.codex.yml +++ b/docker/docker-compose.codex.yml @@ -4,4 +4,3 @@ services: - type: bind source: ${HOST_CODEX_DIR} target: /root/.codex - read_only: true diff --git a/docker/docker-compose.cursor.yml b/docker/docker-compose.cursor.yml index fd4de5b..60aad96 100644 --- a/docker/docker-compose.cursor.yml +++ b/docker/docker-compose.cursor.yml @@ -4,4 +4,3 @@ services: - type: bind source: ${HOST_CURSOR_DIR} target: /root/.cursor - read_only: true diff --git a/docker/docker-compose.kiro.yml b/docker/docker-compose.kiro.yml index 1b59471..0e86d93 100644 --- a/docker/docker-compose.kiro.yml +++ b/docker/docker-compose.kiro.yml @@ -4,4 +4,3 @@ services: - type: bind source: ${HOST_KIRO_DB} target: /root/Library/Application Support/kiro-cli/data.sqlite3 - read_only: true diff --git a/docker/docker-compose.opencode.yml b/docker/docker-compose.opencode.yml index c0bb138..33b52a0 100644 --- a/docker/docker-compose.opencode.yml +++ b/docker/docker-compose.opencode.yml @@ -4,4 +4,3 @@ services: - type: bind source: ${HOST_OPENCODE_DB} target: /root/.local/share/opencode/opencode.db - read_only: true diff --git a/src/server.js b/src/server.js index 4e28af8..2744e38 100644 --- a/src/server.js +++ b/src/server.js @@ -2,7 +2,7 @@ const http = require('http'); const https = require('https'); const { URL } = require('url'); -const { exec } = require('child_process'); +const { exec, execFile } = require('child_process'); const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions, getSessionReplay, getCostAnalytics, computeSessionCost } = require('./data'); const { detectTerminals, openInTerminal, focusTerminalByPid } = require('./terminals'); const { convertSession } = require('./convert'); @@ -28,6 +28,7 @@ function log(tag, msg, data) { function startServer(port, openBrowser = true) { const host = process.env.CODEDASH_HOST || DEFAULT_HOST; + const browserUrl = getBrowserUrl(host, port); const server = http.createServer((req, res) => { const parsed = new URL(req.url, `http://localhost:${port}`); const pathname = parsed.pathname; @@ -376,15 +377,15 @@ function startServer(port, openBrowser = true) { console.log(''); console.log(' \x1b[36m\x1b[1mcodedash\x1b[0m — Claude & Codex Sessions Dashboard'); console.log(` \x1b[2mbind ${host}:${port}\x1b[0m`); - console.log(` \x1b[2mhttp://localhost:${port}\x1b[0m`); + console.log(` \x1b[2m${browserUrl}\x1b[0m`); console.log(' \x1b[2mPress Ctrl+C to stop\x1b[0m'); console.log(''); if (openBrowser) { if (process.platform === 'darwin') { - exec(`open http://localhost:${port}`); + execFile('open', [browserUrl]); } else if (process.platform === 'linux') { - exec(`xdg-open http://localhost:${port}`); + execFile('xdg-open', [browserUrl]); } } }); @@ -402,6 +403,26 @@ function readBody(req, cb) { req.on('end', () => cb(body)); } +function getBrowserUrl(host, port) { + const browserHost = getBrowserHost(host); + const wrappedHost = browserHost.includes(':') && !browserHost.startsWith('[') + ? `[${browserHost}]` + : browserHost; + return `http://${wrappedHost}:${port}`; +} + +function getBrowserHost(host) { + if (!host || host === DEFAULT_HOST || host === 'localhost' || host === '::1') { + return 'localhost'; + } + if (host === '0.0.0.0' || host === '::' || host === '[::]') { + // This URL is only used to show/open the app locally on the machine that started it. + // Wildcard bind addresses are valid listen targets, but they are not usable browser hosts. + return 'localhost'; + } + return host; +} + // ── npm version check ─────────────────── function fetchLatestVersion(packageName) { return new Promise((resolve, reject) => { From 28845454c3e57b9b9ead1b66496ccec86c1f37d1 Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Tue, 7 Apr 2026 08:25:22 -0600 Subject: [PATCH 4/4] bind address clarification comment --- src/server.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/server.js b/src/server.js index f82c7a5..fb1f2aa 100644 --- a/src/server.js +++ b/src/server.js @@ -29,6 +29,8 @@ function log(tag, msg, data) { function startServer(host, port, openBrowser = true) { const browserUrl = getBrowserUrl(host, port); const server = http.createServer((req, res) => { + // req.url is usually relative, so this base is only for URL parsing. + // Keep it stable instead of reusing the bind host, which may be a wildcard listen address. const parsed = new URL(req.url, `http://localhost:${port}`); const pathname = parsed.pathname; const reqStart = Date.now();