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/.vite/
|
||||
.vite/
|
||||
*.tgz
|
||||
|
||||
# ===================
|
||||
# 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
|
||||
186
README.md
186
README.md
@@ -14,9 +14,11 @@ A long-running autonomous coding agent powered by the Claude Agent SDK. This too
|
||||
|
||||
## 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:**
|
||||
```bash
|
||||
@@ -39,35 +41,63 @@ You need one of the following:
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Web UI (Recommended)
|
||||
### Option 1: npm Install (Recommended)
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
start_ui.bat
|
||||
```
|
||||
|
||||
**macOS / Linux:**
|
||||
```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:
|
||||
- Project selection and creation
|
||||
- Kanban board view of features
|
||||
- Real-time agent output streaming
|
||||
- Start/pause/stop controls
|
||||
|
||||
### Option 2: CLI Mode
|
||||
**CLI Mode:**
|
||||
|
||||
**Windows:**
|
||||
```cmd
|
||||
start.bat
|
||||
```
|
||||
|
||||
**macOS / Linux:**
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
| Platform | Command |
|
||||
|---|---|
|
||||
| Windows | `start.bat` |
|
||||
| macOS / Linux | `./start.sh` |
|
||||
|
||||
The start script will:
|
||||
1. Check if Claude CLI is installed
|
||||
@@ -130,44 +160,43 @@ Features are stored in SQLite via SQLAlchemy and managed through an MCP server t
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
autonomous-coding/
|
||||
├── start.bat # Windows CLI start script
|
||||
├── start.sh # macOS/Linux CLI start script
|
||||
├── 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_ui.py # Web UI backend (FastAPI server launcher)
|
||||
├── autonomous_agent_demo.py # Agent entry point
|
||||
├── agent.py # Agent session logic
|
||||
├── client.py # Claude SDK client configuration
|
||||
├── security.py # Bash command allowlist and validation
|
||||
├── progress.py # Progress tracking utilities
|
||||
├── prompts.py # Prompt loading utilities
|
||||
autoforge/
|
||||
├── bin/ # npm CLI entry point
|
||||
├── lib/ # CLI bootstrap and setup logic
|
||||
├── start.py # CLI menu and project management
|
||||
├── start_ui.py # Web UI backend (FastAPI server launcher)
|
||||
├── autonomous_agent_demo.py # Agent entry point
|
||||
├── agent.py # Agent session logic
|
||||
├── client.py # Claude SDK client configuration
|
||||
├── security.py # Bash command allowlist and validation
|
||||
├── progress.py # Progress tracking utilities
|
||||
├── prompts.py # Prompt loading utilities
|
||||
├── api/
|
||||
│ └── database.py # SQLAlchemy models (Feature table)
|
||||
│ └── database.py # SQLAlchemy models (Feature table)
|
||||
├── mcp_server/
|
||||
│ └── feature_mcp.py # MCP server for feature management tools
|
||||
│ └── feature_mcp.py # MCP server for feature management tools
|
||||
├── server/
|
||||
│ ├── main.py # FastAPI REST API server
|
||||
│ ├── websocket.py # WebSocket handler for real-time updates
|
||||
│ ├── schemas.py # Pydantic schemas
|
||||
│ ├── routers/ # API route handlers
|
||||
│ └── services/ # Business logic services
|
||||
├── ui/ # React frontend
|
||||
│ ├── main.py # FastAPI REST API server
|
||||
│ ├── websocket.py # WebSocket handler for real-time updates
|
||||
│ ├── schemas.py # Pydantic schemas
|
||||
│ ├── routers/ # API route handlers
|
||||
│ └── services/ # Business logic services
|
||||
├── ui/ # React frontend
|
||||
│ ├── src/
|
||||
│ │ ├── App.tsx # Main app component
|
||||
│ │ ├── hooks/ # React Query and WebSocket hooks
|
||||
│ │ └── lib/ # API client and types
|
||||
│ │ ├── App.tsx # Main app component
|
||||
│ │ ├── hooks/ # React Query and WebSocket hooks
|
||||
│ │ └── lib/ # API client and types
|
||||
│ ├── package.json
|
||||
│ └── vite.config.ts
|
||||
├── .claude/
|
||||
│ ├── commands/
|
||||
│ │ └── create-spec.md # /create-spec slash command
|
||||
│ ├── skills/ # Claude Code skills
|
||||
│ └── templates/ # Prompt templates
|
||||
├── generations/ # Generated projects go here
|
||||
├── requirements.txt # Python dependencies
|
||||
└── .env # Optional configuration (N8N webhook)
|
||||
│ │ └── create-spec.md # /create-spec slash command
|
||||
│ ├── skills/ # Claude Code skills
|
||||
│ └── templates/ # Prompt templates
|
||||
├── requirements.txt # Python dependencies (development)
|
||||
├── requirements-prod.txt # Python dependencies (npm install)
|
||||
├── 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
|
||||
|
||||
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
|
||||
# Optional: N8N webhook for progress notifications
|
||||
@@ -290,7 +328,7 @@ When test progress increases, the agent sends:
|
||||
|
||||
### 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
|
||||
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
|
||||
|
||||
### 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
|
||||
@@ -335,6 +403,18 @@ This is normal. The initializer agent is generating detailed test cases, which t
|
||||
**"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`.
|
||||
|
||||
**"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
|
||||
|
||||
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",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "autoforge",
|
||||
"name": "autoforge-ui",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
@@ -50,6 +50,18 @@
|
||||
"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": {
|
||||
"version": "7.27.1",
|
||||
"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,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -35,12 +35,14 @@ export const DOC_SECTIONS: DocSection[] = [
|
||||
icon: Rocket,
|
||||
subsections: [
|
||||
{ id: 'what-is-autoforge', title: 'What is AutoForge?' },
|
||||
{ id: 'installation', title: 'Installation' },
|
||||
{ id: 'quick-start', title: 'Quick Start' },
|
||||
{ id: 'cli-commands', title: 'CLI Commands' },
|
||||
{ id: 'creating-a-project', title: 'Creating a New Project' },
|
||||
{ id: 'existing-project', title: 'Adding to an Existing Project' },
|
||||
{ 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',
|
||||
|
||||
@@ -7,6 +7,25 @@
|
||||
|
||||
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() {
|
||||
return (
|
||||
<div>
|
||||
@@ -33,24 +52,141 @@ export function GettingStarted() {
|
||||
in real time.
|
||||
</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 */}
|
||||
<h3 id="quick-start" className="text-lg font-semibold text-foreground mt-8 mb-3">
|
||||
Quick Start
|
||||
</h3>
|
||||
<p className="text-muted-foreground mb-3">
|
||||
Launch AutoForge with a single command. The CLI menu lets you create or select a project,
|
||||
while the Web UI provides a full dashboard experience.
|
||||
After installing, start AutoForge with a single command:
|
||||
</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>
|
||||
<div className="bg-muted rounded-lg p-4 font-mono text-sm">
|
||||
<pre><code>{`# Windows
|
||||
start.bat # CLI menu
|
||||
start_ui.bat # Web UI
|
||||
|
||||
# macOS/Linux
|
||||
./start.sh # CLI menu
|
||||
./start_ui.sh # Web UI`}</code></pre>
|
||||
</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 */}
|
||||
<h3 id="creating-a-project" className="text-lg font-semibold text-foreground mt-8 mb-3">
|
||||
Creating a New Project
|
||||
@@ -102,17 +238,18 @@ start_ui.bat # Web UI
|
||||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
<td className="border border-border px-3 py-2">Node.js</td>
|
||||
<td className="border border-border px-3 py-2">
|
||||
<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>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
Reference in New Issue
Block a user