feat: add npm global package for one-command install

Add a Node.js CLI wrapper that allows installing AutoForge globally via
`npm install -g autoforge-ai` and running it with a single `autoforge`
command. The CLI handles Python detection, venv management, config
loading, and uvicorn server lifecycle automatically.

New files:
- package.json: npm package config with bin entry, files whitelist,
  and prepublishOnly script that builds the UI
- bin/autoforge.js: thin entry point that imports lib/cli.js
- lib/cli.js: main CLI module (~790 lines) with cross-platform Python
  3.11+ detection, composite venv marker for smart invalidation
  (requirements hash + Python version + path), .env config management
  at ~/.autoforge/.env, server startup with PID file and port detection,
  and signal handling with process tree cleanup
- requirements-prod.txt: runtime-only deps (excludes ruff, mypy, pytest)
- .npmignore: excludes dev files, tests, __pycache__, UI source

Modified files:
- ui/package.json: rename to autoforge-ui to avoid confusion with root
- .gitignore: add *.tgz for npm pack output
- README.md: add npm install as primary quick start method, document
  CLI commands, add Ollama/Vertex AI config sections, new troubleshooting
  entries for Python/venv issues
- GettingStarted.tsx: add Installation, Quick Start, and CLI Commands
  sections to in-app documentation with command reference table
- docsData.ts: add installation and cli-commands sidebar entries

Published as autoforge-ai@0.1.0 on npm.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-02-04 14:48:00 +02:00
parent 451a5a9d05
commit 4549840330
11 changed files with 1193 additions and 68 deletions

1
.gitignore vendored
View File

@@ -114,6 +114,7 @@ Desktop.ini
ui/dist/ ui/dist/
ui/.vite/ ui/.vite/
.vite/ .vite/
*.tgz
# =================== # ===================
# Environment files # Environment files

32
.npmignore Normal file
View File

@@ -0,0 +1,32 @@
venv/
**/__pycache__/
**/*.pyc
.git/
.github/
node_modules/
test_*.py
tests/
generations/
*.db
.env
requirements.txt
CLAUDE.md
LICENSE.md
README.md
ui/src/
ui/node_modules/
ui/tsconfig*.json
ui/vite.config.ts
ui/eslint.config.js
ui/index.html
ui/public/
ui/playwright.config.ts
ui/tests/
start.bat
start_ui.bat
start.sh
start_ui.sh
start_ui.py
.claude/agents/
.claude/skills/
.claude/settings.json

142
README.md
View File

@@ -14,9 +14,11 @@ A long-running autonomous coding agent powered by the Claude Agent SDK. This too
## Prerequisites ## Prerequisites
### Claude Code CLI (Required) - **Node.js 20+** - Required for the CLI
- **Python 3.11+** - Auto-detected on first run ([download](https://www.python.org/downloads/))
- **Claude Code CLI** - Install and authenticate (see below)
This project requires the Claude Code CLI to be installed. Install it using one of these methods: ### Claude Code CLI (Required)
**macOS / Linux:** **macOS / Linux:**
```bash ```bash
@@ -39,35 +41,63 @@ You need one of the following:
## Quick Start ## Quick Start
### Option 1: Web UI (Recommended) ### Option 1: npm Install (Recommended)
**Windows:**
```cmd
start_ui.bat
```
**macOS / Linux:**
```bash ```bash
./start_ui.sh npm install -g autoforge-ai
autoforge
``` ```
On first run, AutoForge automatically:
1. Checks for Python 3.11+
2. Creates a virtual environment at `~/.autoforge/venv/`
3. Installs Python dependencies
4. Copies a default config file to `~/.autoforge/.env`
5. Starts the server and opens your browser
### CLI Commands
```
autoforge Start the server (default)
autoforge config Open ~/.autoforge/.env in $EDITOR
autoforge config --path Print config file path
autoforge config --show Show active configuration values
autoforge --port PORT Custom port (default: auto from 8888)
autoforge --host HOST Custom host (default: 127.0.0.1)
autoforge --no-browser Don't auto-open browser
autoforge --repair Delete and recreate virtual environment
autoforge --version Print version
autoforge --help Show help
```
### Option 2: From Source (Development)
Clone the repository and use the start scripts directly. This is the recommended path if you want to contribute or modify AutoForge itself.
```bash
git clone https://github.com/leonvanzyl/autoforge.git
cd autoforge
```
**Web UI:**
| Platform | Command |
|---|---|
| Windows | `start_ui.bat` |
| macOS / Linux | `./start_ui.sh` |
This launches the React-based web UI at `http://localhost:5173` with: This launches the React-based web UI at `http://localhost:5173` with:
- Project selection and creation - Project selection and creation
- Kanban board view of features - Kanban board view of features
- Real-time agent output streaming - Real-time agent output streaming
- Start/pause/stop controls - Start/pause/stop controls
### Option 2: CLI Mode **CLI Mode:**
**Windows:** | Platform | Command |
```cmd |---|---|
start.bat | Windows | `start.bat` |
``` | macOS / Linux | `./start.sh` |
**macOS / Linux:**
```bash
./start.sh
```
The start script will: The start script will:
1. Check if Claude CLI is installed 1. Check if Claude CLI is installed
@@ -130,11 +160,9 @@ Features are stored in SQLite via SQLAlchemy and managed through an MCP server t
## Project Structure ## Project Structure
``` ```
autonomous-coding/ autoforge/
├── start.bat # Windows CLI start script ├── bin/ # npm CLI entry point
├── start.sh # macOS/Linux CLI start script ├── lib/ # CLI bootstrap and setup logic
├── start_ui.bat # Windows Web UI start script
├── start_ui.sh # macOS/Linux Web UI start script
├── start.py # CLI menu and project management ├── start.py # CLI menu and project management
├── start_ui.py # Web UI backend (FastAPI server launcher) ├── start_ui.py # Web UI backend (FastAPI server launcher)
├── autonomous_agent_demo.py # Agent entry point ├── autonomous_agent_demo.py # Agent entry point
@@ -165,9 +193,10 @@ autonomous-coding/
│ │ └── create-spec.md # /create-spec slash command │ │ └── create-spec.md # /create-spec slash command
│ ├── skills/ # Claude Code skills │ ├── skills/ # Claude Code skills
│ └── templates/ # Prompt templates │ └── templates/ # Prompt templates
├── generations/ # Generated projects go here ├── requirements.txt # Python dependencies (development)
├── requirements.txt # Python dependencies ├── requirements-prod.txt # Python dependencies (npm install)
── .env # Optional configuration (N8N webhook) ── package.json # npm package definition
└── .env # Optional configuration
``` ```
--- ---
@@ -264,11 +293,20 @@ The UI receives live updates via WebSocket (`/ws/projects/{project_name}`):
--- ---
## Configuration (Optional) ## Configuration
AutoForge reads configuration from a `.env` file. The file location depends on how you installed AutoForge:
| Install method | Config file location | Edit command |
|---|---|---|
| npm (global) | `~/.autoforge/.env` | `autoforge config` |
| From source | `.env` in the project root | Edit directly |
A default config file is created automatically on first run. Use `autoforge config` to open it in your editor, or `autoforge config --show` to print the active values.
### N8N Webhook Integration ### N8N Webhook Integration
The agent can send progress notifications to an N8N webhook. Create a `.env` file: Add to your `.env` to send progress notifications to an N8N webhook:
```bash ```bash
# Optional: N8N webhook for progress notifications # Optional: N8N webhook for progress notifications
@@ -290,7 +328,7 @@ When test progress increases, the agent sends:
### Using GLM Models (Alternative to Claude) ### Using GLM Models (Alternative to Claude)
To use Zhipu AI's GLM models instead of Claude, add these variables to your `.env` file in the AutoForge directory: Add these variables to your `.env` file to use Zhipu AI's GLM models:
```bash ```bash
ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
@@ -305,6 +343,36 @@ This routes AutoForge's API requests through Zhipu's Claude-compatible API, allo
Get an API key at: https://z.ai/subscribe Get an API key at: https://z.ai/subscribe
### Using Ollama Local Models
Add these variables to your `.env` file to run agents with local models via Ollama v0.14.0+:
```bash
ANTHROPIC_BASE_URL=http://localhost:11434
ANTHROPIC_AUTH_TOKEN=ollama
API_TIMEOUT_MS=3000000
ANTHROPIC_DEFAULT_SONNET_MODEL=qwen3-coder
ANTHROPIC_DEFAULT_OPUS_MODEL=qwen3-coder
ANTHROPIC_DEFAULT_HAIKU_MODEL=qwen3-coder
```
See the [CLAUDE.md](CLAUDE.md) for recommended models and known limitations.
### Using Vertex AI
Add these variables to your `.env` file to run agents via Google Cloud Vertex AI:
```bash
CLAUDE_CODE_USE_VERTEX=1
CLOUD_ML_REGION=us-east5
ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project-id
ANTHROPIC_DEFAULT_OPUS_MODEL=claude-opus-4-5@20251101
ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4-5@20250929
ANTHROPIC_DEFAULT_HAIKU_MODEL=claude-3-5-haiku@20241022
```
Requires `gcloud auth application-default login` first. Note the `@` separator (not `-`) in Vertex AI model names.
--- ---
## Customization ## Customization
@@ -335,6 +403,18 @@ This is normal. The initializer agent is generating detailed test cases, which t
**"Command blocked by security hook"** **"Command blocked by security hook"**
The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`. The agent tried to run a command not in the allowlist. This is the security system working as intended. If needed, add the command to `ALLOWED_COMMANDS` in `security.py`.
**"Python 3.11+ required but not found"**
Install Python 3.11 or later from [python.org](https://www.python.org/downloads/). Make sure `python3` (or `python` on Windows) is on your PATH.
**"Python venv module not available"**
On Debian/Ubuntu, the venv module is packaged separately. Install it with `sudo apt install python3.XX-venv` (replace `XX` with your Python minor version, e.g., `python3.12-venv`).
**"AutoForge is already running"**
A server instance is already active. Use the browser URL shown in the terminal, or stop the existing instance with Ctrl+C first.
**Virtual environment issues after a Python upgrade**
Run `autoforge --repair` to delete and recreate the virtual environment from scratch.
--- ---
## License ## License

3
bin/autoforge.js Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env node
import { run } from '../lib/cli.js';
run(process.argv.slice(2));

791
lib/cli.js Normal file
View File

@@ -0,0 +1,791 @@
/**
* AutoForge CLI
* =============
*
* Main CLI module for the AutoForge npm global package.
* Handles Python detection, virtual environment management,
* config loading, and uvicorn server lifecycle.
*
* Uses only Node.js built-in modules -- no external dependencies.
*/
import { execFileSync, spawn, execSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync, copyFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { createServer } from 'node:net';
import { homedir, platform } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
// ---------------------------------------------------------------------------
// Path constants
// ---------------------------------------------------------------------------
/** Root of the autoforge npm package (one level up from lib/) */
const PKG_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
/** User config home: ~/.autoforge/ */
const CONFIG_HOME = join(homedir(), '.autoforge');
/** Virtual-environment directory managed by the CLI */
const VENV_DIR = join(CONFIG_HOME, 'venv');
/** Composite marker written after a successful pip install */
const DEPS_MARKER = join(VENV_DIR, '.deps-installed');
/** PID file for the running server */
const PID_FILE = join(CONFIG_HOME, 'server.pid');
/** Path to the production requirements file inside the package */
const REQUIREMENTS_FILE = join(PKG_DIR, 'requirements-prod.txt');
/** Path to the .env example shipped with the package */
const ENV_EXAMPLE = join(PKG_DIR, '.env.example');
/** User .env config file */
const ENV_FILE = join(CONFIG_HOME, '.env');
const IS_WIN = platform() === 'win32';
// ---------------------------------------------------------------------------
// Package version (read lazily via createRequire)
// ---------------------------------------------------------------------------
const require = createRequire(import.meta.url);
const { version: VERSION } = require(join(PKG_DIR, 'package.json'));
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Indented console output matching the spec format. */
function log(msg = '') {
console.log(` ${msg}`);
}
/** Print a fatal error and exit. */
function die(msg) {
console.error(`\n Error: ${msg}\n`);
process.exit(1);
}
/**
* Parse a Python version string like "Python 3.13.6" and return
* { major, minor, patch, raw } or null on failure.
*/
function parsePythonVersion(raw) {
const m = raw.match(/Python\s+(\d+)\.(\d+)\.(\d+)/);
if (!m) return null;
return {
major: Number(m[1]),
minor: Number(m[2]),
patch: Number(m[3]),
raw: `${m[1]}.${m[2]}.${m[3]}`,
};
}
/**
* Try a single Python candidate. Returns { exe, version } or null.
* `candidate` is either a bare name or an array of args (e.g. ['py', '-3']).
*/
function tryPythonCandidate(candidate) {
const args = Array.isArray(candidate) ? candidate : [candidate];
const exe = args[0];
const extraArgs = args.slice(1);
try {
const out = execFileSync(exe, [...extraArgs, '--version'], {
encoding: 'utf8',
timeout: 10_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
const ver = parsePythonVersion(out);
if (!ver) return null;
// Require 3.11+
if (ver.major < 3 || (ver.major === 3 && ver.minor < 11)) {
return { exe: args.join(' '), version: ver, tooOld: true };
}
return { exe: args.join(' '), version: ver, tooOld: false };
} catch {
return null;
}
}
// ---------------------------------------------------------------------------
// Python detection
// ---------------------------------------------------------------------------
/**
* Find a suitable Python >= 3.11 interpreter.
*
* Search order is platform-dependent:
* Windows: python -> py -3 -> python3
* macOS/Linux: python3 -> python
*
* The AUTOFORGE_PYTHON env var overrides automatic detection.
*
* After finding a candidate we also verify that the venv module is
* available (Debian/Ubuntu strip it out of the base package).
*/
function findPython() {
// Allow explicit override via environment variable
const override = process.env.AUTOFORGE_PYTHON;
if (override) {
const result = tryPythonCandidate(override);
if (!result) {
die(`AUTOFORGE_PYTHON is set to "${override}" but it could not be executed.`);
}
if (result.tooOld) {
die(
`Python ${result.version.raw} found (via AUTOFORGE_PYTHON), but 3.11+ required.\n` +
' Install Python 3.11+ from https://python.org'
);
}
return result;
}
// Platform-specific candidate order
const candidates = IS_WIN
? ['python', ['py', '-3'], 'python3']
: ['python3', 'python'];
let bestTooOld = null;
for (const candidate of candidates) {
const result = tryPythonCandidate(candidate);
if (!result) continue;
if (result.tooOld) {
// Remember the first "too old" result for a better error message
if (!bestTooOld) bestTooOld = result;
continue;
}
// Verify venv module is available (Debian/Ubuntu may need python3-venv)
try {
const exeParts = result.exe.split(' ');
execFileSync(exeParts[0], [...exeParts.slice(1), '-c', 'import ensurepip'], {
encoding: 'utf8',
timeout: 10_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch {
die(
`Python venv module not available.\n` +
` Run: sudo apt install python3.${result.version.minor}-venv`
);
}
return result;
}
// Provide the most helpful error message we can
if (bestTooOld) {
die(
`Python ${bestTooOld.version.raw} found, but 3.11+ required.\n` +
' Install Python 3.11+ from https://python.org'
);
}
die(
'Python 3.11+ required but not found.\n' +
' Install from https://python.org'
);
}
// ---------------------------------------------------------------------------
// Venv management
// ---------------------------------------------------------------------------
/** Return the path to the Python executable inside the venv. */
function venvPython() {
return IS_WIN
? join(VENV_DIR, 'Scripts', 'python.exe')
: join(VENV_DIR, 'bin', 'python');
}
/** SHA-256 hash of the requirements-prod.txt file contents. */
function requirementsHash() {
const content = readFileSync(REQUIREMENTS_FILE, 'utf8');
return createHash('sha256').update(content).digest('hex');
}
/**
* Read the composite deps marker. Returns the parsed JSON object
* or null if the file is missing / corrupt.
*/
function readMarker() {
try {
return JSON.parse(readFileSync(DEPS_MARKER, 'utf8'));
} catch {
return null;
}
}
/**
* Ensure the virtual environment exists and dependencies are installed.
* Returns true if all setup steps were already satisfied (fast path).
*
* @param {object} python - The result of findPython()
* @param {boolean} forceRecreate - If true, delete and recreate the venv
*/
function ensureVenv(python, forceRecreate) {
mkdirSync(CONFIG_HOME, { recursive: true });
const marker = readMarker();
const reqHash = requirementsHash();
const pyExe = venvPython();
// Determine if the venv itself needs to be (re)created
let needsCreate = forceRecreate || !existsSync(pyExe);
if (!needsCreate && marker) {
// Recreate if Python major.minor changed
const markerMinor = marker.python_version;
const currentMinor = `${python.version.major}.${python.version.minor}`;
if (markerMinor && markerMinor !== currentMinor) {
needsCreate = true;
}
// Recreate if the recorded python path no longer exists
if (marker.python_path && !existsSync(marker.python_path)) {
needsCreate = true;
}
}
let depsUpToDate = false;
if (!needsCreate && marker && marker.requirements_hash === reqHash) {
depsUpToDate = true;
}
// Fast path: nothing to do
if (!needsCreate && depsUpToDate) {
return true;
}
// --- Slow path: show setup progress ---
log('[2/3] Setting up environment...');
if (needsCreate) {
if (existsSync(VENV_DIR)) {
log(' Removing old virtual environment...');
rmSync(VENV_DIR, { recursive: true, force: true });
}
log(` Creating virtual environment at ~/.autoforge/venv/`);
const exeParts = python.exe.split(' ');
try {
execFileSync(exeParts[0], [...exeParts.slice(1), '-m', 'venv', VENV_DIR], {
encoding: 'utf8',
timeout: 120_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (err) {
die(`Failed to create virtual environment: ${err.message}`);
}
}
// Install / update dependencies
log(' Installing dependencies...');
try {
execFileSync(pyExe, ['-m', 'pip', 'install', '-q', '--upgrade', 'pip'], {
encoding: 'utf8',
timeout: 300_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
execFileSync(pyExe, ['-m', 'pip', 'install', '-q', '-r', REQUIREMENTS_FILE], {
encoding: 'utf8',
timeout: 600_000,
stdio: ['pipe', 'pipe', 'pipe'],
});
} catch (err) {
die(`Failed to install dependencies: ${err.message}`);
}
// Write marker only after pip succeeds to prevent partial state
const markerData = {
requirements_hash: reqHash,
python_version: `${python.version.major}.${python.version.minor}`,
python_path: pyExe,
created_at: new Date().toISOString(),
};
writeFileSync(DEPS_MARKER, JSON.stringify(markerData, null, 2), 'utf8');
log(' Done');
return false;
}
// ---------------------------------------------------------------------------
// Config (.env) management
// ---------------------------------------------------------------------------
/**
* Parse a .env file into a plain object.
* Handles comments, blank lines, and quoted values.
*/
function parseEnvFile(filePath) {
const env = {};
if (!existsSync(filePath)) return env;
const lines = readFileSync(filePath, 'utf8').split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIdx = trimmed.indexOf('=');
if (eqIdx === -1) continue;
const key = trimmed.slice(0, eqIdx).trim();
let value = trimmed.slice(eqIdx + 1).trim();
// Strip matching quotes (single or double)
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (key) {
env[key] = value;
}
}
return env;
}
/**
* Ensure ~/.autoforge/.env exists. On first run, copy .env.example
* from the package directory and print a notice.
*
* Returns true if the file was newly created.
*/
function ensureEnvFile() {
if (existsSync(ENV_FILE)) return false;
mkdirSync(CONFIG_HOME, { recursive: true });
if (existsSync(ENV_EXAMPLE)) {
copyFileSync(ENV_EXAMPLE, ENV_FILE);
} else {
// Fallback: create a minimal placeholder
writeFileSync(ENV_FILE, '# AutoForge configuration\n# See documentation for available options.\n', 'utf8');
}
return true;
}
// ---------------------------------------------------------------------------
// Port detection
// ---------------------------------------------------------------------------
/**
* Find an available TCP port starting from `start`.
* Tries by actually binding a socket (most reliable cross-platform approach).
*/
function findAvailablePort(start = 8888, maxAttempts = 20) {
for (let port = start; port < start + maxAttempts; port++) {
try {
const server = createServer();
// Use a synchronous-like approach: try to listen, then close immediately
const result = new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(port, '127.0.0.1', () => {
server.close(() => resolve(port));
});
});
// We cannot await here (sync context), so use the blocking approach:
// Try to bind synchronously using a different technique.
server.close();
} catch {
// fall through
}
}
// Synchronous fallback: try to connect; if connection refused, port is free.
for (let port = start; port < start + maxAttempts; port++) {
try {
execFileSync(process.execPath, [
'-e',
`const s=require("net").createServer();` +
`s.listen(${port},"127.0.0.1",()=>{s.close();process.exit(0)});` +
`s.on("error",()=>process.exit(1))`,
], { timeout: 3000, stdio: 'pipe' });
return port;
} catch {
continue;
}
}
die(`No available ports found in range ${start}-${start + maxAttempts - 1}`);
}
// ---------------------------------------------------------------------------
// PID file management
// ---------------------------------------------------------------------------
/** Read PID from the PID file. Returns the PID number or null. */
function readPid() {
try {
const content = readFileSync(PID_FILE, 'utf8').trim();
const pid = Number(content);
return Number.isFinite(pid) && pid > 0 ? pid : null;
} catch {
return null;
}
}
/** Check whether a process with the given PID is still running. */
function isProcessAlive(pid) {
try {
process.kill(pid, 0); // signal 0 = existence check
return true;
} catch {
return false;
}
}
/** Write the PID file. */
function writePid(pid) {
mkdirSync(CONFIG_HOME, { recursive: true });
writeFileSync(PID_FILE, String(pid), 'utf8');
}
/** Remove the PID file. */
function removePid() {
try {
unlinkSync(PID_FILE);
} catch {
// Ignore -- file may already be gone
}
}
// ---------------------------------------------------------------------------
// Browser opening
// ---------------------------------------------------------------------------
/** Open a URL in the user's default browser (best-effort). */
function openBrowser(url) {
try {
if (IS_WIN) {
// "start" is a cmd built-in; the empty title string avoids
// issues when the URL contains special characters.
execSync(`start "" "${url}"`, { stdio: 'ignore' });
} else if (platform() === 'darwin') {
execFileSync('open', [url], { stdio: 'ignore' });
} else {
// Linux: only attempt if a display server is available and
// we are not in an SSH session.
const hasDisplay = process.env.DISPLAY || process.env.WAYLAND_DISPLAY;
const isSSH = !!process.env.SSH_TTY;
if (hasDisplay && !isSSH) {
execFileSync('xdg-open', [url], { stdio: 'ignore' });
}
}
} catch {
// Non-fatal: user can open the URL manually
}
}
/** Detect headless / CI environments where opening a browser is pointless. */
function isHeadless() {
if (process.env.CI) return true;
if (process.env.CODESPACES) return true;
if (process.env.SSH_TTY) return true;
// Linux without a display server
if (!IS_WIN && platform() !== 'darwin' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
return true;
}
return false;
}
// ---------------------------------------------------------------------------
// Process cleanup
// ---------------------------------------------------------------------------
/** Kill a process tree. On Windows uses taskkill; elsewhere sends SIGTERM. */
function killProcess(pid) {
try {
if (IS_WIN) {
execSync(`taskkill /pid ${pid} /t /f`, { stdio: 'ignore' });
} else {
process.kill(pid, 'SIGTERM');
}
} catch {
// Process may already be gone
}
}
// ---------------------------------------------------------------------------
// CLI commands
// ---------------------------------------------------------------------------
function printVersion() {
console.log(`autoforge v${VERSION}`);
}
function printHelp() {
console.log(`
AutoForge v${VERSION}
Autonomous coding agent with web UI
Usage:
autoforge Start the server (default)
autoforge config Open ~/.autoforge/.env in $EDITOR
autoforge config --path Print config file path
autoforge config --show Show effective configuration
Options:
--port PORT Custom port (default: auto from 8888)
--host HOST Custom host (default: 127.0.0.1)
--no-browser Don't auto-open browser
--repair Delete and recreate virtual environment
--dev Development mode (requires cloned repo)
--version Print version
--help Show this help
`);
}
function handleConfig(args) {
ensureEnvFile();
if (args.includes('--path')) {
console.log(ENV_FILE);
return;
}
if (args.includes('--show')) {
if (!existsSync(ENV_FILE)) {
log('No configuration file found.');
return;
}
const lines = readFileSync(ENV_FILE, 'utf8').split('\n');
const active = lines.filter(l => {
const t = l.trim();
return t && !t.startsWith('#');
});
if (active.length === 0) {
log('No active configuration. All lines are commented out.');
log(`Edit: ${ENV_FILE}`);
} else {
for (const line of active) {
console.log(line);
}
}
return;
}
// Open in editor
const editor = process.env.EDITOR || process.env.VISUAL || (IS_WIN ? 'notepad' : 'vi');
try {
execFileSync(editor, [ENV_FILE], { stdio: 'inherit' });
} catch {
log(`Could not open editor "${editor}".`);
log(`Edit the file manually: ${ENV_FILE}`);
}
}
// ---------------------------------------------------------------------------
// Main server start
// ---------------------------------------------------------------------------
function startServer(opts) {
const { port: requestedPort, host, noBrowser, repair } = opts;
// Step 1: Find Python
const fastPath = !repair && existsSync(venvPython()) && readMarker()?.requirements_hash === requirementsHash();
let python;
if (fastPath) {
// Skip the Python search header on fast path -- we already have a working venv
python = null;
} else {
log(`[1/3] Checking Python...`);
python = findPython();
log(` Found Python ${python.version.raw} at ${python.exe}`);
}
// Step 2: Ensure venv and deps
if (!python) {
// Fast path still needs a python reference for potential repair
python = findPython();
}
const wasAlreadyReady = ensureVenv(python, repair);
// Step 3: Config file
const configCreated = ensureEnvFile();
// Load .env into process.env for the spawned server
const dotenvVars = parseEnvFile(ENV_FILE);
// Determine port
const port = requestedPort || findAvailablePort();
// Check for already-running instance
const existingPid = readPid();
if (existingPid && isProcessAlive(existingPid)) {
log(`AutoForge is already running at http://${host}:${port}`);
log('Opening browser...');
if (!noBrowser && !isHeadless()) {
openBrowser(`http://${host}:${port}`);
}
return;
}
// Clean up stale PID file
if (existingPid) {
removePid();
}
// Show server startup step only on slow path
if (!wasAlreadyReady) {
log('[3/3] Starting server...');
}
if (configCreated) {
log(` Created config file: ~/.autoforge/.env`);
log(' Edit this file to configure API providers (Ollama, Vertex AI, z.ai)');
log('');
}
// Security warning for non-localhost host
if (host !== '127.0.0.1') {
console.log('');
console.log(' !! SECURITY WARNING !!');
console.log(` Remote access enabled on host: ${host}`);
console.log(' The AutoForge UI will be accessible from other machines.');
console.log(' Ensure you understand the security implications.');
console.log('');
}
// Build environment for uvicorn
const serverEnv = { ...process.env, ...dotenvVars, PYTHONPATH: PKG_DIR };
// Enable remote access flag for the FastAPI server
if (host !== '127.0.0.1') {
serverEnv.AUTOFORGE_ALLOW_REMOTE = '1';
}
// Spawn uvicorn
const pyExe = venvPython();
const child = spawn(
pyExe,
[
'-m', 'uvicorn',
'server.main:app',
'--host', host,
'--port', String(port),
],
{
cwd: PKG_DIR,
env: serverEnv,
stdio: 'inherit',
}
);
writePid(child.pid);
// Open browser after a short delay to let the server start
if (!noBrowser && !isHeadless()) {
setTimeout(() => openBrowser(`http://${host}:${port}`), 2000);
}
const url = `http://${host}:${port}`;
console.log('');
log(`Server running at ${url}`);
log('Press Ctrl+C to stop');
// Graceful shutdown handlers
const cleanup = () => {
killProcess(child.pid);
removePid();
};
process.on('SIGINT', () => {
console.log('');
cleanup();
process.exit(0);
});
process.on('SIGTERM', () => {
cleanup();
process.exit(0);
});
// If the child exits on its own, clean up and propagate the exit code
child.on('exit', (code) => {
removePid();
process.exit(code ?? 1);
});
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
/**
* Main CLI entry point.
*
* @param {string[]} args - Command-line arguments (process.argv.slice(2))
*/
export function run(args) {
// --version / -v
if (args.includes('--version') || args.includes('-v')) {
printVersion();
return;
}
// --help / -h
if (args.includes('--help') || args.includes('-h')) {
printHelp();
return;
}
// --dev guard: this only works from a cloned repository
if (args.includes('--dev')) {
die(
'Dev mode requires a cloned repository.\n' +
' Clone from https://github.com/paperlinguist/autocoder and run start_ui.sh'
);
return;
}
// "config" subcommand
if (args[0] === 'config') {
handleConfig(args.slice(1));
return;
}
// Parse flags for server start
const host = getFlagValue(args, '--host') || '127.0.0.1';
const portStr = getFlagValue(args, '--port');
const port = portStr ? Number(portStr) : null;
const noBrowser = args.includes('--no-browser');
const repair = args.includes('--repair');
if (port !== null && (!Number.isFinite(port) || port < 1 || port > 65535)) {
die('Invalid port number. Must be between 1 and 65535.');
}
// Print banner
console.log('');
log(`AutoForge v${VERSION}`);
console.log('');
startServer({ port, host, noBrowser, repair });
}
// ---------------------------------------------------------------------------
// Argument parsing helpers
// ---------------------------------------------------------------------------
/**
* Extract the value following a flag from the args array.
* E.g. getFlagValue(['--port', '9000', '--host', '0.0.0.0'], '--port') => '9000'
*/
function getFlagValue(args, flag) {
const idx = args.indexOf(flag);
if (idx === -1 || idx + 1 >= args.length) return null;
return args[idx + 1];
}

53
package.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "autoforge-ai",
"version": "0.1.0",
"description": "Autonomous coding agent with web UI - build complete apps with AI",
"license": "AGPL-3.0",
"bin": {
"autoforge": "./bin/autoforge.js"
},
"type": "module",
"engines": {
"node": ">=20"
},
"files": [
"bin/",
"lib/",
"api/",
"server/",
"mcp_server/",
"ui/dist/",
"ui/package.json",
".claude/commands/",
".claude/templates/",
"examples/",
"start.py",
"agent.py",
"auth.py",
"autoforge_paths.py",
"autonomous_agent_demo.py",
"client.py",
"env_constants.py",
"parallel_orchestrator.py",
"progress.py",
"prompts.py",
"registry.py",
"rate_limit_utils.py",
"security.py",
"requirements-prod.txt",
"pyproject.toml",
".env.example",
"!**/__pycache__/",
"!**/*.pyc"
],
"keywords": [
"ai",
"coding-agent",
"claude",
"autonomous",
"code-generation"
],
"scripts": {
"prepublishOnly": "npm --prefix ui install && npm --prefix ui run build"
}
}

14
requirements-prod.txt Normal file
View File

@@ -0,0 +1,14 @@
# Production runtime dependencies only
# For development, use requirements.txt (includes ruff, mypy, pytest)
claude-agent-sdk>=0.1.0,<0.2.0
python-dotenv>=1.0.0
sqlalchemy>=2.0.0
fastapi>=0.115.0
uvicorn[standard]>=0.32.0
websockets>=13.0
python-multipart>=0.0.17
psutil>=6.0.0
aiofiles>=24.0.0
apscheduler>=3.10.0,<4.0.0
pywinpty>=2.0.0; sys_platform == "win32"
pyyaml>=6.0.0

16
ui/package-lock.json generated
View File

@@ -1,11 +1,11 @@
{ {
"name": "autoforge", "name": "autoforge-ui",
"version": "1.0.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "autoforge", "name": "autoforge-ui",
"version": "1.0.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
@@ -50,6 +50,18 @@
"vite": "^7.3.0" "vite": "^7.3.0"
} }
}, },
"..": {
"name": "autoforge-ai",
"version": "0.1.0",
"extraneous": true,
"license": "AGPL-3.0",
"bin": {
"autoforge": "bin/autoforge.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",

View File

@@ -1,5 +1,5 @@
{ {
"name": "autoforge", "name": "autoforge-ui",
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",

View File

@@ -35,12 +35,14 @@ export const DOC_SECTIONS: DocSection[] = [
icon: Rocket, icon: Rocket,
subsections: [ subsections: [
{ id: 'what-is-autoforge', title: 'What is AutoForge?' }, { id: 'what-is-autoforge', title: 'What is AutoForge?' },
{ id: 'installation', title: 'Installation' },
{ id: 'quick-start', title: 'Quick Start' }, { id: 'quick-start', title: 'Quick Start' },
{ id: 'cli-commands', title: 'CLI Commands' },
{ id: 'creating-a-project', title: 'Creating a New Project' }, { id: 'creating-a-project', title: 'Creating a New Project' },
{ id: 'existing-project', title: 'Adding to an Existing Project' }, { id: 'existing-project', title: 'Adding to an Existing Project' },
{ id: 'system-requirements', title: 'System Requirements' }, { id: 'system-requirements', title: 'System Requirements' },
], ],
keywords: ['install', 'setup', 'start', 'begin', 'new', 'requirements', 'prerequisites'], keywords: ['install', 'setup', 'start', 'begin', 'new', 'requirements', 'prerequisites', 'npm', 'config', 'port', 'repair'],
}, },
{ {
id: 'app-spec-setup', id: 'app-spec-setup',

View File

@@ -7,6 +7,25 @@
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
/** CLI command descriptor for the reference table. */
interface CliCommand {
command: string
description: string
}
const CLI_COMMANDS: CliCommand[] = [
{ command: 'autoforge', description: 'Start the server (default)' },
{ command: 'autoforge config', description: 'Open ~/.autoforge/.env in your editor' },
{ command: 'autoforge config --path', description: 'Print config file path' },
{ command: 'autoforge config --show', description: 'Show active configuration values' },
{ command: 'autoforge --port PORT', description: 'Custom port (default: auto from 8888)' },
{ command: 'autoforge --host HOST', description: 'Custom host (default: 127.0.0.1)' },
{ command: 'autoforge --no-browser', description: "Don't auto-open browser" },
{ command: 'autoforge --repair', description: 'Delete and recreate virtual environment' },
{ command: 'autoforge --version', description: 'Print version' },
{ command: 'autoforge --help', description: 'Show help' },
]
export function GettingStarted() { export function GettingStarted() {
return ( return (
<div> <div>
@@ -33,24 +52,141 @@ export function GettingStarted() {
in real time. in real time.
</p> </p>
{/* Installation */}
<h3 id="installation" className="text-lg font-semibold text-foreground mt-8 mb-3">
Installation
</h3>
<p className="text-muted-foreground mb-3">
Install AutoForge globally via npm:
</p>
<div className="bg-muted rounded-lg p-4 font-mono text-sm">
<pre><code>{`npm install -g autoforge-ai`}</code></pre>
</div>
<p className="text-muted-foreground mt-3">
This requires{' '}
<strong className="text-foreground">Node.js 20+</strong> and{' '}
<strong className="text-foreground">Python 3.11+</strong>.
Python is auto-detected on first run.
</p>
{/* Quick Start */} {/* Quick Start */}
<h3 id="quick-start" className="text-lg font-semibold text-foreground mt-8 mb-3"> <h3 id="quick-start" className="text-lg font-semibold text-foreground mt-8 mb-3">
Quick Start Quick Start
</h3> </h3>
<p className="text-muted-foreground mb-3"> <p className="text-muted-foreground mb-3">
Launch AutoForge with a single command. The CLI menu lets you create or select a project, After installing, start AutoForge with a single command:
while the Web UI provides a full dashboard experience. </p>
<div className="bg-muted rounded-lg p-4 font-mono text-sm">
<pre><code>{`autoforge`}</code></pre>
</div>
<p className="text-muted-foreground mt-3 mb-2">
On first run, AutoForge automatically:
</p>
<ol className="list-decimal space-y-1 ml-4 text-muted-foreground">
<li>Checks for Python 3.11+</li>
<li>Creates a virtual environment at{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">~/.autoforge/venv/</span>
</li>
<li>Installs Python dependencies</li>
<li>Copies a default config file to{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">~/.autoforge/.env</span>
</li>
<li>Starts the server and opens your browser</li>
</ol>
<p className="text-muted-foreground mt-3">
On subsequent runs, AutoForge starts instantly &mdash; the environment is already set up.
</p>
<h4 className="text-base font-semibold text-foreground mt-6 mb-3">
Running from Source
</h4>
<p className="text-muted-foreground mb-3">
If you prefer to clone the repository (for development or contributing):
</p> </p>
<div className="bg-muted rounded-lg p-4 font-mono text-sm"> <div className="bg-muted rounded-lg p-4 font-mono text-sm">
<pre><code>{`# Windows <pre><code>{`# Windows
start.bat # CLI menu
start_ui.bat # Web UI start_ui.bat # Web UI
# macOS/Linux # macOS/Linux
./start.sh # CLI menu
./start_ui.sh # Web UI`}</code></pre> ./start_ui.sh # Web UI`}</code></pre>
</div> </div>
{/* CLI Commands */}
<h3 id="cli-commands" className="text-lg font-semibold text-foreground mt-8 mb-3">
CLI Commands
</h3>
<p className="text-muted-foreground mb-3">
The{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">autoforge</span>{' '}
command supports these options:
</p>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Command
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Description
</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
{CLI_COMMANDS.map((c) => (
<tr key={c.command}>
<td className="border border-border px-3 py-2">
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">{c.command}</span>
</td>
<td className="border border-border px-3 py-2">{c.description}</td>
</tr>
))}
</tbody>
</table>
<h4 className="text-base font-semibold text-foreground mt-6 mb-3">
Configuration
</h4>
<p className="text-muted-foreground mb-3">
AutoForge reads configuration from a{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.env</span>{' '}
file. The location depends on your install method:
</p>
<table className="w-full text-sm mt-3">
<thead>
<tr className="bg-muted/50">
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Install method
</th>
<th className="border border-border px-3 py-2 text-left font-medium text-foreground">
Config location
</th>
</tr>
</thead>
<tbody className="text-muted-foreground">
<tr>
<td className="border border-border px-3 py-2">npm (global)</td>
<td className="border border-border px-3 py-2">
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">~/.autoforge/.env</span>
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">From source</td>
<td className="border border-border px-3 py-2">
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">.env</span>{' '}
in the project root
</td>
</tr>
</tbody>
</table>
<p className="text-muted-foreground mt-3">
Run{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">autoforge config</span>{' '}
to open the config file in your editor, or{' '}
<span className="bg-muted px-1.5 py-0.5 rounded text-sm font-mono">autoforge config --show</span>{' '}
to print the active values. See{' '}
<a href="#/docs/advanced-config" className="text-primary underline">Advanced Configuration</a>{' '}
for API provider setup (Ollama, Vertex AI, z.ai).
</p>
{/* Creating a New Project */} {/* Creating a New Project */}
<h3 id="creating-a-project" className="text-lg font-semibold text-foreground mt-8 mb-3"> <h3 id="creating-a-project" className="text-lg font-semibold text-foreground mt-8 mb-3">
Creating a New Project Creating a New Project
@@ -102,17 +238,18 @@ start_ui.bat # Web UI
</tr> </tr>
</thead> </thead>
<tbody className="text-muted-foreground"> <tbody className="text-muted-foreground">
<tr>
<td className="border border-border px-3 py-2">Python</td>
<td className="border border-border px-3 py-2">
<Badge variant="secondary">3.11+</Badge>
</td>
</tr>
<tr> <tr>
<td className="border border-border px-3 py-2">Node.js</td> <td className="border border-border px-3 py-2">Node.js</td>
<td className="border border-border px-3 py-2"> <td className="border border-border px-3 py-2">
<Badge variant="secondary">20+</Badge>{' '} <Badge variant="secondary">20+</Badge>{' '}
<span className="text-xs">(for UI development)</span> <span className="text-xs">(required for CLI and UI)</span>
</td>
</tr>
<tr>
<td className="border border-border px-3 py-2">Python</td>
<td className="border border-border px-3 py-2">
<Badge variant="secondary">3.11+</Badge>{' '}
<span className="text-xs">(auto-detected on first run)</span>
</td> </td>
</tr> </tr>
<tr> <tr>