diff --git a/.gitignore b/.gitignore index 5efd9e1f..d35ec2d1 100644 --- a/.gitignore +++ b/.gitignore @@ -80,4 +80,4 @@ blob-report/ *.pem docker-compose.override.yml -.claude/ \ No newline at end of file +.claude/docker-compose.override.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..e7a0237a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,155 @@ +# Automaker Multi-Stage Dockerfile +# Single Dockerfile for both server and UI builds +# Usage: +# docker build --target server -t automaker-server . +# docker build --target ui -t automaker-ui . +# Or use docker-compose which selects targets automatically + +# ============================================================================= +# BASE STAGE - Common setup for all builds (DRY: defined once, used by all) +# ============================================================================= +FROM node:22-alpine AS base + +# Install build dependencies for native modules (node-pty) +RUN apk add --no-cache python3 make g++ + +WORKDIR /app + +# Copy root package files +COPY package*.json ./ + +# Copy all libs package.json files (centralized - add new libs here) +COPY libs/types/package*.json ./libs/types/ +COPY libs/utils/package*.json ./libs/utils/ +COPY libs/prompts/package*.json ./libs/prompts/ +COPY libs/platform/package*.json ./libs/platform/ +COPY libs/model-resolver/package*.json ./libs/model-resolver/ +COPY libs/dependency-resolver/package*.json ./libs/dependency-resolver/ +COPY libs/git-utils/package*.json ./libs/git-utils/ + +# Copy scripts (needed by npm workspace) +COPY scripts ./scripts + +# ============================================================================= +# SERVER BUILD STAGE +# ============================================================================= +FROM base AS server-builder + +# Copy server-specific package.json +COPY apps/server/package*.json ./apps/server/ + +# Install dependencies (--ignore-scripts to skip husky/prepare, then rebuild native modules) +RUN npm ci --ignore-scripts && npm rebuild node-pty + +# Copy all source files +COPY libs ./libs +COPY apps/server ./apps/server + +# Build packages in dependency order, then build server +RUN npm run build:packages && npm run build --workspace=apps/server + +# ============================================================================= +# SERVER PRODUCTION STAGE +# ============================================================================= +FROM node:22-alpine AS server + +# Install git, curl, bash (for terminal), and GitHub CLI (pinned version, multi-arch) +RUN apk add --no-cache git curl bash && \ + 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} + +# Install Claude CLI globally +RUN npm install -g @anthropic-ai/claude-code + +WORKDIR /app + +# Create non-root user +RUN addgroup -g 1001 -S automaker && \ + adduser -S automaker -u 1001 + +# Copy root package.json (needed for workspace resolution) +COPY --from=server-builder /app/package*.json ./ + +# Copy built libs (workspace packages are symlinked in node_modules) +COPY --from=server-builder /app/libs ./libs + +# Copy built server +COPY --from=server-builder /app/apps/server/dist ./apps/server/dist +COPY --from=server-builder /app/apps/server/package*.json ./apps/server/ + +# Copy node_modules (includes symlinks to libs) +COPY --from=server-builder /app/node_modules ./node_modules + +# Create data and projects directories +RUN mkdir -p /data /projects && chown automaker:automaker /data /projects + +# Configure git for mounted volumes and authentication +# Use --system so it's not overwritten by mounted user .gitconfig +RUN git config --system --add safe.directory '*' && \ + # Use gh as credential helper (works with GH_TOKEN env var) + git config --system credential.helper '!gh auth git-credential' + +# Switch to non-root user +USER automaker + +# Environment variables +ENV NODE_ENV=production +ENV PORT=3008 +ENV DATA_DIR=/data + +# Expose port +EXPOSE 3008 + +# Health check (using curl since it's already installed, more reliable than busybox wget) +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:3008/api/health || exit 1 + +# Start server +CMD ["node", "apps/server/dist/index.js"] + +# ============================================================================= +# UI BUILD STAGE +# ============================================================================= +FROM base AS ui-builder + +# Copy UI-specific package.json +COPY apps/ui/package*.json ./apps/ui/ + +# Install dependencies (--ignore-scripts to skip husky and build:packages in prepare script) +RUN npm ci --ignore-scripts + +# Copy all source files +COPY libs ./libs +COPY apps/ui ./apps/ui + +# Build packages in dependency order, then build UI +# VITE_SERVER_URL tells the UI where to find the API server +# Use ARG to allow overriding at build time: --build-arg VITE_SERVER_URL=http://api.example.com +ARG VITE_SERVER_URL=http://localhost:3008 +ENV VITE_SKIP_ELECTRON=true +ENV VITE_SERVER_URL=${VITE_SERVER_URL} +RUN npm run build:packages && npm run build --workspace=apps/ui + +# ============================================================================= +# UI PRODUCTION STAGE +# ============================================================================= +FROM nginx:alpine AS ui + +# Copy built files +COPY --from=ui-builder /app/apps/ui/dist /usr/share/nginx/html + +# Copy nginx config for SPA routing +COPY apps/ui/nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md index 0cde7221..67dd17dd 100644 --- a/README.md +++ b/README.md @@ -223,14 +223,111 @@ npm run build:electron:linux # Linux (AppImage + DEB, x64) #### Docker Deployment +Docker provides the most secure way to run Automaker by isolating it from your host filesystem. + ```bash -# Build and run with Docker Compose (recommended for security) +# Build and run with Docker Compose docker-compose up -d -# Access at http://localhost:3007 +# Access UI at http://localhost:3007 # API at http://localhost:3008 + +# View logs +docker-compose logs -f + +# Stop containers +docker-compose down ``` +##### Configuration + +Create a `.env` file in the project root if using API key authentication: + +```bash +# Optional: Anthropic API key (not needed if using Claude CLI authentication) +ANTHROPIC_API_KEY=sk-ant-... +``` + +**Note:** Most users authenticate via Claude CLI instead of API keys. See [Claude CLI Authentication](#claude-cli-authentication-optional) below. + +##### Working with Projects (Host Directory Access) + +By default, the container is isolated from your host filesystem. To work on projects from your host machine, create a `docker-compose.override.yml` file (gitignored): + +```yaml +services: + server: + volumes: + # Mount your project directories + - /path/to/your/project:/projects/your-project +``` + +##### Claude CLI Authentication (Optional) + +To use Claude Code CLI authentication instead of an API key, mount your Claude CLI config directory: + +```yaml +services: + server: + volumes: + # Linux/macOS + - ~/.claude:/home/automaker/.claude + # Windows + - C:/Users/YourName/.claude:/home/automaker/.claude +``` + +**Note:** The Claude CLI config must be writable (do not use `:ro` flag) as the CLI writes debug files. + +##### GitHub CLI Authentication (For Git Push/PR Operations) + +To enable git push and GitHub CLI operations inside the container: + +```yaml +services: + server: + volumes: + # Mount GitHub CLI config + # Linux/macOS + - ~/.config/gh:/home/automaker/.config/gh + # Windows + - 'C:/Users/YourName/AppData/Roaming/GitHub CLI:/home/automaker/.config/gh' + + # Mount git config for user identity (name, email) + - ~/.gitconfig:/home/automaker/.gitconfig:ro + environment: + # GitHub token (required on Windows where tokens are in Credential Manager) + # Get your token with: gh auth token + - GH_TOKEN=${GH_TOKEN} +``` + +Then add `GH_TOKEN` to your `.env` file: + +```bash +GH_TOKEN=gho_your_github_token_here +``` + +##### Complete docker-compose.override.yml Example + +```yaml +services: + server: + volumes: + # Your projects + - /path/to/project1:/projects/project1 + - /path/to/project2:/projects/project2 + + # Authentication configs + - ~/.claude:/home/automaker/.claude + - ~/.config/gh:/home/automaker/.config/gh + - ~/.gitconfig:/home/automaker/.gitconfig:ro + environment: + - GH_TOKEN=${GH_TOKEN} +``` + +##### Architecture Support + +The Docker image supports both AMD64 and ARM64 architectures. The GitHub CLI and Claude CLI are automatically downloaded for the correct architecture during build. + ### Testing #### End-to-End Tests (Playwright) diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile deleted file mode 100644 index 67ecedf0..00000000 --- a/apps/server/Dockerfile +++ /dev/null @@ -1,67 +0,0 @@ -# Automaker Backend Server -# Multi-stage build for minimal production image - -# Build stage -FROM node:20-alpine AS builder - -# Install build dependencies for native modules (node-pty) -RUN apk add --no-cache python3 make g++ - -WORKDIR /app - -# Copy package files and scripts needed for postinstall -COPY package*.json ./ -COPY apps/server/package*.json ./apps/server/ -COPY scripts ./scripts - -# Install dependencies -RUN npm ci --workspace=apps/server - -# Copy source -COPY apps/server ./apps/server - -# Build TypeScript -RUN npm run build --workspace=apps/server - -# Production stage -FROM node:20-alpine - -# Install git, curl, and GitHub CLI (pinned version for reproducible builds) -RUN apk add --no-cache git curl && \ - GH_VERSION="2.63.2" && \ - curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \ - tar -xzf gh.tar.gz && \ - mv "gh_${GH_VERSION}_linux_amd64/bin/gh" /usr/local/bin/gh && \ - rm -rf gh.tar.gz "gh_${GH_VERSION}_linux_amd64" - -WORKDIR /app - -# Create non-root user -RUN addgroup -g 1001 -S automaker && \ - adduser -S automaker -u 1001 - -# Copy built files and production dependencies -COPY --from=builder /app/apps/server/dist ./dist -COPY --from=builder /app/apps/server/package*.json ./ -COPY --from=builder /app/node_modules ./node_modules - -# Create data directory -RUN mkdir -p /data && chown automaker:automaker /data - -# Switch to non-root user -USER automaker - -# Environment variables -ENV NODE_ENV=production -ENV PORT=3008 -ENV DATA_DIR=/data - -# Expose port -EXPOSE 3008 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:3008/api/health || exit 1 - -# Start server -CMD ["node", "dist/index.js"] diff --git a/apps/server/src/routes/setup/routes/gh-status.ts b/apps/server/src/routes/setup/routes/gh-status.ts index 4d36561c..e48b5c25 100644 --- a/apps/server/src/routes/setup/routes/gh-status.ts +++ b/apps/server/src/routes/setup/routes/gh-status.ts @@ -94,23 +94,37 @@ async function getGhStatus(): Promise { // Version command failed } - // Check authentication status + // Check authentication status by actually making an API call + // gh auth status can return non-zero even when GH_TOKEN is valid + let apiCallSucceeded = false; try { - const { stdout } = await execAsync('gh auth status', { env: execEnv }); - // If this succeeds without error, we're authenticated - status.authenticated = true; - - // Try to extract username from output - const userMatch = - stdout.match(/Logged in to [^\s]+ account ([^\s]+)/i) || - stdout.match(/Logged in to [^\s]+ as ([^\s]+)/i); - if (userMatch) { - status.user = userMatch[1]; + const { stdout } = await execAsync('gh api user --jq ".login"', { env: execEnv }); + const user = stdout.trim(); + if (user) { + status.authenticated = true; + status.user = user; + apiCallSucceeded = true; } - } catch (error: unknown) { - // Auth status returns non-zero if not authenticated - const err = error as { stderr?: string }; - if (err.stderr?.includes('not logged in')) { + // If stdout is empty, fall through to gh auth status fallback + } catch { + // API call failed - fall through to gh auth status fallback + } + + // Fallback: try gh auth status if API call didn't succeed + if (!apiCallSucceeded) { + try { + const { stdout } = await execAsync('gh auth status', { env: execEnv }); + status.authenticated = true; + + // Try to extract username from output + const userMatch = + stdout.match(/Logged in to [^\s]+ account ([^\s]+)/i) || + stdout.match(/Logged in to [^\s]+ as ([^\s]+)/i); + if (userMatch) { + status.user = userMatch[1]; + } + } catch { + // Auth status returns non-zero if not authenticated status.authenticated = false; } } diff --git a/apps/ui/Dockerfile b/apps/ui/Dockerfile deleted file mode 100644 index 3ccd09c7..00000000 --- a/apps/ui/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -# Automaker UI -# Multi-stage build for minimal production image - -# Build stage -FROM node:20-alpine AS builder - -# Install build dependencies -RUN apk add --no-cache python3 make g++ - -WORKDIR /app - -# Copy package files -COPY package*.json ./ -COPY apps/ui/package*.json ./apps/ui/ -COPY scripts ./scripts - -# Install dependencies (skip electron postinstall) -RUN npm ci --workspace=apps/ui --ignore-scripts - -# Copy source -COPY apps/ui ./apps/ui - -# Build for web (skip electron) -# VITE_SERVER_URL tells the UI where to find the API server -# Using localhost:3008 since both containers expose ports to the host -# Use ARG to allow overriding at build time: --build-arg VITE_SERVER_URL=http://api.example.com -ARG VITE_SERVER_URL=http://localhost:3008 -ENV VITE_SKIP_ELECTRON=true -ENV VITE_SERVER_URL=${VITE_SERVER_URL} -RUN npm run build --workspace=apps/ui - -# Production stage - serve with nginx -FROM nginx:alpine - -# Copy built files -COPY --from=builder /app/apps/ui/dist /usr/share/nginx/html - -# Copy nginx config for SPA routing -COPY apps/ui/nginx.conf /etc/nginx/conf.d/default.conf - -EXPOSE 80 - -CMD ["nginx", "-g", "daemon off;"] diff --git a/docker-compose.yml b/docker-compose.yml index 0cbc28c2..bdf5ff19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,8 @@ services: ui: build: context: . - dockerfile: apps/ui/Dockerfile + dockerfile: Dockerfile + target: ui container_name: automaker-ui restart: unless-stopped ports: @@ -25,7 +26,8 @@ services: server: build: context: . - dockerfile: apps/server/Dockerfile + dockerfile: Dockerfile + target: server container_name: automaker-server restart: unless-stopped ports: