Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 53 additions & 22 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -51,32 +53,59 @@ 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), 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 \
FROM node:22-slim AS server

# 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}

# Install Claude CLI globally
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 (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

# 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
Comment on lines +85 to +94
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Curl-pipe-to-bash pattern introduces supply chain risk.

Piping remote scripts directly to bash (curl ... | bash) is a security anti-pattern as it executes arbitrary code without verification. If cursor.com is compromised or serves different content, the build could be affected.

Consider:

  1. Downloading the script first and inspecting/checksumming it
  2. Pinning to a specific installer version if available
  3. Documenting this as an accepted risk if no alternative exists
🤖 Prompt for AI Agents
In @Dockerfile around lines 85-94, Avoid the curl|bash pipeline: instead fetch
the installer artifact first, verify its authenticity (checksum or signature)
and only then execute it as the automaker user; update the Dockerfile section
that currently uses "USER automaker"/"ENV HOME=/home/automaker" and the RUN that
pipes curl to bash to download the installer to a temporary file, validate a
pinned version or checksum/signature, then run that local script as automaker,
and finally check installation with "which cursor-agent" and "cursor-agent
--version"; if no signed artifact or pinned URL exists, document the risk and
pin the installer version or source before executing.


# 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
# 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
Comment on lines +101 to +106
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To reduce Docker image layers and group related commands, you can combine the RUN instructions for updating .bashrc files into a single instruction. This makes the Dockerfile more efficient and easier to maintain.

# Add to automaker's and root's .bashrc for bash interactive shells
RUN echo 'export PATH="/home/automaker/.local/bin:$PATH"' >> /home/automaker/.bashrc && \
    chown automaker:automaker /home/automaker/.bashrc && \
    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 ./
Expand Down Expand Up @@ -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
Expand Down
98 changes: 53 additions & 45 deletions apps/ui/src/components/views/settings-view/hooks/use-cli-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,53 @@ export function useCliStatus() {

const [isCheckingClaudeCli, setIsCheckingClaudeCli] = useState(false);

// Refresh Claude auth status from the server
const refreshAuthStatus = useCallback(async () => {
const api = getElectronAPI();
if (!api?.setup?.getClaudeStatus) return;

try {
const result = await api.setup.getClaudeStatus();
if (result.success && result.auth) {
// Cast to extended type that includes server-added fields
const auth = result.auth as typeof result.auth & {
oauthTokenValid?: boolean;
apiKeyValid?: boolean;
};
// Map server method names to client method types
// Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none
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'; // Default authenticated to api_key, not 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);
}
}, [setClaudeAuthStatus]);

// Check CLI status on mount
useEffect(() => {
const checkCliStatus = async () => {
Expand All @@ -48,54 +95,13 @@ export function useCliStatus() {
}

// Check Claude auth status (re-fetch on mount to ensure persistence)
if (api?.setup?.getClaudeStatus) {
try {
const result = await api.setup.getClaudeStatus();
if (result.success && result.auth) {
// Cast to extended type that includes server-added fields
const auth = result.auth as typeof result.auth & {
oauthTokenValid?: boolean;
apiKeyValid?: boolean;
};
// Map server method names to client method types
// Server returns: oauth_token_env, oauth_token, api_key_env, api_key, credentials_file, cli_authenticated, none
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'; // Default authenticated to api_key, not 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 check Claude auth status:', error);
}
}
await refreshAuthStatus();
};

checkCliStatus();
}, [setClaudeAuthStatus]);
}, [refreshAuthStatus]);

// Refresh Claude CLI status
// Refresh Claude CLI status and auth status
const handleRefreshClaudeCli = useCallback(async () => {
setIsCheckingClaudeCli(true);
try {
Expand All @@ -104,12 +110,14 @@ export function useCliStatus() {
const status = await api.checkClaudeCli();
setClaudeCliStatus(status);
}
// Also refresh auth status
await refreshAuthStatus();
} catch (error) {
logger.error('Failed to refresh Claude CLI status:', error);
} finally {
setIsCheckingClaudeCli(false);
}
}, []);
}, [refreshAuthStatus]);

return {
claudeCliStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ interface UseCliStatusOptions {
setCliStatus: (status: any) => void;
setAuthStatus: (status: any) => void;
}

// Create logger once outside the hook to prevent infinite re-renders
const logger = createLogger('CliStatus');

export function useCliStatus({
Expand Down Expand Up @@ -66,7 +68,7 @@ export function useCliStatus({
} finally {
setIsChecking(false);
}
}, [cliType, statusApi, setCliStatus, setAuthStatus, logger]);
}, [cliType, statusApi, setCliStatus, setAuthStatus]);

return { isChecking, checkStatus };
}
50 changes: 8 additions & 42 deletions dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@

import path from 'path';
import { fileURLToPath } from 'url';
import { createRequire } from 'module';

import {
createRestrictedFs,
log,
runNpm,
runNpmAndWait,
runNpx,
printHeader,
printModeMenu,
resolvePortConfiguration,
Expand All @@ -26,11 +26,9 @@ import {
startServerAndWait,
ensureDependencies,
prompt,
launchDockerContainers,
} from './scripts/launcher-utils.mjs';

const require = createRequire(import.meta.url);
const crossSpawn = require('cross-spawn');

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

Expand All @@ -52,10 +50,11 @@ async function installPlaywrightBrowsers() {
log('Checking Playwright browsers...', 'yellow');
try {
const exitCode = await new Promise((resolve) => {
const playwright = crossSpawn('npx', ['playwright', 'install', 'chromium'], {
stdio: 'inherit',
cwd: path.join(__dirname, 'apps', 'ui'),
});
const playwright = runNpx(
['playwright', 'install', 'chromium'],
{ stdio: 'inherit' },
path.join(__dirname, 'apps', 'ui')
);
playwright.on('close', (code) => resolve(code));
playwright.on('error', () => resolve(1));
});
Expand Down Expand Up @@ -171,40 +170,7 @@ async function main() {
break;
} 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');
console.log('');

// Check if ANTHROPIC_API_KEY is set
if (!process.env.ANTHROPIC_API_KEY) {
log('Warning: ANTHROPIC_API_KEY environment variable is not set.', 'yellow');
log('The server will require an API key to function.', 'yellow');
log('Set it with: export ANTHROPIC_API_KEY=your-key', 'yellow');
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'], {
stdio: 'inherit',
cwd: __dirname,
env: {
...process.env,
},
});

log('Docker containers starting...', 'blue');
log('UI will be available at: http://localhost:3007', 'green');
log('API will be available at: http://localhost:3008', 'green');
console.log('');
log('Press Ctrl+C to stop the containers.', 'yellow');

await new Promise((resolve) => {
processes.docker.on('close', resolve);
});

await launchDockerContainers({ baseDir: __dirname, processes });
break;
} else {
log('Invalid choice. Please enter 1, 2, or 3.', 'red');
Expand Down
18 changes: 18 additions & 0 deletions docker-compose.override.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -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:-}
Comment on lines +25 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Environment variable name inconsistency.

Line 29 references CURSOR_API_KEY, but docker-compose.yml defines CURSOR_AUTH_TOKEN and the extraction script (scripts/get-cursor-token.sh) also uses CURSOR_AUTH_TOKEN.

🔎 Proposed fix
       # 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:-}
+      # - CURSOR_AUTH_TOKEN=${CURSOR_AUTH_TOKEN:-}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# ===== 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:-}
# ===== 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_AUTH_TOKEN=${CURSOR_AUTH_TOKEN:-}
🤖 Prompt for AI Agents
In @docker-compose.override.yml.example around lines 25-29, The env var name is
inconsistent: the example uses CURSOR_API_KEY while docker-compose.yml and
scripts/get-cursor-token.sh use CURSOR_AUTH_TOKEN; update the example line to
use CURSOR_AUTH_TOKEN (or vice versa everywhere) so the extraction script,
docker-compose.yml and docker-compose.override.yml.example all reference the
same variable name (CURSOR_AUTH_TOKEN) and ensure any documentation/comments
mention that exact symbol.

Loading