mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-02-05 08:23:08 +00:00
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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
32
.npmignore
Normal 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
142
README.md
@@ -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
3
bin/autoforge.js
Normal 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
791
lib/cli.js
Normal 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
53
package.json
Normal 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
14
requirements-prod.txt
Normal 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
16
ui/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 — 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user