diff --git a/Dockerfile b/Dockerfile index 84ddc49a..d4675da5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,10 +8,12 @@ # ============================================================================= # BASE STAGE - Common setup for all builds (DRY: defined once, used by all) # ============================================================================= -FROM node:22-alpine AS base +FROM node:22-slim AS base # Install build dependencies for native modules (node-pty) -RUN apk add --no-cache python3 make g++ +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3 make g++ \ + && rm -rf /var/lib/apt/lists/* WORKDIR /app @@ -51,32 +53,59 @@ RUN npm run build:packages && npm run build --workspace=apps/server # ============================================================================= # SERVER PRODUCTION STAGE # ============================================================================= -FROM node:22-alpine AS server +FROM node:22-slim AS server -# Install git, curl, bash (for terminal), su-exec (for user switching), and GitHub CLI (pinned version, multi-arch) -RUN apk add --no-cache git curl bash su-exec && \ - GH_VERSION="2.63.2" && \ - ARCH=$(uname -m) && \ - case "$ARCH" in \ +# Install git, curl, bash (for terminal), gosu (for user switching), and GitHub CLI (pinned version, multi-arch) +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl bash gosu ca-certificates \ + && GH_VERSION="2.63.2" \ + && ARCH=$(uname -m) \ + && case "$ARCH" in \ x86_64) GH_ARCH="amd64" ;; \ aarch64|arm64) GH_ARCH="arm64" ;; \ *) echo "Unsupported architecture: $ARCH" && exit 1 ;; \ - esac && \ - curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" -o gh.tar.gz && \ - tar -xzf gh.tar.gz && \ - mv gh_${GH_VERSION}_linux_${GH_ARCH}/bin/gh /usr/local/bin/gh && \ - rm -rf gh.tar.gz gh_${GH_VERSION}_linux_${GH_ARCH} + esac \ + && curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_${GH_ARCH}.tar.gz" -o gh.tar.gz \ + && tar -xzf gh.tar.gz \ + && mv gh_${GH_VERSION}_linux_${GH_ARCH}/bin/gh /usr/local/bin/gh \ + && rm -rf gh.tar.gz gh_${GH_VERSION}_linux_${GH_ARCH} \ + && rm -rf /var/lib/apt/lists/* -# Install Claude CLI globally +# Install Claude CLI globally (available to all users via npm global bin) RUN npm install -g @anthropic-ai/claude-code -WORKDIR /app +# Create non-root user with home directory BEFORE installing Cursor CLI +RUN groupadd -g 1001 automaker && \ + useradd -u 1001 -g automaker -m -d /home/automaker -s /bin/bash automaker && \ + mkdir -p /home/automaker/.local/bin && \ + mkdir -p /home/automaker/.cursor && \ + chown -R automaker:automaker /home/automaker && \ + chmod 700 /home/automaker/.cursor -# Create non-root user with home directory -RUN addgroup -g 1001 -S automaker && \ - adduser -S automaker -u 1001 -h /home/automaker && \ - mkdir -p /home/automaker && \ - chown automaker:automaker /home/automaker +# Install Cursor CLI as the automaker user +# Set HOME explicitly and install to /home/automaker/.local/bin/ +USER automaker +ENV HOME=/home/automaker +RUN curl https://cursor.com/install -fsS | bash && \ + echo "=== Checking Cursor CLI installation ===" && \ + ls -la /home/automaker/.local/bin/ && \ + echo "=== PATH is: $PATH ===" && \ + (which cursor-agent && cursor-agent --version) || echo "cursor-agent installed (may need auth setup)" +USER root + +# Add PATH to profile so it's available in all interactive shells (for login shells) +RUN mkdir -p /etc/profile.d && \ + echo 'export PATH="/home/automaker/.local/bin:$PATH"' > /etc/profile.d/cursor-cli.sh && \ + chmod +x /etc/profile.d/cursor-cli.sh + +# Add to automaker's .bashrc for bash interactive shells +RUN echo 'export PATH="/home/automaker/.local/bin:$PATH"' >> /home/automaker/.bashrc && \ + chown automaker:automaker /home/automaker/.bashrc + +# Also add to root's .bashrc since docker exec defaults to root +RUN echo 'export PATH="/home/automaker/.local/bin:$PATH"' >> /root/.bashrc + +WORKDIR /app # Copy root package.json (needed for workspace resolution) COPY --from=server-builder /app/package*.json ./ @@ -111,6 +140,8 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh ENV PORT=3008 ENV DATA_DIR=/data ENV HOME=/home/automaker +# Add user's local bin to PATH for cursor-agent +ENV PATH="/home/automaker/.local/bin:${PATH}" # Expose port EXPOSE 3008 diff --git a/apps/ui/src/components/views/settings-view/hooks/use-cli-status.ts b/apps/ui/src/components/views/settings-view/hooks/use-cli-status.ts index ce067e17..ac97040a 100644 --- a/apps/ui/src/components/views/settings-view/hooks/use-cli-status.ts +++ b/apps/ui/src/components/views/settings-view/hooks/use-cli-status.ts @@ -95,7 +95,7 @@ export function useCliStatus() { checkCliStatus(); }, [setClaudeAuthStatus]); - // Refresh Claude CLI status + // Refresh Claude CLI status and auth status const handleRefreshClaudeCli = useCallback(async () => { setIsCheckingClaudeCli(true); try { @@ -104,12 +104,52 @@ export function useCliStatus() { const status = await api.checkClaudeCli(); setClaudeCliStatus(status); } + // Also refresh auth status + if (api?.setup?.getClaudeStatus) { + try { + const result = await api.setup.getClaudeStatus(); + if (result.success && result.auth) { + const auth = result.auth as typeof result.auth & { + oauthTokenValid?: boolean; + apiKeyValid?: boolean; + }; + const validMethods = [ + 'oauth_token_env', + 'oauth_token', + 'api_key', + 'api_key_env', + 'credentials_file', + 'cli_authenticated', + 'none', + ] as const; + type AuthMethod = (typeof validMethods)[number]; + const method: AuthMethod = validMethods.includes(auth.method as AuthMethod) + ? (auth.method as AuthMethod) + : auth.authenticated + ? 'api_key' + : 'none'; + const authStatus = { + authenticated: auth.authenticated, + method, + hasCredentialsFile: auth.hasCredentialsFile ?? false, + oauthTokenValid: + auth.oauthTokenValid || auth.hasStoredOAuthToken || auth.hasEnvOAuthToken, + apiKeyValid: auth.apiKeyValid || auth.hasStoredApiKey || auth.hasEnvApiKey, + hasEnvOAuthToken: auth.hasEnvOAuthToken, + hasEnvApiKey: auth.hasEnvApiKey, + }; + setClaudeAuthStatus(authStatus); + } + } catch (error) { + logger.error('Failed to refresh Claude auth status:', error); + } + } } catch (error) { logger.error('Failed to refresh Claude CLI status:', error); } finally { setIsCheckingClaudeCli(false); } - }, []); + }, [setClaudeAuthStatus]); return { claudeCliStatus, diff --git a/dev.mjs b/dev.mjs index e6a44c30..ea549c65 100644 --- a/dev.mjs +++ b/dev.mjs @@ -12,6 +12,8 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; +import { statSync } from 'fs'; +import { execSync } from 'child_process'; import { createRestrictedFs, @@ -45,6 +47,89 @@ const processes = { docker: null, }; +/** + * Check if Docker images need to be rebuilt based on Dockerfile or package.json changes + */ +function shouldRebuildDockerImages() { + try { + const dockerfilePath = path.join(__dirname, 'Dockerfile'); + const packageJsonPath = path.join(__dirname, 'package.json'); + + // Get modification times of source files + const dockerfileMtime = statSync(dockerfilePath).mtimeMs; + const packageJsonMtime = statSync(packageJsonPath).mtimeMs; + const latestSourceMtime = Math.max(dockerfileMtime, packageJsonMtime); + + // Get image names from docker-compose config + let serverImageName, uiImageName; + try { + const composeConfig = execSync('docker compose config --format json', { + encoding: 'utf-8', + cwd: __dirname, + }); + const config = JSON.parse(composeConfig); + + // Docker Compose generates image names as _ + // Get project name from config or default to directory name + const projectName = + config.name || + path + .basename(__dirname) + .toLowerCase() + .replace(/[^a-z0-9]/g, ''); + serverImageName = `${projectName}_server`; + uiImageName = `${projectName}_ui`; + } catch (error) { + // Fallback to default naming convention + const projectName = path + .basename(__dirname) + .toLowerCase() + .replace(/[^a-z0-9]/g, ''); + serverImageName = `${projectName}_server`; + uiImageName = `${projectName}_ui`; + } + + // Check if images exist and get their creation times + let needsRebuild = false; + + try { + // Check server image + const serverImageInfo = execSync( + `docker image inspect ${serverImageName} --format "{{.Created}}" 2>/dev/null || echo ""`, + { encoding: 'utf-8', cwd: __dirname } + ).trim(); + + // Check UI image + const uiImageInfo = execSync( + `docker image inspect ${uiImageName} --format "{{.Created}}" 2>/dev/null || echo ""`, + { encoding: 'utf-8', cwd: __dirname } + ).trim(); + + // If either image doesn't exist, we need to rebuild + if (!serverImageInfo || !uiImageInfo) { + return true; + } + + // Parse image creation times (ISO 8601 format) + const serverCreated = new Date(serverImageInfo).getTime(); + const uiCreated = new Date(uiImageInfo).getTime(); + const oldestImageTime = Math.min(serverCreated, uiCreated); + + // If source files are newer than images, rebuild + needsRebuild = latestSourceMtime > oldestImageTime; + } catch (error) { + // If images don't exist or inspect fails, rebuild + needsRebuild = true; + } + + return needsRebuild; + } catch (error) { + // If we can't check, err on the side of rebuilding + log('Could not check Docker image status, will rebuild to be safe', 'yellow'); + return true; + } +} + /** * Install Playwright browsers (dev-only dependency) */ @@ -172,9 +257,16 @@ async function main() { } else if (choice === '3') { console.log(''); log('Launching Docker Container (Isolated Mode)...', 'blue'); - log('Starting Docker containers...', 'yellow'); - log('Note: Containers will only rebuild if images are missing.', 'yellow'); - log('To force a rebuild, run: docker compose up --build', 'yellow'); + + // Check if Dockerfile or package.json changed and rebuild if needed + const needsRebuild = shouldRebuildDockerImages(); + const buildFlag = needsRebuild ? ['--build'] : []; + + if (needsRebuild) { + log('Dockerfile or package.json changed - rebuilding images...', 'yellow'); + } else { + log('Starting Docker containers...', 'yellow'); + } console.log(''); // Check if ANTHROPIC_API_KEY is set @@ -185,9 +277,9 @@ async function main() { console.log(''); } - // Start containers with docker-compose (without --build to preserve volumes) - // Images will only be built if they don't exist - processes.docker = crossSpawn('docker', ['compose', 'up'], { + // Start containers with docker-compose + // Will rebuild if Dockerfile or package.json changed + processes.docker = crossSpawn('docker', ['compose', 'up', ...buildFlag], { stdio: 'inherit', cwd: __dirname, env: { diff --git a/docker-compose.override.yml.example b/docker-compose.override.yml.example index 611ff588..b4ef6c47 100644 --- a/docker-compose.override.yml.example +++ b/docker-compose.override.yml.example @@ -4,8 +4,26 @@ services: # Mount your workspace directory to /projects inside the container # Example: mount your local /workspace to /projects inside the container - /Users/webdevcody/Workspace/automaker-workspace:/projects:rw + + # ===== CLI Authentication (Optional) ===== + # Mount host CLI credentials to avoid re-authenticating in container + + # Claude CLI - mount your ~/.claude directory (Linux/Windows) + # This shares your 'claude login' OAuth session with the container + # - ~/.claude:/home/automaker/.claude + + # Cursor CLI - mount your ~/.cursor directory (Linux/Windows) + # This shares your 'cursor-agent login' OAuth session with the container + # - ~/.cursor:/home/automaker/.cursor + environment: # Set root directory for all projects and file operations # Users can only create/open projects within this directory - ALLOWED_ROOT_DIRECTORY=/projects - NODE_ENV=development + + # ===== macOS Users ===== + # On macOS, OAuth tokens are stored in SQLite databases, not plain files. + # Extract your Cursor token with: ./scripts/get-cursor-token.sh + # Then set it here or in your .env file: + # - CURSOR_API_KEY=${CURSOR_API_KEY:-} diff --git a/docker-compose.yml b/docker-compose.yml index b9e51abf..227450ad 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,6 +36,17 @@ services: # Required - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} + # Optional - Claude CLI OAuth credentials (for macOS users) + # Extract with: ./scripts/get-claude-token.sh + # This writes the OAuth tokens to ~/.claude/.credentials.json in the container + - CLAUDE_OAUTH_CREDENTIALS=${CLAUDE_OAUTH_CREDENTIALS:-} + + # Optional - Cursor CLI OAuth token (extract from host with the command shown below) + # macOS: ./scripts/get-cursor-token.sh (extracts from Keychain) + # Linux: jq -r '.accessToken' ~/.config/cursor/auth.json + # Note: cursor-agent stores its OAuth tokens separately from Cursor IDE + - CURSOR_AUTH_TOKEN=${CURSOR_AUTH_TOKEN:-} + # Optional - authentication, one will generate if left blank - AUTOMAKER_API_KEY=${AUTOMAKER_API_KEY:-} @@ -63,6 +74,10 @@ services: # This allows 'claude login' authentication to persist between restarts - automaker-claude-config:/home/automaker/.claude + # Persist Cursor CLI configuration and authentication across container restarts + # This allows 'cursor-agent login' authentication to persist between restarts + - automaker-cursor-config:/home/automaker/.cursor + # NO host directory mounts - container cannot access your laptop files # If you need to work on a project, create it INSIDE the container # or use a separate docker-compose override file @@ -81,3 +96,8 @@ volumes: name: automaker-claude-config # Named volume for Claude CLI OAuth session keys and configuration # Persists authentication across container restarts + + automaker-cursor-config: + name: automaker-cursor-config + # Named volume for Cursor CLI configuration and authentication + # Persists cursor-agent login authentication across container restarts diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 6537a66e..a13c4553 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,19 +1,51 @@ #!/bin/sh set -e -# Fix permissions on Claude CLI config directory if it exists +# Ensure Claude CLI config directory exists with correct permissions +if [ ! -d "/home/automaker/.claude" ]; then + mkdir -p /home/automaker/.claude +fi + +# If CLAUDE_OAUTH_CREDENTIALS is set, write it to the credentials file +# This allows passing OAuth tokens from host (especially macOS where they're in Keychain) +if [ -n "$CLAUDE_OAUTH_CREDENTIALS" ]; then + echo "$CLAUDE_OAUTH_CREDENTIALS" > /home/automaker/.claude/.credentials.json + chmod 600 /home/automaker/.claude/.credentials.json +fi + +# Fix permissions on Claude CLI config directory +chown -R automaker:automaker /home/automaker/.claude +chmod 700 /home/automaker/.claude + +# Fix permissions on Cursor CLI config directory if it exists # This handles the case where a volume is mounted and owned by root -if [ -d "/home/automaker/.claude" ]; then - chown -R automaker:automaker /home/automaker/.claude - chmod -R 755 /home/automaker/.claude +if [ -d "/home/automaker/.cursor" ]; then + chown -R automaker:automaker /home/automaker/.cursor + chmod -R 700 /home/automaker/.cursor fi # Ensure the directory exists with correct permissions if volume is empty -if [ ! -d "/home/automaker/.claude" ]; then - mkdir -p /home/automaker/.claude - chown automaker:automaker /home/automaker/.claude - chmod 755 /home/automaker/.claude +if [ ! -d "/home/automaker/.cursor" ]; then + mkdir -p /home/automaker/.cursor + chown automaker:automaker /home/automaker/.cursor + chmod 700 /home/automaker/.cursor +fi + +# If CURSOR_AUTH_TOKEN is set, write it to the cursor auth file +# On Linux, cursor-agent uses ~/.config/cursor/auth.json for file-based credential storage +# The env var CURSOR_AUTH_TOKEN is also checked directly by cursor-agent +if [ -n "$CURSOR_AUTH_TOKEN" ]; then + CURSOR_CONFIG_DIR="/home/automaker/.config/cursor" + mkdir -p "$CURSOR_CONFIG_DIR" + # Write auth.json with the access token + cat > "$CURSOR_CONFIG_DIR/auth.json" << EOF +{ + "accessToken": "$CURSOR_AUTH_TOKEN" +} +EOF + chmod 600 "$CURSOR_CONFIG_DIR/auth.json" + chown -R automaker:automaker /home/automaker/.config fi # Switch to automaker user and execute the command -exec su-exec automaker "$@" +exec gosu automaker "$@" diff --git a/docs/docker-isolation.md b/docs/docker-isolation.md index 5ebd4c71..af190d9d 100644 --- a/docs/docker-isolation.md +++ b/docs/docker-isolation.md @@ -57,10 +57,63 @@ docker-compose -f docker-compose.yml -f docker-compose.project.yml up -d **Tip**: Use `:ro` (read-only) when possible for extra safety. +## CLI Authentication (macOS) + +On macOS, OAuth tokens are stored in Keychain (Claude) and SQLite (Cursor). Use these scripts to extract and pass them to the container: + +### Claude CLI + +```bash +# Extract and add to .env +echo "CLAUDE_OAUTH_CREDENTIALS=$(./scripts/get-claude-token.sh)" >> .env +``` + +### Cursor CLI + +```bash +# Extract and add to .env (extracts from macOS Keychain) +echo "CURSOR_AUTH_TOKEN=$(./scripts/get-cursor-token.sh)" >> .env +``` + +**Note**: The cursor-agent CLI stores its OAuth tokens separately from the Cursor IDE: + +- **macOS**: Tokens are stored in Keychain (service: `cursor-access-token`) +- **Linux**: Tokens are stored in `~/.config/cursor/auth.json` + +### Apply to container + +```bash +# Restart with new credentials +docker-compose down && docker-compose up -d +``` + +**Note**: Tokens expire periodically. If you get authentication errors, re-run the extraction scripts. + +## CLI Authentication (Linux/Windows) + +On Linux/Windows, cursor-agent stores credentials in files, so you can either: + +**Option 1: Extract tokens to environment variables (recommended)** + +```bash +# Linux: Extract tokens to .env +echo "CURSOR_AUTH_TOKEN=$(jq -r '.accessToken' ~/.config/cursor/auth.json)" >> .env +``` + +**Option 2: Bind mount credential directories directly** + +```yaml +# In docker-compose.override.yml +volumes: + - ~/.claude:/home/automaker/.claude + - ~/.config/cursor:/home/automaker/.config/cursor +``` + ## Troubleshooting -| Problem | Solution | -| --------------------- | -------------------------------------------------------------------------------------------- | -| Container won't start | Check `.env` has `ANTHROPIC_API_KEY` set. Run `docker-compose logs` for errors. | -| Can't access web UI | Verify container is running with `docker ps \| grep automaker` | -| Need a fresh start | Run `docker-compose down && docker volume rm automaker-data && docker-compose up -d --build` | +| Problem | Solution | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Container won't start | Check `.env` has `ANTHROPIC_API_KEY` set. Run `docker-compose logs` for errors. | +| Can't access web UI | Verify container is running with `docker ps \| grep automaker` | +| Need a fresh start | Run `docker-compose down && docker volume rm automaker-data && docker-compose up -d --build` | +| Cursor auth fails | Re-extract token with `./scripts/get-cursor-token.sh` - tokens expire periodically. Make sure you've run `cursor-agent login` on your host first. | diff --git a/libs/platform/src/system-paths.ts b/libs/platform/src/system-paths.ts index 2824d623..6011e559 100644 --- a/libs/platform/src/system-paths.ts +++ b/libs/platform/src/system-paths.ts @@ -800,8 +800,14 @@ export async function getClaudeAuthIndicators(): Promise { const content = await systemPathReadFile(credPath); const credentials = JSON.parse(content); result.hasCredentialsFile = true; + // Support multiple credential formats: + // 1. Claude Code CLI format: { claudeAiOauth: { accessToken, refreshToken } } + // 2. Legacy format: { oauth_token } or { access_token } + // 3. API key format: { api_key } + const hasClaudeOauth = !!credentials.claudeAiOauth?.accessToken; + const hasLegacyOauth = !!(credentials.oauth_token || credentials.access_token); result.credentials = { - hasOAuthToken: !!(credentials.oauth_token || credentials.access_token), + hasOAuthToken: hasClaudeOauth || hasLegacyOauth, hasApiKey: !!credentials.api_key, }; break; diff --git a/scripts/get-claude-token.sh b/scripts/get-claude-token.sh new file mode 100755 index 00000000..1ebdd0cd --- /dev/null +++ b/scripts/get-claude-token.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# Extract Claude OAuth token from macOS Keychain for use in Docker container +# Usage: ./scripts/get-claude-token.sh +# or: export CLAUDE_OAUTH_TOKEN=$(./scripts/get-claude-token.sh) + +set -e + +# Only works on macOS (uses security command for Keychain access) +if [[ "$OSTYPE" != "darwin"* ]]; then + echo "Error: This script only works on macOS." >&2 + echo "On Linux, mount ~/.claude directory directly instead." >&2 + exit 1 +fi + +# Check if security command exists +if ! command -v security &> /dev/null; then + echo "Error: 'security' command not found." >&2 + exit 1 +fi + +# Get the current username +USERNAME=$(whoami) + +# Extract credentials from Keychain +CREDS=$(security find-generic-password -s "Claude Code-credentials" -a "$USERNAME" -w 2>/dev/null) + +if [ -z "$CREDS" ]; then + echo "Error: No Claude credentials found in Keychain." >&2 + echo "Make sure you've logged in with 'claude login' first." >&2 + exit 1 +fi + +# Output the full credentials JSON (contains accessToken and refreshToken) +echo "$CREDS" diff --git a/scripts/get-cursor-token.sh b/scripts/get-cursor-token.sh new file mode 100755 index 00000000..912cce6b --- /dev/null +++ b/scripts/get-cursor-token.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# Extract Cursor CLI OAuth token from host machine for use in Docker container +# +# IMPORTANT: This extracts the cursor-agent CLI OAuth token, NOT the Cursor IDE token. +# cursor-agent stores tokens in macOS Keychain (not SQLite like the IDE). +# +# Usage: ./scripts/get-cursor-token.sh +# or: export CURSOR_AUTH_TOKEN=$(./scripts/get-cursor-token.sh) +# +# For Docker: echo "CURSOR_AUTH_TOKEN=$(./scripts/get-cursor-token.sh)" >> .env + +set -e + +# Determine platform and extract token accordingly +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS: cursor-agent stores OAuth tokens in Keychain + # Service: cursor-access-token, Account: cursor-user + + if ! command -v security &> /dev/null; then + echo "Error: 'security' command not found." >&2 + exit 1 + fi + + # Extract access token from Keychain + TOKEN=$(security find-generic-password -a "cursor-user" -s "cursor-access-token" -w 2>/dev/null) + + if [ -z "$TOKEN" ]; then + echo "Error: No Cursor CLI token found in Keychain." >&2 + echo "Make sure you've logged in with 'cursor-agent login' first." >&2 + exit 1 + fi + +elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + # Linux: cursor-agent stores OAuth tokens in a JSON file + # Default location: ~/.config/cursor/auth.json + # Or: $XDG_CONFIG_HOME/cursor/auth.json + + if [ -n "$XDG_CONFIG_HOME" ]; then + AUTH_FILE="$XDG_CONFIG_HOME/cursor/auth.json" + else + AUTH_FILE="$HOME/.config/cursor/auth.json" + fi + + if [ ! -f "$AUTH_FILE" ]; then + echo "Error: Cursor auth file not found at: $AUTH_FILE" >&2 + echo "Make sure you've logged in with 'cursor-agent login' first." >&2 + exit 1 + fi + + # Check if jq is available + if ! command -v jq &> /dev/null; then + echo "Error: jq is required but not installed." >&2 + echo "Install it with: apt install jq" >&2 + exit 1 + fi + + TOKEN=$(jq -r '.accessToken // empty' "$AUTH_FILE" 2>/dev/null) + + if [ -z "$TOKEN" ]; then + echo "Error: No access token found in $AUTH_FILE" >&2 + exit 1 + fi +else + echo "Error: Unsupported platform: $OSTYPE" >&2 + exit 1 +fi + +# Output the token +echo "$TOKEN" diff --git a/start.mjs b/start.mjs index 2eb1739c..80ed4746 100755 --- a/start.mjs +++ b/start.mjs @@ -19,6 +19,8 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; +import { statSync } from 'fs'; +import { execSync } from 'child_process'; import { createRestrictedFs, log, @@ -54,6 +56,89 @@ const processes = { docker: null, }; +/** + * Check if Docker images need to be rebuilt based on Dockerfile or package.json changes + */ +function shouldRebuildDockerImages() { + try { + const dockerfilePath = path.join(__dirname, 'Dockerfile'); + const packageJsonPath = path.join(__dirname, 'package.json'); + + // Get modification times of source files + const dockerfileMtime = statSync(dockerfilePath).mtimeMs; + const packageJsonMtime = statSync(packageJsonPath).mtimeMs; + const latestSourceMtime = Math.max(dockerfileMtime, packageJsonMtime); + + // Get image names from docker-compose config + let serverImageName, uiImageName; + try { + const composeConfig = execSync('docker compose config --format json', { + encoding: 'utf-8', + cwd: __dirname, + }); + const config = JSON.parse(composeConfig); + + // Docker Compose generates image names as _ + // Get project name from config or default to directory name + const projectName = + config.name || + path + .basename(__dirname) + .toLowerCase() + .replace(/[^a-z0-9]/g, ''); + serverImageName = `${projectName}_server`; + uiImageName = `${projectName}_ui`; + } catch (error) { + // Fallback to default naming convention + const projectName = path + .basename(__dirname) + .toLowerCase() + .replace(/[^a-z0-9]/g, ''); + serverImageName = `${projectName}_server`; + uiImageName = `${projectName}_ui`; + } + + // Check if images exist and get their creation times + let needsRebuild = false; + + try { + // Check server image + const serverImageInfo = execSync( + `docker image inspect ${serverImageName} --format "{{.Created}}" 2>/dev/null || echo ""`, + { encoding: 'utf-8', cwd: __dirname } + ).trim(); + + // Check UI image + const uiImageInfo = execSync( + `docker image inspect ${uiImageName} --format "{{.Created}}" 2>/dev/null || echo ""`, + { encoding: 'utf-8', cwd: __dirname } + ).trim(); + + // If either image doesn't exist, we need to rebuild + if (!serverImageInfo || !uiImageInfo) { + return true; + } + + // Parse image creation times (ISO 8601 format) + const serverCreated = new Date(serverImageInfo).getTime(); + const uiCreated = new Date(uiImageInfo).getTime(); + const oldestImageTime = Math.min(serverCreated, uiCreated); + + // If source files are newer than images, rebuild + needsRebuild = latestSourceMtime > oldestImageTime; + } catch (error) { + // If images don't exist or inspect fails, rebuild + needsRebuild = true; + } + + return needsRebuild; + } catch (error) { + // If we can't check, err on the side of rebuilding + log('Could not check Docker image status, will rebuild to be safe', 'yellow'); + return true; + } +} + /** * Build all production artifacts */ @@ -231,9 +316,16 @@ async function main() { } else if (choice === '3') { console.log(''); log('Launching Docker Container (Isolated Mode)...', 'blue'); - log('Starting Docker containers...', 'yellow'); - log('Note: Containers will only rebuild if images are missing.', 'yellow'); - log('To force a rebuild, run: docker compose up --build', 'yellow'); + + // Check if Dockerfile or package.json changed and rebuild if needed + const needsRebuild = shouldRebuildDockerImages(); + const buildFlag = needsRebuild ? ['--build'] : []; + + if (needsRebuild) { + log('Dockerfile or package.json changed - rebuilding images...', 'yellow'); + } else { + log('Starting Docker containers...', 'yellow'); + } console.log(''); // Check if ANTHROPIC_API_KEY is set @@ -244,9 +336,9 @@ async function main() { console.log(''); } - // Start containers with docker-compose (without --build to preserve volumes) - // Images will only be built if they don't exist - processes.docker = crossSpawn('docker', ['compose', 'up'], { + // Start containers with docker-compose + // Will rebuild if Dockerfile or package.json changed + processes.docker = crossSpawn('docker', ['compose', 'up', ...buildFlag], { stdio: 'inherit', cwd: __dirname, env: {