diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f4ea1d33 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,137 @@ +name: Release CLI Binaries + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version tag (e.g., v0.2.0)' + required: true + type: string + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + build: + strategy: + matrix: + include: + - os: ubuntu-latest + target: bun-linux-x64 + binary: archon-linux-x64 + - os: ubuntu-latest + target: bun-linux-arm64 + binary: archon-linux-arm64 + - os: macos-latest + target: bun-darwin-x64 + binary: archon-darwin-x64 + - os: macos-latest + target: bun-darwin-arm64 + binary: archon-darwin-arm64 + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.4 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build binary + run: | + mkdir -p dist + bun build --compile --target=${{ matrix.target }} --outfile=dist/${{ matrix.binary }} packages/cli/src/cli.ts + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.binary }} + path: dist/${{ matrix.binary }} + retention-days: 1 + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Generate checksums + run: | + cd dist + sha256sum archon-* > checksums.txt + cat checksums.txt + + - name: Get version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT + else + echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version.outputs.version }} + name: Archon CLI ${{ steps.version.outputs.version }} + draft: false + prerelease: ${{ contains(steps.version.outputs.version, '-') }} + generate_release_notes: true + files: | + dist/archon-* + dist/checksums.txt + body: | + ## Installation + + ### Quick Install (Recommended) + ```bash + curl -fsSL https://raw.githubusercontent.com/raswonders/remote-coding-agent/main/scripts/install.sh | bash + ``` + + ### Manual Installation + + **macOS (Apple Silicon)** + ```bash + curl -fsSL https://github.com/raswonders/remote-coding-agent/releases/latest/download/archon-darwin-arm64 -o /usr/local/bin/archon + chmod +x /usr/local/bin/archon + ``` + + **macOS (Intel)** + ```bash + curl -fsSL https://github.com/raswonders/remote-coding-agent/releases/latest/download/archon-darwin-x64 -o /usr/local/bin/archon + chmod +x /usr/local/bin/archon + ``` + + **Linux (x64)** + ```bash + curl -fsSL https://github.com/raswonders/remote-coding-agent/releases/latest/download/archon-linux-x64 -o /usr/local/bin/archon + chmod +x /usr/local/bin/archon + ``` + + **Linux (ARM64)** + ```bash + curl -fsSL https://github.com/raswonders/remote-coding-agent/releases/latest/download/archon-linux-arm64 -o /usr/local/bin/archon + chmod +x /usr/local/bin/archon + ``` + + ### Verify installation + ```bash + archon version + ``` diff --git a/CLAUDE.md b/CLAUDE.md index 60a1ab21..12d5ffb6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -626,8 +626,9 @@ console.log('Processing...'); **Defaults:** - Bundled in `.archon/commands/defaults/` and `.archon/workflows/defaults/` -- Loaded at runtime (merged with repo-specific commands/workflows) -- Repo commands/workflows override app defaults by name +- Binary builds: Embedded at compile time (no filesystem access needed) +- Source builds: Loaded from filesystem at runtime +- Merged with repo-specific commands/workflows (repo overrides defaults by name) - Opt-out: Set `defaults.loadDefaultCommands: false` or `defaults.loadDefaultWorkflows: false` in `.archon/config.yaml` ### Error Handling diff --git a/Dockerfile b/Dockerfile index f9d3be8a..3910bcd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:1-slim +FROM oven/bun:1.3.4-slim # OCI Labels for GHCR LABEL org.opencontainers.image.source="https://github.com/dynamous-community/remote-coding-agent" diff --git a/README.md b/README.md index 68eb429e..34c74861 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Control AI coding assistants (Claude Code, Codex) remotely from Telegram, GitHub, and more. Built for developers who want to code from anywhere with persistent sessions and flexible workflows/systems. -**Quick Start:** [Core Configuration](#1-core-configuration-required) • [AI Assistant Setup](#2-ai-assistant-setup-choose-at-least-one) • [Platform Setup](#3-platform-adapter-setup-choose-at-least-one) • [Start the App](#4-start-the-application) • [Usage Guide](#usage) +**Quick Start:** [CLI Installation](#cli-installation) • [Server Setup](#server-quick-start) • [AI Assistant Setup](#2-ai-assistant-setup-choose-at-least-one) • [Platform Setup](#3-platform-adapter-setup-choose-at-least-one) • [Usage Guide](#usage) ## Features @@ -14,20 +14,71 @@ Control AI coding assistants (Claude Code, Codex) remotely from Telegram, GitHub - **Generic Command System**: User-defined commands versioned with Git - **Docker Ready**: Simple deployment with Docker Compose +## CLI Installation + +Archon CLI lets you run AI workflows directly from your terminal, without needing a server or messaging platform. + +### macOS / Linux (Recommended) + +```bash +# Install with one command +curl -fsSL https://raw.githubusercontent.com/raswonders/remote-coding-agent/main/scripts/install.sh | bash + +# Verify installation +archon version +``` + +Or download directly from [GitHub Releases](https://github.com/raswonders/remote-coding-agent/releases/latest). + +### macOS with Homebrew + +```bash +brew install raswonders/remote-coding-agent/archon +``` + +### Windows (WSL2 Required) + +Windows requires WSL2 for full compatibility. See [Windows Setup](#windows-wsl2-setup) for details. + +```bash +# Inside WSL2 (Ubuntu) +curl -fsSL https://raw.githubusercontent.com/raswonders/remote-coding-agent/main/scripts/install.sh | bash +``` + +### CLI Quick Start + +```bash +# List available workflows +archon workflow list + +# Run a workflow +archon workflow run assist "What does this codebase do?" + +# Run in a specific directory +archon workflow run assist --cwd /path/to/repo "Explain the architecture" +``` + +--- + ## Prerequisites **System Requirements:** -- Docker & Docker Compose (for deployment) +- Docker & Docker Compose (for server deployment) - [Bun](https://bun.sh) 1.0+ (for local development) +**Platform Support:** +- **macOS**: Apple Silicon (M1/M2/M3) and Intel - fully supported +- **Linux**: x64 and ARM64 - fully supported +- **Windows**: Requires WSL2 (Windows Subsystem for Linux 2) - see [Windows Setup](#windows-wsl2-setup) below + **Accounts Required:** - GitHub account (for repository cloning via `/clone` command) - At least one of: Claude Pro/Max subscription OR Codex account -- At least one of: Telegram, Slack, Discord, or GitHub account (for interaction) +- At least one of: Telegram, Slack, Discord, or GitHub account (for server interaction) --- -## Quick Start +## Server Quick Start ### Option 1: Docker (*Not working yet => works when repo goes public*) @@ -1319,3 +1370,61 @@ lsof -i :3090 # Windows: netstat -ano | findstr :3090 ``` + +--- + +## Windows (WSL2 Setup) + +Archon CLI requires WSL2 (Windows Subsystem for Linux 2) on Windows. Native Windows binaries are not currently supported. + +### Why WSL2? + +The Archon CLI relies on Unix-specific features and tools: +- Git worktree operations with symlinks +- Shell scripting for AI agent execution +- File system operations that differ between Windows and Unix + +WSL2 provides a full Linux environment that runs seamlessly on Windows. + +### Quick WSL2 Setup + +1. **Install WSL2** (requires Windows 10 version 2004+ or Windows 11): + ```powershell + wsl --install + ``` + This installs Ubuntu by default. Restart your computer when prompted. + +2. **Set up Ubuntu**: + Open "Ubuntu" from the Start menu and create a username/password. + +3. **Install Bun in WSL2**: + ```bash + curl -fsSL https://bun.sh/install | bash + source ~/.bashrc + ``` + +4. **Install Archon CLI**: + ```bash + curl -fsSL https://raw.githubusercontent.com/raswonders/remote-coding-agent/main/scripts/install.sh | bash + ``` + +5. **Verify installation**: + ```bash + archon version + ``` + +### Working with Windows Files + +WSL2 can access your Windows files at `/mnt/c/` (for C: drive): +```bash +cd /mnt/c/Users/YourName/Projects/my-repo +archon workflow run assist "What does this code do?" +``` + +For best performance, keep projects inside the WSL2 file system (`~/projects/`) rather than `/mnt/c/`. + +### Tips + +- **VS Code Integration**: Install "Remote - WSL" extension to edit WSL2 files from VS Code +- **Terminal**: Windows Terminal provides excellent WSL2 support +- **Git**: Use Git inside WSL2 for consistent behavior with Archon diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 00000000..ab6dd2f4 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,162 @@ +# Releasing Archon CLI + +This guide covers how to create a new release of the Archon CLI. + +## Version Management + +Versions follow [Semantic Versioning](https://semver.org/): +- **Major** (1.0.0): Breaking changes to CLI interface or workflow format +- **Minor** (0.1.0): New features, new workflows, new commands +- **Patch** (0.0.1): Bug fixes, documentation updates + +Version is stored in: +- `package.json` (root) - source of truth +- `packages/cli/package.json` - should match root +- `packages/core/package.json` - should match root + +## Release Process + +### 1. Prepare the Release + +```bash +# Ensure you're on main and up to date +git checkout main +git pull origin main + +# Update version in package.json files +# (update root, cli, and core package.json) +# Commit the version bump +git add -A +git commit -m "chore: bump version to X.Y.Z" +git push origin main +``` + +### 2. Create Release Tag + +```bash +# Create and push the tag +git tag vX.Y.Z +git push origin vX.Y.Z +``` + +This triggers the GitHub Actions release workflow which: +1. Builds binaries for all platforms (macOS arm64/x64, Linux arm64/x64) +2. Generates checksums +3. Creates a GitHub Release with all artifacts + +### 3. Update Homebrew Formula (Optional) + +After the release workflow completes: + +```bash +# Update checksums in the Homebrew formula +./scripts/update-homebrew.sh vX.Y.Z + +# Review and commit +git diff homebrew/archon.rb +git add homebrew/archon.rb +git commit -m "chore: update Homebrew formula for vX.Y.Z" +git push origin main +``` + +If you maintain a Homebrew tap (`homebrew-archon`), copy the updated formula there. + +### 4. Verify the Release + +```bash +# Test the install script +curl -fsSL https://raw.githubusercontent.com/raswonders/remote-coding-agent/main/scripts/install.sh | bash + +# Verify version +archon version +``` + +## Manual Build (for Testing) + +To build binaries locally without creating a release: + +```bash +# Build all platform binaries +bun run build:binaries + +# Binaries are in dist/binaries/ +ls -la dist/binaries/ + +# Generate checksums +bun run build:checksums +``` + +## Release Workflow Details + +The `.github/workflows/release.yml` workflow: + +1. **Triggers on**: + - Push of tags matching `v*` + - Manual workflow dispatch with version input + +2. **Build job** (runs in parallel for each platform): + - Sets up Bun + - Installs dependencies + - Compiles binary with `bun build --compile` + - Uploads as artifact + +3. **Release job** (runs after all builds complete): + - Downloads all artifacts + - Generates SHA256 checksums + - Creates GitHub Release with: + - All binaries attached + - checksums.txt + - Auto-generated release notes + - Installation instructions + +## Troubleshooting + +### Build Fails on GitHub Actions + +Check the Actions tab for specific errors. Common issues: +- Dependency installation failure: Check bun.lockb is committed +- Type errors: Run `bun run type-check` locally first + +### Install Script Fails + +The install script requires: +- `curl` for downloading +- `sha256sum` or `shasum` for verification +- Write access to `/usr/local/bin` (or custom `INSTALL_DIR`) + +### Checksums Don't Match + +If users report checksum failures: +1. Check the release artifacts are complete +2. Verify checksums.txt was generated correctly +3. Ensure binaries weren't modified after checksum generation + +## Pre-release Versions + +For testing releases before public announcement: + +```bash +# Create a pre-release tag +git tag v0.3.0-beta.1 +git push origin v0.3.0-beta.1 +``` + +Pre-releases (tags containing `-`) are marked as such on GitHub. + +## Hotfix Process + +For urgent fixes to a released version: + +```bash +# Create hotfix branch from tag +git checkout -b hotfix/0.2.1 v0.2.0 + +# Make fixes, then tag +git tag v0.2.1 +git push origin v0.2.1 + +# Merge fixes back to main +git checkout main +git merge hotfix/0.2.1 +git push origin main +``` diff --git a/eslint.config.mjs b/eslint.config.mjs index 0ffaa9f6..f8ebdc85 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,6 +17,7 @@ export default tseslint.config( '**/*.js', '*.mjs', '**/*.test.ts', + '*.d.ts', // Root-level declaration files (not in tsconfig project scope) ], }, diff --git a/homebrew/archon.rb b/homebrew/archon.rb new file mode 100644 index 00000000..b6c97c4c --- /dev/null +++ b/homebrew/archon.rb @@ -0,0 +1,54 @@ +# Homebrew formula for Archon CLI +# To install: brew install raswonders/tap/archon +# +# This formula downloads pre-built binaries from GitHub releases. +# For development, see: https://github.com/raswonders/remote-coding-agent + +class Archon < Formula + desc "Remote agentic coding platform - control AI assistants from anywhere" + homepage "https://github.com/raswonders/remote-coding-agent" + version "0.2.0" + license "MIT" + + on_macos do + on_arm do + url "https://github.com/raswonders/remote-coding-agent/releases/download/v#{version}/archon-darwin-arm64" + sha256 "PLACEHOLDER_SHA256_DARWIN_ARM64" + end + on_intel do + url "https://github.com/raswonders/remote-coding-agent/releases/download/v#{version}/archon-darwin-x64" + sha256 "PLACEHOLDER_SHA256_DARWIN_X64" + end + end + + on_linux do + on_arm do + url "https://github.com/raswonders/remote-coding-agent/releases/download/v#{version}/archon-linux-arm64" + sha256 "PLACEHOLDER_SHA256_LINUX_ARM64" + end + on_intel do + url "https://github.com/raswonders/remote-coding-agent/releases/download/v#{version}/archon-linux-x64" + sha256 "PLACEHOLDER_SHA256_LINUX_X64" + end + end + + def install + binary_name = case + when OS.mac? && Hardware::CPU.arm? + "archon-darwin-arm64" + when OS.mac? && Hardware::CPU.intel? + "archon-darwin-x64" + when OS.linux? && Hardware::CPU.arm? + "archon-linux-arm64" + when OS.linux? && Hardware::CPU.intel? + "archon-linux-x64" + end + + bin.install binary_name => "archon" + end + + test do + # Basic version check - archon version should exit with 0 on success + assert_match version.to_s, shell_output("#{bin}/archon version") + end +end diff --git a/package.json b/package.json index aad086cc..dba03322 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "archon", + "version": "0.2.0", "private": true, "workspaces": [ "packages/*" @@ -10,6 +11,8 @@ "dev": "bun --cwd packages/server --watch src/index.ts", "start": "bun --cwd packages/server src/index.ts", "build": "bun --filter '*' build", + "build:binaries": "bash scripts/build-binaries.sh", + "build:checksums": "bash scripts/checksums.sh", "test": "bun --filter '*' test", "test:watch": "bun --filter @archon/server test:watch", "type-check": "bun --filter '*' type-check", diff --git a/packages/cli/src/commands/bundled-version.ts b/packages/cli/src/commands/bundled-version.ts new file mode 100644 index 00000000..ac0b0b49 --- /dev/null +++ b/packages/cli/src/commands/bundled-version.ts @@ -0,0 +1,10 @@ +/** + * Bundled version for compiled binaries + * + * This file is updated by scripts/build-binaries.sh before compilation. + * The version is read from package.json at build time and embedded here. + * + * For development, the version command reads directly from package.json instead. + */ + +export const BUNDLED_VERSION = '0.2.0'; diff --git a/packages/cli/src/commands/version.test.ts b/packages/cli/src/commands/version.test.ts index 4c92abc2..19d1635f 100644 --- a/packages/cli/src/commands/version.test.ts +++ b/packages/cli/src/commands/version.test.ts @@ -1,7 +1,7 @@ /** * Tests for version command */ -import { describe, it, expect, beforeEach, afterEach, spyOn, mock } from 'bun:test'; +import { describe, it, expect, beforeEach, afterEach, spyOn } from 'bun:test'; import { versionCommand } from './version'; describe('versionCommand', () => { @@ -15,27 +15,42 @@ describe('versionCommand', () => { consoleSpy.mockRestore(); }); - it('should output package name and version', async () => { + it('should output version and system info', async () => { await versionCommand(); - // Should have called console.log twice (package version and Bun version) - expect(consoleSpy).toHaveBeenCalledTimes(2); + // Should have called console.log 4 times (version, platform, build, database) + expect(consoleSpy).toHaveBeenCalledTimes(4); - // First call should contain package name and version + // First call should contain "Archon CLI" and version const firstCall = consoleSpy.mock.calls[0][0] as string; - expect(firstCall).toContain('@archon/cli'); + expect(firstCall).toContain('Archon CLI'); expect(firstCall).toMatch(/v\d+\.\d+\.\d+/); - // Second call should contain Bun version + // Second call should contain platform info const secondCall = consoleSpy.mock.calls[1][0] as string; - expect(secondCall).toContain('Bun v'); + expect(secondCall).toContain('Platform:'); + + // Third call should contain build type + const thirdCall = consoleSpy.mock.calls[2][0] as string; + expect(thirdCall).toContain('Build:'); + + // Fourth call should contain database type + const fourthCall = consoleSpy.mock.calls[3][0] as string; + expect(fourthCall).toContain('Database:'); }); - it('should output correct format', async () => { + it('should output correct format for version line', async () => { await versionCommand(); const firstCall = consoleSpy.mock.calls[0][0] as string; - // Format: "@archon/cli v1.0.0" - expect(firstCall).toMatch(/^@archon\/cli v\d+\.\d+\.\d+$/); + // Format: "Archon CLI v0.2.0" + expect(firstCall).toMatch(/^Archon CLI v\d+\.\d+\.\d+$/); + }); + + it('should show source (bun) build type in development', async () => { + await versionCommand(); + + const buildCall = consoleSpy.mock.calls[2][0] as string; + expect(buildCall).toContain('source (bun)'); }); }); diff --git a/packages/cli/src/commands/version.ts b/packages/cli/src/commands/version.ts index 34e51441..1b0752d4 100644 --- a/packages/cli/src/commands/version.ts +++ b/packages/cli/src/commands/version.ts @@ -1,9 +1,15 @@ /** * Version command - displays version info + * + * For compiled binaries, version is embedded via bundled-version.ts + * For development (Bun), reads from package.json */ import { readFile } from 'fs/promises'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; +import { isBinaryBuild } from '@archon/core/defaults/bundled-defaults'; +import { getDatabaseType } from '@archon/core'; +import { BUNDLED_VERSION } from './bundled-version'; const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); @@ -12,8 +18,10 @@ interface PackageJson { version: string; } -export async function versionCommand(): Promise { - // Read package.json from cli package +/** + * Get version for development mode (reads package.json) + */ +async function getDevVersion(): Promise<{ name: string; version: string }> { const pkgPath = join(SCRIPT_DIR, '../../package.json'); let content: string; @@ -36,6 +44,28 @@ export async function versionCommand(): Promise { throw new Error('Failed to read version: package.json is malformed'); } - console.log(`${pkg.name} v${pkg.version}`); - console.log(`Bun v${Bun.version}`); + return { name: pkg.name, version: pkg.version }; +} + +export async function versionCommand(): Promise { + let version: string; + + if (isBinaryBuild()) { + // Compiled binary: use embedded version + version = BUNDLED_VERSION; + } else { + // Development mode: read from package.json + const devInfo = await getDevVersion(); + version = devInfo.version; + } + + const platform = process.platform; + const arch = process.arch; + const dbType = getDatabaseType(); + const buildType = isBinaryBuild() ? 'binary' : 'source (bun)'; + + console.log(`Archon CLI v${version}`); + console.log(` Platform: ${platform}-${arch}`); + console.log(` Build: ${buildType}`); + console.log(` Database: ${dbType}`); } diff --git a/packages/core/package.json b/packages/core/package.json index 4440094d..434204f4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,6 +15,8 @@ "./orchestrator": "./src/orchestrator/orchestrator.ts", "./handlers": "./src/handlers/command-handler.ts", "./config": "./src/config/index.ts", + "./defaults": "./src/defaults/index.ts", + "./defaults/*": "./src/defaults/*.ts", "./utils/*": "./src/utils/*.ts", "./services/*": "./src/services/*.ts", "./state/*": "./src/state/*.ts" diff --git a/packages/core/src/db/connection.test.ts b/packages/core/src/db/connection.test.ts new file mode 100644 index 00000000..3d58a153 --- /dev/null +++ b/packages/core/src/db/connection.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { getDatabaseType, resetDatabase } from './connection'; + +describe('connection', () => { + describe('getDatabaseType', () => { + let originalDatabaseUrl: string | undefined; + + beforeEach(() => { + originalDatabaseUrl = process.env.DATABASE_URL; + // Reset the database singleton to ensure clean state + resetDatabase(); + }); + + afterEach(() => { + // Restore original DATABASE_URL + if (originalDatabaseUrl !== undefined) { + process.env.DATABASE_URL = originalDatabaseUrl; + } else { + delete process.env.DATABASE_URL; + } + resetDatabase(); + }); + + it('should return postgresql when DATABASE_URL is set', () => { + process.env.DATABASE_URL = 'postgresql://localhost:5432/test'; + expect(getDatabaseType()).toBe('postgresql'); + }); + + it('should return sqlite when DATABASE_URL is not set', () => { + delete process.env.DATABASE_URL; + expect(getDatabaseType()).toBe('sqlite'); + }); + + it('should return postgresql for any truthy DATABASE_URL value', () => { + process.env.DATABASE_URL = 'postgres://user:pass@host:5432/db'; + expect(getDatabaseType()).toBe('postgresql'); + + process.env.DATABASE_URL = 'postgresql://localhost/mydb'; + expect(getDatabaseType()).toBe('postgresql'); + }); + + it('should return sqlite when DATABASE_URL is empty string', () => { + process.env.DATABASE_URL = ''; + expect(getDatabaseType()).toBe('sqlite'); + }); + + it('should not initialize database connection', () => { + // getDatabaseType should work without connecting to database + // This is important for version command that runs without db + delete process.env.DATABASE_URL; + + // Should not throw even without a database available + const result = getDatabaseType(); + expect(result).toBe('sqlite'); + }); + }); +}); diff --git a/packages/core/src/db/connection.ts b/packages/core/src/db/connection.ts index 4c47e215..67edefa0 100644 --- a/packages/core/src/db/connection.ts +++ b/packages/core/src/db/connection.ts @@ -57,6 +57,14 @@ export function getDialect(): SqlDialect { return dialect; } +/** + * Get the current database type without initializing the database + * Useful for version/info commands that don't need a connection + */ +export function getDatabaseType(): 'postgresql' | 'sqlite' { + return process.env.DATABASE_URL ? 'postgresql' : 'sqlite'; +} + /** * Close the database connection */ diff --git a/packages/core/src/defaults/bundled-defaults.test.ts b/packages/core/src/defaults/bundled-defaults.test.ts new file mode 100644 index 00000000..60573865 --- /dev/null +++ b/packages/core/src/defaults/bundled-defaults.test.ts @@ -0,0 +1,164 @@ +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { isBinaryBuild, BUNDLED_COMMANDS, BUNDLED_WORKFLOWS } from './bundled-defaults'; + +describe('bundled-defaults', () => { + describe('isBinaryBuild', () => { + let originalExecPath: string; + + beforeEach(() => { + originalExecPath = process.execPath; + }); + + afterEach(() => { + // Restore original execPath (note: this is read-only in practice, but tests may mock it) + Object.defineProperty(process, 'execPath', { value: originalExecPath, writable: true }); + }); + + it('should return false when running with Bun', () => { + // In test environment, we're running with Bun + expect(process.execPath.toLowerCase()).toContain('bun'); + expect(isBinaryBuild()).toBe(false); + }); + + it('should detect bun in path case-insensitively', () => { + // The function uses toLowerCase() so it should handle mixed case + const result = isBinaryBuild(); + // Since we're running in Bun, should be false + expect(result).toBe(false); + }); + + it('should return true for non-bun executable paths', () => { + // Mock a binary executable path + Object.defineProperty(process, 'execPath', { + value: '/usr/local/bin/archon', + writable: true, + }); + + expect(isBinaryBuild()).toBe(true); + }); + + it('should return true for Windows-style binary paths', () => { + Object.defineProperty(process, 'execPath', { + value: 'C:\\Program Files\\archon\\archon.exe', + writable: true, + }); + + expect(isBinaryBuild()).toBe(true); + }); + + it('should return false for Bun paths on different platforms', () => { + // macOS Homebrew + Object.defineProperty(process, 'execPath', { + value: '/opt/homebrew/bin/bun', + writable: true, + }); + expect(isBinaryBuild()).toBe(false); + + // Linux + Object.defineProperty(process, 'execPath', { + value: '/home/user/.bun/bin/bun', + writable: true, + }); + expect(isBinaryBuild()).toBe(false); + + // Windows + Object.defineProperty(process, 'execPath', { + value: 'C:\\Users\\user\\.bun\\bin\\bun.exe', + writable: true, + }); + expect(isBinaryBuild()).toBe(false); + }); + }); + + describe('BUNDLED_COMMANDS', () => { + it('should have all expected default commands', () => { + const expectedCommands = [ + 'archon-assist', + 'archon-code-review-agent', + 'archon-comment-quality-agent', + 'archon-create-pr', + 'archon-docs-impact-agent', + 'archon-error-handling-agent', + 'archon-implement-issue', + 'archon-implement-review-fixes', + 'archon-implement', + 'archon-investigate-issue', + 'archon-pr-review-scope', + 'archon-ralph-prd', + 'archon-resolve-merge-conflicts', + 'archon-sync-pr-with-main', + 'archon-synthesize-review', + 'archon-test-coverage-agent', + ]; + + for (const cmd of expectedCommands) { + expect(BUNDLED_COMMANDS).toHaveProperty(cmd); + } + + expect(Object.keys(BUNDLED_COMMANDS)).toHaveLength(16); + }); + + it('should have non-empty content for all commands', () => { + for (const [name, content] of Object.entries(BUNDLED_COMMANDS)) { + expect(content).toBeDefined(); + expect(typeof content).toBe('string'); + expect(content.length).toBeGreaterThan(0); + // Commands should have meaningful content (at least some markdown) + expect(content.length).toBeGreaterThan(50); + } + }); + + it('should have markdown content format', () => { + // Commands are markdown files, should have typical markdown patterns + for (const [name, content] of Object.entries(BUNDLED_COMMANDS)) { + // Should contain some text (not just whitespace) + expect(content.trim().length).toBeGreaterThan(0); + } + }); + }); + + describe('BUNDLED_WORKFLOWS', () => { + it('should have all expected default workflows', () => { + const expectedWorkflows = [ + 'archon-assist', + 'archon-comprehensive-pr-review', + 'archon-feature-development', + 'archon-fix-github-issue', + 'archon-ralph-fresh', + 'archon-ralph-stateful', + 'archon-resolve-conflicts', + 'archon-test-loop', + ]; + + for (const wf of expectedWorkflows) { + expect(BUNDLED_WORKFLOWS).toHaveProperty(wf); + } + + expect(Object.keys(BUNDLED_WORKFLOWS)).toHaveLength(8); + }); + + it('should have non-empty content for all workflows', () => { + for (const [name, content] of Object.entries(BUNDLED_WORKFLOWS)) { + expect(content).toBeDefined(); + expect(typeof content).toBe('string'); + expect(content.length).toBeGreaterThan(0); + // Workflows should have meaningful YAML content + expect(content.length).toBeGreaterThan(50); + } + }); + + it('should have valid YAML structure', () => { + // Workflows are YAML files, should parse without error + for (const [name, content] of Object.entries(BUNDLED_WORKFLOWS)) { + // Should contain 'name:' as all workflows require a name field + expect(content).toContain('name:'); + // Should contain 'description:' as all workflows require description + expect(content).toContain('description:'); + // Should contain either 'steps:' or 'loop:' as these are the two workflow types + const hasSteps = content.includes('steps:'); + const hasLoop = content.includes('loop:'); + expect(hasSteps || hasLoop).toBe(true); + } + }); + }); +}); diff --git a/packages/core/src/defaults/bundled-defaults.ts b/packages/core/src/defaults/bundled-defaults.ts new file mode 100644 index 00000000..b4554a6f --- /dev/null +++ b/packages/core/src/defaults/bundled-defaults.ts @@ -0,0 +1,91 @@ +/** + * Bundled default commands and workflows for binary distribution + * + * These static imports are resolved at compile time and embedded into the binary. + * When running as a standalone binary (without Bun), these provide the default + * commands and workflows without needing filesystem access to the source repo. + * + * Import syntax uses `with { type: 'text' }` to import file contents as strings. + */ + +// ============================================================================= +// Default Commands (16 total) +// ============================================================================= + +import archonAssistCmd from '../../../../.archon/commands/defaults/archon-assist.md' with { type: 'text' }; +import archonCodeReviewAgentCmd from '../../../../.archon/commands/defaults/archon-code-review-agent.md' with { type: 'text' }; +import archonCommentQualityAgentCmd from '../../../../.archon/commands/defaults/archon-comment-quality-agent.md' with { type: 'text' }; +import archonCreatePrCmd from '../../../../.archon/commands/defaults/archon-create-pr.md' with { type: 'text' }; +import archonDocsImpactAgentCmd from '../../../../.archon/commands/defaults/archon-docs-impact-agent.md' with { type: 'text' }; +import archonErrorHandlingAgentCmd from '../../../../.archon/commands/defaults/archon-error-handling-agent.md' with { type: 'text' }; +import archonImplementIssueCmd from '../../../../.archon/commands/defaults/archon-implement-issue.md' with { type: 'text' }; +import archonImplementReviewFixesCmd from '../../../../.archon/commands/defaults/archon-implement-review-fixes.md' with { type: 'text' }; +import archonImplementCmd from '../../../../.archon/commands/defaults/archon-implement.md' with { type: 'text' }; +import archonInvestigateIssueCmd from '../../../../.archon/commands/defaults/archon-investigate-issue.md' with { type: 'text' }; +import archonPrReviewScopeCmd from '../../../../.archon/commands/defaults/archon-pr-review-scope.md' with { type: 'text' }; +import archonRalphPrdCmd from '../../../../.archon/commands/defaults/archon-ralph-prd.md' with { type: 'text' }; +import archonResolveMergeConflictsCmd from '../../../../.archon/commands/defaults/archon-resolve-merge-conflicts.md' with { type: 'text' }; +import archonSyncPrWithMainCmd from '../../../../.archon/commands/defaults/archon-sync-pr-with-main.md' with { type: 'text' }; +import archonSynthesizeReviewCmd from '../../../../.archon/commands/defaults/archon-synthesize-review.md' with { type: 'text' }; +import archonTestCoverageAgentCmd from '../../../../.archon/commands/defaults/archon-test-coverage-agent.md' with { type: 'text' }; + +// ============================================================================= +// Default Workflows (8 total) +// ============================================================================= + +import archonAssistWf from '../../../../.archon/workflows/defaults/archon-assist.yaml' with { type: 'text' }; +import archonComprehensivePrReviewWf from '../../../../.archon/workflows/defaults/archon-comprehensive-pr-review.yaml' with { type: 'text' }; +import archonFeatureDevelopmentWf from '../../../../.archon/workflows/defaults/archon-feature-development.yaml' with { type: 'text' }; +import archonFixGithubIssueWf from '../../../../.archon/workflows/defaults/archon-fix-github-issue.yaml' with { type: 'text' }; +import archonRalphFreshWf from '../../../../.archon/workflows/defaults/archon-ralph-fresh.yaml' with { type: 'text' }; +import archonRalphStatefulWf from '../../../../.archon/workflows/defaults/archon-ralph-stateful.yaml' with { type: 'text' }; +import archonResolveConflictsWf from '../../../../.archon/workflows/defaults/archon-resolve-conflicts.yaml' with { type: 'text' }; +import archonTestLoopWf from '../../../../.archon/workflows/defaults/archon-test-loop.yaml' with { type: 'text' }; + +// ============================================================================= +// Exports +// ============================================================================= + +/** + * Bundled default commands - filename (without extension) -> content + */ +export const BUNDLED_COMMANDS: Record = { + 'archon-assist': archonAssistCmd, + 'archon-code-review-agent': archonCodeReviewAgentCmd, + 'archon-comment-quality-agent': archonCommentQualityAgentCmd, + 'archon-create-pr': archonCreatePrCmd, + 'archon-docs-impact-agent': archonDocsImpactAgentCmd, + 'archon-error-handling-agent': archonErrorHandlingAgentCmd, + 'archon-implement-issue': archonImplementIssueCmd, + 'archon-implement-review-fixes': archonImplementReviewFixesCmd, + 'archon-implement': archonImplementCmd, + 'archon-investigate-issue': archonInvestigateIssueCmd, + 'archon-pr-review-scope': archonPrReviewScopeCmd, + 'archon-ralph-prd': archonRalphPrdCmd, + 'archon-resolve-merge-conflicts': archonResolveMergeConflictsCmd, + 'archon-sync-pr-with-main': archonSyncPrWithMainCmd, + 'archon-synthesize-review': archonSynthesizeReviewCmd, + 'archon-test-coverage-agent': archonTestCoverageAgentCmd, +}; + +/** + * Bundled default workflows - filename (without extension) -> content + */ +export const BUNDLED_WORKFLOWS: Record = { + 'archon-assist': archonAssistWf, + 'archon-comprehensive-pr-review': archonComprehensivePrReviewWf, + 'archon-feature-development': archonFeatureDevelopmentWf, + 'archon-fix-github-issue': archonFixGithubIssueWf, + 'archon-ralph-fresh': archonRalphFreshWf, + 'archon-ralph-stateful': archonRalphStatefulWf, + 'archon-resolve-conflicts': archonResolveConflictsWf, + 'archon-test-loop': archonTestLoopWf, +}; + +/** + * Check if the current process is running as a compiled binary (not via Bun) + * When running as a binary, process.execPath won't contain 'bun' + */ +export function isBinaryBuild(): boolean { + return !process.execPath.toLowerCase().includes('bun'); +} diff --git a/packages/core/src/defaults/index.ts b/packages/core/src/defaults/index.ts new file mode 100644 index 00000000..4c9478c6 --- /dev/null +++ b/packages/core/src/defaults/index.ts @@ -0,0 +1,8 @@ +/** + * Defaults module - provides access to bundled default commands and workflows + * + * This module is the bridge between the bundled defaults (embedded in binary) + * and the runtime loaders that need to access them. + */ + +export { BUNDLED_COMMANDS, BUNDLED_WORKFLOWS, isBinaryBuild } from './bundled-defaults'; diff --git a/packages/core/src/defaults/text-imports.d.ts b/packages/core/src/defaults/text-imports.d.ts new file mode 100644 index 00000000..0f9ecca2 --- /dev/null +++ b/packages/core/src/defaults/text-imports.d.ts @@ -0,0 +1,28 @@ +/** + * Type declarations for text file imports using Bun's import attributes + * + * These declarations allow TypeScript to understand imports like: + * import content from './file.md' with { type: 'text' }; + * + * Bun handles the actual import at compile/runtime. + * + * Using wildcard patterns to match all .md and .yaml files. + */ + +// Match all .md files (Markdown) +declare module '*.md' { + const content: string; + export default content; +} + +// Match all .yaml files (YAML) +declare module '*.yaml' { + const content: string; + export default content; +} + +// Match all .yml files (YAML alternative extension) +declare module '*.yml' { + const content: string; + export default content; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3e27afde..dcc927ae 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -30,7 +30,14 @@ export { // ============================================================================= // Database // ============================================================================= -export { pool, getDatabase, getDialect, closeDatabase, resetDatabase } from './db/connection'; +export { + pool, + getDatabase, + getDialect, + getDatabaseType, + closeDatabase, + resetDatabase, +} from './db/connection'; export type { IDatabase, SqlDialect } from './db/adapters/types'; // Namespaced db modules for explicit access diff --git a/packages/core/src/workflows/executor.test.ts b/packages/core/src/workflows/executor.test.ts index 964baac3..1b72f304 100644 --- a/packages/core/src/workflows/executor.test.ts +++ b/packages/core/src/workflows/executor.test.ts @@ -75,6 +75,7 @@ function createMockPlatform(): IPlatformAdapter { import { executeWorkflow, isValidCommandName } from './executor'; import * as gitUtils from '../utils/git'; import * as configLoader from '../config/config-loader'; +import * as bundledDefaults from '../defaults/bundled-defaults'; describe('Workflow Executor', () => { let mockPlatform: IPlatformAdapter; @@ -3481,4 +3482,139 @@ describe('app defaults command loading', () => { ); expect(failureMessages.length).toBeGreaterThan(0); }); + + describe('binary build bundled commands', () => { + let isBinaryBuildSpy: Mock; + let loadConfigSpy: Mock; + + beforeEach(() => { + isBinaryBuildSpy = spyOn(bundledDefaults, 'isBinaryBuild'); + loadConfigSpy = spyOn(configLoader, 'loadConfig'); + }); + + afterEach(() => { + isBinaryBuildSpy.mockRestore(); + loadConfigSpy.mockRestore(); + }); + + it('should load command from bundled defaults when running as binary', async () => { + // Simulate binary build + isBinaryBuildSpy.mockReturnValue(true); + + // Enable default command loading + loadConfigSpy.mockResolvedValue({ + botName: 'Archon', + assistant: 'claude', + streaming: { telegram: 'stream', discord: 'batch', slack: 'batch', github: 'batch' }, + paths: { workspaces: '/tmp', worktrees: '/tmp' }, + concurrency: { maxConversations: 10 }, + commands: { autoLoad: true }, + defaults: { copyDefaults: true, loadDefaultCommands: true, loadDefaultWorkflows: true }, + }); + + // Use a known bundled command name + const workflow: WorkflowDefinition = { + name: 'bundled-cmd-test', + description: 'Test bundled command loading', + steps: [{ command: 'archon-assist' }], + }; + + await executeWorkflow( + mockPlatform, + 'conv-123', + testDir, + workflow, + 'User message', + 'db-conv-id' + ); + + // Should have called AI with the bundled command content (not fail with not found) + const sendMessageCalls = (mockPlatform.sendMessage as ReturnType).mock.calls; + const notFoundMessages = sendMessageCalls.filter( + (call: unknown[]) => + typeof call[1] === 'string' && (call[1] as string).includes('Command prompt not found') + ); + // Should NOT have not found error when using bundled command + expect(notFoundMessages.length).toBe(0); + }); + + it('should fallback to not found when bundled command does not exist', async () => { + // Simulate binary build + isBinaryBuildSpy.mockReturnValue(true); + + // Enable default command loading + loadConfigSpy.mockResolvedValue({ + botName: 'Archon', + assistant: 'claude', + streaming: { telegram: 'stream', discord: 'batch', slack: 'batch', github: 'batch' }, + paths: { workspaces: '/tmp', worktrees: '/tmp' }, + concurrency: { maxConversations: 10 }, + commands: { autoLoad: true }, + defaults: { copyDefaults: true, loadDefaultCommands: true, loadDefaultWorkflows: true }, + }); + + const workflow: WorkflowDefinition = { + name: 'nonexistent-bundled-test', + description: 'Test nonexistent bundled command', + steps: [{ command: 'nonexistent-command-xyz' }], + }; + + await executeWorkflow( + mockPlatform, + 'conv-123', + testDir, + workflow, + 'User message', + 'db-conv-id' + ); + + // Should fail with not found error + const sendMessageCalls = (mockPlatform.sendMessage as ReturnType).mock.calls; + const notFoundMessages = sendMessageCalls.filter( + (call: unknown[]) => + typeof call[1] === 'string' && (call[1] as string).includes('Command prompt not found') + ); + expect(notFoundMessages.length).toBeGreaterThan(0); + }); + + it('should skip bundled commands when loadDefaultCommands is false', async () => { + // Simulate binary build + isBinaryBuildSpy.mockReturnValue(true); + + // Disable default command loading + loadConfigSpy.mockResolvedValue({ + botName: 'Archon', + assistant: 'claude', + streaming: { telegram: 'stream', discord: 'batch', slack: 'batch', github: 'batch' }, + paths: { workspaces: '/tmp', worktrees: '/tmp' }, + concurrency: { maxConversations: 10 }, + commands: { autoLoad: true }, + defaults: { copyDefaults: true, loadDefaultCommands: false, loadDefaultWorkflows: true }, + }); + + // Use a known bundled command name, but defaults are disabled + const workflow: WorkflowDefinition = { + name: 'disabled-defaults-test', + description: 'Test with disabled defaults', + steps: [{ command: 'archon-assist' }], + }; + + await executeWorkflow( + mockPlatform, + 'conv-123', + testDir, + workflow, + 'User message', + 'db-conv-id' + ); + + // Should fail with not found because defaults are disabled + const sendMessageCalls = (mockPlatform.sendMessage as ReturnType).mock.calls; + const notFoundMessages = sendMessageCalls.filter( + (call: unknown[]) => + typeof call[1] === 'string' && (call[1] as string).includes('Command prompt not found') + ); + expect(notFoundMessages.length).toBeGreaterThan(0); + }); + }); }); diff --git a/packages/core/src/workflows/executor.ts b/packages/core/src/workflows/executor.ts index 61df85fc..e33e8450 100644 --- a/packages/core/src/workflows/executor.ts +++ b/packages/core/src/workflows/executor.ts @@ -9,6 +9,7 @@ import * as workflowDb from '../db/workflows'; import { formatToolCall } from '../utils/tool-formatter'; import * as archonPaths from '../utils/archon-paths'; import * as configLoader from '../config/config-loader'; +import { BUNDLED_COMMANDS, isBinaryBuild } from '../defaults/bundled-defaults'; import { commitAllChanges } from '../utils/git'; import type { WorkflowDefinition, @@ -336,29 +337,40 @@ async function loadCommandPrompt( // If not found in repo and app defaults enabled, search app defaults const loadDefaultCommands = config.defaults?.loadDefaultCommands ?? true; if (loadDefaultCommands) { - const appDefaultsPath = archonPaths.getDefaultCommandsPath(); - const filePath = join(appDefaultsPath, `${commandName}.md`); - try { - await access(filePath); - const content = await readFile(filePath, 'utf-8'); - if (!content.trim()) { - console.error(`[WorkflowExecutor] Empty app default command file: ${commandName}.md`); - return { - success: false, - reason: 'empty_file', - message: `App default command file is empty: ${commandName}.md`, - }; + if (isBinaryBuild()) { + // Binary: check bundled commands + const bundledContent = BUNDLED_COMMANDS[commandName]; + if (bundledContent) { + console.log(`[WorkflowExecutor] Loaded command from bundled defaults: ${commandName}.md`); + return { success: true, content: bundledContent }; } - console.log(`[WorkflowExecutor] Loaded command from app defaults: ${commandName}.md`); - return { success: true, content }; - } catch (error) { - const err = error as NodeJS.ErrnoException; - if (err.code !== 'ENOENT') { - console.warn(`[WorkflowExecutor] Error reading app default: ${err.message}`); - } else { - console.log(`[WorkflowExecutor] App default command not found: ${commandName}.md`); + console.log(`[WorkflowExecutor] Bundled default command not found: ${commandName}.md`); + } else { + // Bun: load from filesystem + const appDefaultsPath = archonPaths.getDefaultCommandsPath(); + const filePath = join(appDefaultsPath, `${commandName}.md`); + try { + await access(filePath); + const content = await readFile(filePath, 'utf-8'); + if (!content.trim()) { + console.error(`[WorkflowExecutor] Empty app default command file: ${commandName}.md`); + return { + success: false, + reason: 'empty_file', + message: `App default command file is empty: ${commandName}.md`, + }; + } + console.log(`[WorkflowExecutor] Loaded command from app defaults: ${commandName}.md`); + return { success: true, content }; + } catch (error) { + const err = error as NodeJS.ErrnoException; + if (err.code !== 'ENOENT') { + console.warn(`[WorkflowExecutor] Error reading app default: ${err.message}`); + } else { + console.log(`[WorkflowExecutor] App default command not found: ${commandName}.md`); + } + // Fall through to not found } - // Fall through to not found } } diff --git a/packages/core/src/workflows/loader.test.ts b/packages/core/src/workflows/loader.test.ts index 2f4a8f7b..9c516cfb 100644 --- a/packages/core/src/workflows/loader.test.ts +++ b/packages/core/src/workflows/loader.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'os'; import { discoverWorkflows } from './loader'; import { isParallelBlock } from './types'; import * as configLoader from '../config/config-loader'; +import * as bundledDefaults from '../defaults/bundled-defaults'; describe('Workflow Loader', () => { let testDir: string; @@ -983,4 +984,131 @@ steps: expect(customWorkflow).toBeDefined(); }); }); + + describe('binary build bundled workflows', () => { + let isBinaryBuildSpy: Mock; + + beforeEach(() => { + isBinaryBuildSpy = spyOn(bundledDefaults, 'isBinaryBuild'); + }); + + afterEach(() => { + isBinaryBuildSpy.mockRestore(); + }); + + it('should load bundled workflows when running as binary', async () => { + // Simulate binary build + isBinaryBuildSpy.mockReturnValue(true); + + // Enable default workflow loading + loadConfigSpy.mockResolvedValue({ + botName: 'Archon', + assistant: 'claude', + streaming: { telegram: 'stream', discord: 'batch', slack: 'batch', github: 'batch' }, + paths: { workspaces: '/tmp', worktrees: '/tmp' }, + concurrency: { maxConversations: 10 }, + commands: { autoLoad: true }, + defaults: { copyDefaults: true, loadDefaultCommands: true, loadDefaultWorkflows: true }, + }); + + const workflows = await discoverWorkflows(testDir); + + // Should load bundled workflows + expect(workflows.length).toBeGreaterThanOrEqual(1); + // Check that known bundled workflows are loaded + const archonAssist = workflows.find(w => w.name === 'archon-assist'); + expect(archonAssist).toBeDefined(); + }); + + it('should skip bundled workflows when loadDefaultWorkflows is false', async () => { + // Simulate binary build + isBinaryBuildSpy.mockReturnValue(true); + + // Disable default workflow loading + loadConfigSpy.mockResolvedValue({ + botName: 'Archon', + assistant: 'claude', + streaming: { telegram: 'stream', discord: 'batch', slack: 'batch', github: 'batch' }, + paths: { workspaces: '/tmp', worktrees: '/tmp' }, + concurrency: { maxConversations: 10 }, + commands: { autoLoad: true }, + defaults: { copyDefaults: true, loadDefaultCommands: true, loadDefaultWorkflows: false }, + }); + + const workflows = await discoverWorkflows(testDir); + + // Should not have any bundled defaults + const archonWorkflow = workflows.find(w => w.name.startsWith('archon-')); + expect(archonWorkflow).toBeUndefined(); + }); + + it('should allow repo workflows to override bundled defaults', async () => { + // Simulate binary build + isBinaryBuildSpy.mockReturnValue(true); + + // Enable default workflow loading + loadConfigSpy.mockResolvedValue({ + botName: 'Archon', + assistant: 'claude', + streaming: { telegram: 'stream', discord: 'batch', slack: 'batch', github: 'batch' }, + paths: { workspaces: '/tmp', worktrees: '/tmp' }, + concurrency: { maxConversations: 10 }, + commands: { autoLoad: true }, + defaults: { copyDefaults: true, loadDefaultCommands: true, loadDefaultWorkflows: true }, + }); + + // Create repo workflow with same filename as bundled default + const repoWorkflowDir = join(testDir, '.archon', 'workflows'); + await mkdir(repoWorkflowDir, { recursive: true }); + const repoWorkflowYaml = `name: custom-assist-override +description: Custom override of archon-assist +steps: + - command: custom +`; + await writeFile(join(repoWorkflowDir, 'archon-assist.yaml'), repoWorkflowYaml); + + const workflows = await discoverWorkflows(testDir); + + // Repo workflow should override bundled default + const assistWorkflow = workflows.find( + w => w.name === 'custom-assist-override' || w.name === 'archon-assist' + ); + expect(assistWorkflow).toBeDefined(); + expect(assistWorkflow?.name).toBe('custom-assist-override'); + }); + + it('should combine bundled workflows with repo workflows', async () => { + // Simulate binary build + isBinaryBuildSpy.mockReturnValue(true); + + // Enable default workflow loading + loadConfigSpy.mockResolvedValue({ + botName: 'Archon', + assistant: 'claude', + streaming: { telegram: 'stream', discord: 'batch', slack: 'batch', github: 'batch' }, + paths: { workspaces: '/tmp', worktrees: '/tmp' }, + concurrency: { maxConversations: 10 }, + commands: { autoLoad: true }, + defaults: { copyDefaults: true, loadDefaultCommands: true, loadDefaultWorkflows: true }, + }); + + // Create repo workflow with unique name + const repoWorkflowDir = join(testDir, '.archon', 'workflows'); + await mkdir(repoWorkflowDir, { recursive: true }); + const repoWorkflowYaml = `name: my-repo-workflow +description: A repo-specific workflow +steps: + - command: custom +`; + await writeFile(join(repoWorkflowDir, 'my-repo.yaml'), repoWorkflowYaml); + + const workflows = await discoverWorkflows(testDir); + + // Should have both bundled and repo workflows + const archonAssist = workflows.find(w => w.name === 'archon-assist'); + const repoWorkflow = workflows.find(w => w.name === 'my-repo-workflow'); + expect(archonAssist).toBeDefined(); + expect(repoWorkflow).toBeDefined(); + }); + }); }); diff --git a/packages/core/src/workflows/loader.ts b/packages/core/src/workflows/loader.ts index d92cfc37..45b177ea 100644 --- a/packages/core/src/workflows/loader.ts +++ b/packages/core/src/workflows/loader.ts @@ -7,6 +7,7 @@ import type { WorkflowDefinition, LoopConfig, SingleStep, WorkflowStep } from '. import * as archonPaths from '../utils/archon-paths'; import * as configLoader from '../config/config-loader'; import { isValidCommandName } from './executor'; +import { BUNDLED_WORKFLOWS, isBinaryBuild } from '../defaults/bundled-defaults'; /** * Parse YAML using Bun's native YAML parser @@ -242,10 +243,41 @@ async function loadWorkflowsFromDir(dirPath: string): Promise workflow for consistency with loadWorkflowsFromDir + * + * Note: Bundled workflows are embedded at compile time and should ALWAYS be valid. + * Parse failures indicate a build-time corruption and are logged as errors. + */ +function loadBundledWorkflows(): Map { + const workflows = new Map(); + + for (const [name, content] of Object.entries(BUNDLED_WORKFLOWS)) { + const filename = `${name}.yaml`; + const workflow = parseWorkflow(content, filename); + if (workflow) { + workflows.set(filename, workflow); + console.log(`[WorkflowLoader] Loaded bundled workflow: ${workflow.name}`); + } else { + // Bundled workflows should ALWAYS be valid - this indicates a build-time error + console.error(`[WorkflowLoader] CRITICAL: Bundled workflow failed to parse: ${filename}`); + console.error('[WorkflowLoader] This indicates build-time corruption or invalid YAML.'); + console.error(`[WorkflowLoader] Content preview: ${content.slice(0, 200)}...`); + } + } + + return workflows; +} + /** * Discover and load workflows from codebase * Loads from both app's bundled defaults and repo's workflow folder. * Repo workflows override app defaults by exact filename match. + * + * When running as a compiled binary, defaults are loaded from the bundled + * content embedded at compile time. When running with Bun, defaults are + * loaded from the filesystem. */ export async function discoverWorkflows(cwd: string): Promise { // Map of filename -> workflow for deduplication @@ -269,21 +301,34 @@ export async function discoverWorkflows(cwd: string): Promise "$BUNDLED_VERSION_FILE" << EOF +/** + * Bundled version for compiled binaries + * + * This file is updated by scripts/build-binaries.sh before compilation. + * The version is read from package.json at build time and embedded here. + * + * For development, the version command reads directly from package.json instead. + */ + +export const BUNDLED_VERSION = '${VERSION}'; +EOF + +# Output directory +DIST_DIR="dist/binaries" +mkdir -p "$DIST_DIR" + +# Define build targets +# Format: bun-target:output-name +TARGETS=( + "bun-darwin-arm64:archon-darwin-arm64" + "bun-darwin-x64:archon-darwin-x64" + "bun-linux-x64:archon-linux-x64" + "bun-linux-arm64:archon-linux-arm64" +) + +# Minimum expected binary size (1MB - Bun binaries are typically 50MB+) +MIN_BINARY_SIZE=1000000 + +# Build each target +for target_pair in "${TARGETS[@]}"; do + IFS=':' read -r target output_name <<< "$target_pair" + echo "Building for $target..." + + bun build \ + --compile \ + --target="$target" \ + --outfile="$DIST_DIR/$output_name" \ + packages/cli/src/cli.ts + + # Verify build output exists + if [ ! -f "$DIST_DIR/$output_name" ]; then + echo "ERROR: Build failed - $DIST_DIR/$output_name not created" + exit 1 + fi + + # Verify minimum reasonable size (Bun binaries are typically 50MB+) + # Use portable stat command (works on both macOS and Linux) + if stat -f%z "$DIST_DIR/$output_name" >/dev/null 2>&1; then + size=$(stat -f%z "$DIST_DIR/$output_name") + else + size=$(stat --printf="%s" "$DIST_DIR/$output_name") + fi + + if [ "$size" -lt "$MIN_BINARY_SIZE" ]; then + echo "ERROR: Build output suspiciously small ($size bytes): $DIST_DIR/$output_name" + echo "Expected at least $MIN_BINARY_SIZE bytes for a Bun-compiled binary" + exit 1 + fi + + echo " -> $DIST_DIR/$output_name ($size bytes)" +done + +echo "" +echo "Build complete! Binaries in $DIST_DIR:" +ls -lh "$DIST_DIR" diff --git a/scripts/checksums.sh b/scripts/checksums.sh new file mode 100755 index 00000000..b002a13c --- /dev/null +++ b/scripts/checksums.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# scripts/checksums.sh +# Generate SHA256 checksums for release binaries + +set -euo pipefail + +DIST_DIR="${1:-dist/binaries}" +CHECKSUM_FILE="$DIST_DIR/checksums.txt" + +# Expected binaries +EXPECTED_BINARIES=( + "archon-darwin-arm64" + "archon-darwin-x64" + "archon-linux-arm64" + "archon-linux-x64" +) + +echo "Generating checksums for binaries in $DIST_DIR" + +cd "$DIST_DIR" + +# Verify at least one binary exists +if ! ls archon-* 1>/dev/null 2>&1; then + echo "ERROR: No archon-* binaries found in $DIST_DIR" + echo "Expected files: ${EXPECTED_BINARIES[*]}" + exit 1 +fi + +# Verify all expected binaries exist +missing=() +for binary in "${EXPECTED_BINARIES[@]}"; do + if [ ! -f "$binary" ]; then + missing+=("$binary") + fi +done + +if [ ${#missing[@]} -gt 0 ]; then + echo "ERROR: Missing expected binaries: ${missing[*]}" + echo "Found binaries:" + ls -la archon-* 2>/dev/null || echo " (none)" + exit 1 +fi + +# Generate checksums +shasum -a 256 archon-* > checksums.txt + +echo "Checksums written to $CHECKSUM_FILE:" +cat checksums.txt diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 00000000..0212c9cc --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,254 @@ +#!/usr/bin/env bash +# scripts/install.sh +# Install Archon CLI from GitHub releases +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/raswonders/remote-coding-agent/main/scripts/install.sh | bash +# +# Options (via environment variables): +# VERSION - Specific version to install (default: latest) +# INSTALL_DIR - Installation directory (default: /usr/local/bin) +# SKIP_CHECKSUM - Set to "true" to skip checksum verification (not recommended) +# +# Examples: +# # Install latest +# curl -fsSL https://raw.githubusercontent.com/raswonders/remote-coding-agent/main/scripts/install.sh | bash +# +# # Install specific version +# VERSION=v0.2.0 curl -fsSL ... | bash +# +# # Install to custom directory +# INSTALL_DIR=~/.local/bin curl -fsSL ... | bash + +set -euo pipefail + +# Configuration +REPO="raswonders/remote-coding-agent" +BINARY_NAME="archon" +VERSION="${VERSION:-latest}" +INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +info() { echo -e "${BLUE}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } +success() { echo -e "${GREEN}[OK]${NC} $1"; } + +# Detect OS and architecture +detect_platform() { + local os arch + + os="$(uname -s | tr '[:upper:]' '[:lower:]')" + arch="$(uname -m)" + + case "$os" in + darwin) + os="darwin" + ;; + linux) + os="linux" + ;; + mingw*|msys*|cygwin*) + error "Windows is not supported. Please use WSL2 or see documentation." + exit 1 + ;; + *) + error "Unsupported OS: $os" + exit 1 + ;; + esac + + case "$arch" in + x86_64|amd64) + arch="x64" + ;; + arm64|aarch64) + arch="arm64" + ;; + *) + error "Unsupported architecture: $arch" + exit 1 + ;; + esac + + echo "${os}-${arch}" +} + +# Get download URL for the binary +get_download_url() { + local platform="$1" + local version="$2" + + if [ "$version" = "latest" ]; then + echo "https://github.com/${REPO}/releases/latest/download/${BINARY_NAME}-${platform}" + else + echo "https://github.com/${REPO}/releases/download/${version}/${BINARY_NAME}-${platform}" + fi +} + +# Get checksums URL +get_checksums_url() { + local version="$1" + + if [ "$version" = "latest" ]; then + echo "https://github.com/${REPO}/releases/latest/download/checksums.txt" + else + echo "https://github.com/${REPO}/releases/download/${version}/checksums.txt" + fi +} + +# Verify checksum +verify_checksum() { + local binary_path="$1" + local platform="$2" + local checksums_url="$3" + + # Allow explicit skip with clear warning + if [ "${SKIP_CHECKSUM:-false}" = "true" ]; then + warn "Checksum verification SKIPPED by user request (SKIP_CHECKSUM=true)" + warn "This binary has NOT been verified - use at your own risk" + return 0 + fi + + info "Verifying checksum..." + + local checksums + if ! checksums=$(curl -fsSL "$checksums_url" 2>/dev/null); then + error "Could not download checksums file from $checksums_url" + error "Cannot verify binary integrity." + error "To install anyway (not recommended): SKIP_CHECKSUM=true curl -fsSL ... | bash" + exit 1 + fi + + local expected_hash + expected_hash=$(echo "$checksums" | grep "${BINARY_NAME}-${platform}" | awk '{print $1}') + + if [ -z "$expected_hash" ]; then + error "Could not find checksum for ${BINARY_NAME}-${platform} in checksums file" + error "This may indicate a corrupted or incomplete release." + error "To install anyway (not recommended): SKIP_CHECKSUM=true curl -fsSL ... | bash" + exit 1 + fi + + local actual_hash + if command -v sha256sum >/dev/null 2>&1; then + actual_hash=$(sha256sum "$binary_path" | awk '{print $1}') + elif command -v shasum >/dev/null 2>&1; then + actual_hash=$(shasum -a 256 "$binary_path" | awk '{print $1}') + else + error "No sha256sum or shasum available for checksum verification" + error "Please install sha256sum (coreutils) or use shasum" + error "To install anyway (not recommended): SKIP_CHECKSUM=true curl -fsSL ... | bash" + exit 1 + fi + + if [ "$expected_hash" != "$actual_hash" ]; then + error "Checksum verification failed!" + error "Expected: $expected_hash" + error "Actual: $actual_hash" + error "The downloaded binary may be corrupted or tampered with." + exit 1 + fi + + success "Checksum verified" +} + +# Main installation +main() { + echo "" + echo " ╔═══════════════════════════════════════╗" + echo " ║ Archon CLI Installer ║" + echo " ╚═══════════════════════════════════════╝" + echo "" + + # Detect platform + info "Detecting platform..." + local platform + platform=$(detect_platform) + success "Platform: $platform" + + # Get download URL + local download_url checksums_url + download_url=$(get_download_url "$platform" "$VERSION") + checksums_url=$(get_checksums_url "$VERSION") + + info "Version: $VERSION" + info "Download URL: $download_url" + + # Create temp directory + local tmp_dir + tmp_dir=$(mktemp -d) + trap "rm -rf '$tmp_dir'" EXIT + + local binary_path="$tmp_dir/$BINARY_NAME" + + # Download binary + info "Downloading binary..." + if ! curl -fsSL "$download_url" -o "$binary_path"; then + error "Failed to download binary from $download_url" + exit 1 + fi + success "Downloaded successfully" + + # Verify checksum + verify_checksum "$binary_path" "$platform" "$checksums_url" + + # Make executable + chmod +x "$binary_path" + + # Install + info "Installing to $INSTALL_DIR/$BINARY_NAME..." + + # Create install directory if needed + if [ ! -d "$INSTALL_DIR" ]; then + if ! mkdir -p "$INSTALL_DIR" 2>/dev/null; then + warn "Need sudo to create $INSTALL_DIR" + sudo mkdir -p "$INSTALL_DIR" + fi + fi + + # Install binary + if ! mv "$binary_path" "$INSTALL_DIR/$BINARY_NAME" 2>/dev/null; then + warn "Need sudo to install to $INSTALL_DIR" + sudo mv "$binary_path" "$INSTALL_DIR/$BINARY_NAME" + fi + + success "Installed to $INSTALL_DIR/$BINARY_NAME" + + # Verify installation + echo "" + info "Verifying installation..." + local version_output + if version_output=$("$INSTALL_DIR/$BINARY_NAME" version 2>&1); then + echo "$version_output" + success "Installation complete!" + else + warn "Binary installed but version check failed:" + echo "$version_output" + warn "The binary may not work correctly. Please verify manually with: $INSTALL_DIR/$BINARY_NAME version" + fi + + # Check if in PATH + if ! command -v "$BINARY_NAME" >/dev/null 2>&1; then + echo "" + warn "$INSTALL_DIR is not in your PATH" + echo "Add it with:" + echo " export PATH=\"$INSTALL_DIR:\$PATH\"" + echo "" + echo "Or add to your shell config (~/.bashrc, ~/.zshrc, etc.)" + fi + + echo "" + echo "Get started:" + echo " archon workflow list" + echo " archon workflow run assist \"What workflows are available?\"" + echo "" +} + +main "$@" diff --git a/scripts/update-homebrew.sh b/scripts/update-homebrew.sh new file mode 100755 index 00000000..f7f62afd --- /dev/null +++ b/scripts/update-homebrew.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# scripts/update-homebrew.sh +# Update Homebrew formula with checksums from a release +# +# Usage: ./scripts/update-homebrew.sh v0.2.0 + +set -euo pipefail + +VERSION="${1:-}" + +if [ -z "$VERSION" ]; then + echo "Usage: $0 " + echo "Example: $0 v0.2.0" + exit 1 +fi + +# Remove 'v' prefix if present for formula version +FORMULA_VERSION="${VERSION#v}" + +REPO="raswonders/remote-coding-agent" +FORMULA_FILE="homebrew/archon.rb" + +echo "Updating Homebrew formula for version $VERSION" + +# Download checksums +CHECKSUMS_URL="https://github.com/${REPO}/releases/download/${VERSION}/checksums.txt" +echo "Downloading checksums from $CHECKSUMS_URL" + +CHECKSUMS=$(curl -fsSL "$CHECKSUMS_URL") +echo "Checksums:" +echo "$CHECKSUMS" +echo "" + +# Extract individual checksums +SHA_DARWIN_ARM64=$(echo "$CHECKSUMS" | grep "archon-darwin-arm64" | awk '{print $1}') +SHA_DARWIN_X64=$(echo "$CHECKSUMS" | grep "archon-darwin-x64" | awk '{print $1}') +SHA_LINUX_ARM64=$(echo "$CHECKSUMS" | grep "archon-linux-arm64" | awk '{print $1}') +SHA_LINUX_X64=$(echo "$CHECKSUMS" | grep "archon-linux-x64" | awk '{print $1}') + +# Validate all checksums were extracted +validate_checksum() { + local name="$1" + local value="$2" + if [ -z "$value" ]; then + echo "ERROR: Could not extract checksum for $name" + echo "Checksums content:" + echo "$CHECKSUMS" + exit 1 + fi + # Validate it looks like a SHA256 hash (64 hex chars) + if ! echo "$value" | grep -qE '^[a-f0-9]{64}$'; then + echo "ERROR: Invalid checksum format for $name: $value" + echo "Expected 64 hex characters" + exit 1 + fi +} + +validate_checksum "archon-darwin-arm64" "$SHA_DARWIN_ARM64" +validate_checksum "archon-darwin-x64" "$SHA_DARWIN_X64" +validate_checksum "archon-linux-arm64" "$SHA_LINUX_ARM64" +validate_checksum "archon-linux-x64" "$SHA_LINUX_X64" + +echo "Extracted checksums:" +echo " darwin-arm64: $SHA_DARWIN_ARM64" +echo " darwin-x64: $SHA_DARWIN_X64" +echo " linux-arm64: $SHA_LINUX_ARM64" +echo " linux-x64: $SHA_LINUX_X64" +echo "" + +echo "Updating formula..." + +# Update version +sed -i.bak "s/version \".*\"/version \"${FORMULA_VERSION}\"/" "$FORMULA_FILE" + +# Update checksums - handles both PLACEHOLDER and existing 64-char hex hashes +# The formula structure places sha256 on its own line after url in each on_* block +# Pattern matches: sha256 "PLACEHOLDER..." or sha256 "64-hex-chars" +sed -i.bak "s/PLACEHOLDER_SHA256_DARWIN_ARM64/${SHA_DARWIN_ARM64}/" "$FORMULA_FILE" +sed -i.bak "s/PLACEHOLDER_SHA256_DARWIN_X64/${SHA_DARWIN_X64}/" "$FORMULA_FILE" +sed -i.bak "s/PLACEHOLDER_SHA256_LINUX_ARM64/${SHA_LINUX_ARM64}/" "$FORMULA_FILE" +sed -i.bak "s/PLACEHOLDER_SHA256_LINUX_X64/${SHA_LINUX_X64}/" "$FORMULA_FILE" + +# For subsequent runs, match any 64-char hex hash and update based on context +# The formula has separate on_arm/on_intel blocks under on_macos/on_linux +# We need to be careful to update the right checksum for each platform + +# Strategy: Use line context to identify which checksum to update +# Darwin ARM64: line after archon-darwin-arm64 URL +sed -i.bak '/archon-darwin-arm64/{n;s/sha256 "[a-f0-9]\{64\}"/sha256 "'"${SHA_DARWIN_ARM64}"'"/;}' "$FORMULA_FILE" +# Darwin x64: line after archon-darwin-x64 URL +sed -i.bak '/archon-darwin-x64/{n;s/sha256 "[a-f0-9]\{64\}"/sha256 "'"${SHA_DARWIN_X64}"'"/;}' "$FORMULA_FILE" +# Linux ARM64: line after archon-linux-arm64 URL +sed -i.bak '/archon-linux-arm64/{n;s/sha256 "[a-f0-9]\{64\}"/sha256 "'"${SHA_LINUX_ARM64}"'"/;}' "$FORMULA_FILE" +# Linux x64: line after archon-linux-x64 URL +sed -i.bak '/archon-linux-x64/{n;s/sha256 "[a-f0-9]\{64\}"/sha256 "'"${SHA_LINUX_X64}"'"/;}' "$FORMULA_FILE" + +# Clean up backup files +rm -f "${FORMULA_FILE}.bak" + +echo "Updated $FORMULA_FILE" +echo "" +echo "Next steps:" +echo "1. Review changes: git diff $FORMULA_FILE" +echo "2. Commit: git add $FORMULA_FILE && git commit -m 'chore: update Homebrew formula for $VERSION'" +echo "3. If you have a tap repo, copy the formula there" diff --git a/thoughts/shared/plans/2026-01-21-phase-5-cli-binary-distribution.md b/thoughts/shared/plans/2026-01-21-phase-5-cli-binary-distribution.md new file mode 100644 index 00000000..30819263 --- /dev/null +++ b/thoughts/shared/plans/2026-01-21-phase-5-cli-binary-distribution.md @@ -0,0 +1,1459 @@ +# Phase 5: CLI Binary Distribution + +## Overview + +Phase 5 creates standalone binary distributions of the Archon CLI for macOS and Linux. This enables users to install and run Archon without needing Bun, Node.js, or any other runtime installed. Windows users will use a documented manual installation process via cloning the repository. + +**Why this matters:** + +- Zero-dependency installation for end users +- Single binary download (no `npm install`, no `bun install`) +- Works immediately after install (`archon workflow list`) +- Leverages Phase 3's SQLite support (no PostgreSQL required) + +## Prerequisites + +- [x] Phase 1 complete: Monorepo structure with `@archon/core` extracted +- [x] Phase 2 complete: CLI entry point and basic commands working +- [x] Phase 3 complete: Database abstraction (SQLite + PostgreSQL auto-detection) +- [x] Phase 4 complete: Express → Hono migration +- [x] **Issue #322 complete**: Default commands/workflows loaded at runtime (not copied) - PR #324 merged + +**Critical dependency from Phase 3**: The CLI uses SQLite by default when `DATABASE_URL` is not set. This is essential for standalone binary distribution - users don't need a database server. + +**Critical dependency from Issue #322**: Default commands and workflows must be bundled into the binary. See "Bundled Defaults" section below. + +## Current State + +The CLI exists at `packages/cli/` with: + +- Entry point: `src/cli.ts` (shebang: `#!/usr/bin/env bun`) +- Commands: `workflow list/run/status`, `isolation list/cleanup`, `version` +- Package name: `@archon/cli` +- Current invocation: `bun run cli` or `bun packages/cli/src/cli.ts` + +**Existing GitHub Actions:** + +- `publish.yml` - Docker image publishing (triggers on tags/releases) +- `test.yml` - CI testing + +## Desired End State + +Users can install Archon CLI with a single command: + +```bash +# macOS/Linux - Primary method +curl -fsSL https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/scripts/install.sh | bash + +# Homebrew - Alternative for Homebrew users +brew install https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/Formula/archon.rb + +# Then use immediately +archon workflow list +archon workflow run assist "Hello world" +``` + +**Note**: This repository is currently at `dynamous-community/remote-coding-agent` and will be moved to a public repository (likely `archon-cli/archon` or similar) before the first release. All URLs in this plan use a `REPO` variable that will be updated at that time. + +**Platform support:** +| Platform | Architecture | Distribution Method | +|----------|-------------|---------------------| +| macOS | ARM64 (Apple Silicon) | curl script, Homebrew, direct download | +| macOS | x64 (Intel) | curl script, Homebrew, direct download | +| Linux | x64 | curl script, direct download | +| Linux | ARM64 | curl script, direct download | +| Windows | x64 | Manual: clone repo + `bun run cli` | + +**Verification:** + +- Downloaded binary runs without Bun installed +- `archon version` shows correct version +- `archon workflow list` works (uses SQLite, no DATABASE_URL needed) +- curl script detects OS/arch and downloads correct binary +- GitHub Releases contains all platform binaries + +## What We're NOT Doing + +- NOT creating a separate Homebrew tap repository (deferred to Phase 6) +- NOT publishing to npm +- NOT creating Windows binary (users clone + run with Bun) +- NOT setting up custom domain (using GitHub URLs) +- NOT adding new CLI features (distribution only) +- NOT changing CLI behavior + +## Repository Location + +**Current**: `dynamous-community/remote-coding-agent` (private) + +**Future**: Will be moved to a public repository before the first binary release. When this happens: + +1. Update the `REPO` variable in `scripts/install.sh` +2. Update the URLs in `Formula/archon.rb` +3. Update the README.md installation instructions + +All scripts use variables (`$REPO`, `${{ github.repository }}`) to make this transition easier. + +--- + +## Bundled Defaults (Critical for Binary Distribution) + +### The Problem + +When the CLI is compiled into a standalone binary, it has **no access to filesystem defaults**. The binary is just compiled TypeScript—it cannot read the loose `.archon/commands/defaults/` and `.archon/workflows/defaults/` files that exist in the source repository. + +**Without bundling:** +| Scenario | Result | +|----------|--------| +| User installs standalone binary | 0 commands, 0 workflows available | +| User runs `archon workflow list` | Empty list | +| User runs `archon workflow run assist "Hello"` | "Workflow not found" error | + +### The Solution + +Bundle default commands and workflows **into the compiled binary** at build time using Bun's static imports: + +```typescript +// packages/core/src/defaults/bundled-defaults.ts +import assistCommand from '../../../.archon/commands/defaults/assist.md' with { type: 'text' }; +import fixGithubIssueWorkflow from '../../../.archon/workflows/defaults/fix-github-issue.yaml' with { type: 'text' }; +// ... all other defaults + +export const BUNDLED_COMMANDS = { + assist: assistCommand, + implement: implementCommand, + // ... 16 total commands +}; + +export const BUNDLED_WORKFLOWS = { + 'fix-github-issue': fixGithubIssueWorkflow, + assist: assistWorkflow, + // ... 8 total workflows +}; +``` + +These imports are resolved at **compile time** and embedded directly into the binary. At runtime, the CLI reads from the embedded content. + +### How It Works With Issue #322 + +Issue #322 changes the loading behavior to: + +1. Load app's defaults at runtime (from filesystem when running with Bun) +2. Load target repo's project-specific commands (additive) + +For the standalone binary, we extend this: + +1. **If running as binary**: Load from `BUNDLED_COMMANDS` and `BUNDLED_WORKFLOWS` +2. **If running with Bun**: Load from filesystem (existing behavior) +3. Load target repo's project-specific commands (additive, same either way) + +```typescript +// packages/core/src/workflows/loader.ts +import { BUNDLED_WORKFLOWS } from '../defaults/bundled-defaults'; + +function isBinaryBuild(): boolean { + return !process.execPath.toLowerCase().includes('bun'); +} + +async function discoverWorkflows(targetRepoPath: string): Promise { + const workflows: Workflow[] = []; + + // 1. Load defaults (bundled for binary, filesystem for bun) + if (isBinaryBuild()) { + // Binary: use embedded defaults + for (const [name, content] of Object.entries(BUNDLED_WORKFLOWS)) { + workflows.push(parseWorkflow(name, content)); + } + } else { + // Bun: load from filesystem (existing behavior) + const appDefaultsPath = join(getAppArchonBasePath(), 'workflows', 'defaults'); + workflows.push(...(await loadWorkflowsFrom(appDefaultsPath))); + } + + // 2. Load repo's project-specific workflows (additive) + const repoWorkflowsPath = join(targetRepoPath, '.archon', 'workflows'); + workflows.push(...(await loadWorkflowsFrom(repoWorkflowsPath))); + + return dedupeByName(workflows); +} +``` + +### Binary Size Impact + +The defaults are just text files (~150KB total). This adds negligible size to the ~50-100MB binary. + +### Implementation Note + +**Issue #322 must be completed first.** It establishes: + +- Runtime loading architecture (no more copying) +- Multi-source discovery (app defaults + repo-specific) +- Opt-out configuration + +Phase 5 then adds: + +- The `bundled-defaults.ts` file with static imports +- Detection of binary vs Bun runtime +- Conditional loading from bundled content + +--- + +## Implementation Plan + +### Phase 5.0: Bundle Defaults for Binary Distribution + +**Goal**: Create the bundled defaults module so default commands and workflows are embedded in the compiled binary. + +**Prerequisite**: Issue #322 must be merged first (runtime loading architecture). + +**Files Created:** + +- `packages/core/src/defaults/bundled-defaults.ts` - Static imports of all defaults +- `packages/core/src/defaults/index.ts` - Exports for the defaults module + +**Changes:** + +#### 5.0.1: Create bundled-defaults.ts + +```typescript +// packages/core/src/defaults/bundled-defaults.ts +// Static imports - resolved at compile time and embedded in binary + +// Commands (16 total) +import assistCmd from '../../../../.archon/commands/defaults/assist.md' with { type: 'text' }; +import implementCmd from '../../../../.archon/commands/defaults/implement.md' with { type: 'text' }; +import planCmd from '../../../../.archon/commands/defaults/plan.md' with { type: 'text' }; +import investigateCmd from '../../../../.archon/commands/defaults/investigate.md' with { type: 'text' }; +import debugCmd from '../../../../.archon/commands/defaults/debug.md' with { type: 'text' }; +import reviewCmd from '../../../../.archon/commands/defaults/review.md' with { type: 'text' }; +import commitCmd from '../../../../.archon/commands/defaults/commit.md' with { type: 'text' }; +import prCmd from '../../../../.archon/commands/defaults/pr.md' with { type: 'text' }; +// ... import all 16 commands + +// Workflows (8 total) +import fixGithubIssueWf from '../../../../.archon/workflows/defaults/fix-github-issue.yaml' with { type: 'text' }; +import assistWf from '../../../../.archon/workflows/defaults/assist.yaml' with { type: 'text' }; +import implementWf from '../../../../.archon/workflows/defaults/implement.yaml' with { type: 'text' }; +import planWf from '../../../../.archon/workflows/defaults/plan.yaml' with { type: 'text' }; +// ... import all 8 workflows + +export const BUNDLED_COMMANDS: Record = { + assist: assistCmd, + implement: implementCmd, + plan: planCmd, + investigate: investigateCmd, + debug: debugCmd, + review: reviewCmd, + commit: commitCmd, + pr: prCmd, + // ... all 16 commands +}; + +export const BUNDLED_WORKFLOWS: Record = { + 'fix-github-issue': fixGithubIssueWf, + assist: assistWf, + implement: implementWf, + plan: planWf, + // ... all 8 workflows +}; +``` + +**Note**: The actual file will need to import ALL defaults. Generate this file by listing `.archon/commands/defaults/` and `.archon/workflows/defaults/`. + +#### 5.0.2: Update workflow loader to use bundled defaults + +Modify `packages/core/src/workflows/loader.ts`: + +```typescript +import { BUNDLED_WORKFLOWS } from '../defaults/bundled-defaults'; + +/** + * Detect if running as compiled binary (vs running with bun) + */ +function isBinaryBuild(): boolean { + return !process.execPath.toLowerCase().includes('bun'); +} + +/** + * Load bundled workflows (for binary distribution) + */ +function loadBundledWorkflows(): Workflow[] { + return Object.entries(BUNDLED_WORKFLOWS).map(([name, content]) => { + return parseWorkflowYaml(name, content); + }); +} + +// Update discoverWorkflows to check isBinaryBuild() +``` + +#### 5.0.3: Update command handler to use bundled defaults + +Similar changes to `packages/core/src/handlers/command-handler.ts` for commands. + +### Success Criteria (5.0): + +#### Automated Verification: + +- [x] `bun run type-check` passes +- [x] `bun test` passes +- [x] All default commands and workflows are imported in `bundled-defaults.ts` + +#### Manual Verification: + +- [x] Build a test binary: `bun build --compile packages/cli/src/cli.ts --outfile=test-archon` +- [x] Run `./test-archon workflow list` - should show all default workflows +- [ ] Run `./test-archon workflow run assist "Hello"` - should work (deferred - requires database) +- [x] Compare output to `bun run cli workflow list` - should be identical + +**Implementation Note**: This phase must be completed before Phase 5.1 (build scripts). The bundled defaults are essential for the binary to be useful. + +--- + +### Phase 5.1: Build Scripts for Binary Compilation + +**Goal**: Create scripts to compile the CLI into standalone binaries for all target platforms. + +**Files Created:** + +- `scripts/build-binaries.sh` - Main build script +- `scripts/checksums.sh` - Generate SHA256 checksums + +**Changes:** + +#### 5.1.1: Create build-binaries.sh + +```bash +#!/usr/bin/env bash +# scripts/build-binaries.sh +# Build standalone CLI binaries for all supported platforms + +set -euo pipefail + +# Get version from package.json or git tag +VERSION="${VERSION:-$(grep '"version"' package.json | head -1 | cut -d'"' -f4)}" +echo "Building Archon CLI v${VERSION}" + +# Output directory +DIST_DIR="dist/binaries" +mkdir -p "$DIST_DIR" + +# Define build targets +# Format: bun-target output-name +TARGETS=( + "bun-darwin-arm64:archon-darwin-arm64" + "bun-darwin-x64:archon-darwin-x64" + "bun-linux-x64:archon-linux-x64" + "bun-linux-arm64:archon-linux-arm64" +) + +# Build each target +for target_pair in "${TARGETS[@]}"; do + IFS=':' read -r target output_name <<< "$target_pair" + echo "Building for $target..." + + bun build \ + --compile \ + --target="$target" \ + --outfile="$DIST_DIR/$output_name" \ + packages/cli/src/cli.ts + + echo " → $DIST_DIR/$output_name" +done + +echo "" +echo "Build complete! Binaries in $DIST_DIR:" +ls -lh "$DIST_DIR" +``` + +#### 5.1.2: Create checksums.sh + +```bash +#!/usr/bin/env bash +# scripts/checksums.sh +# Generate SHA256 checksums for release binaries + +set -euo pipefail + +DIST_DIR="${1:-dist/binaries}" +CHECKSUM_FILE="$DIST_DIR/checksums.txt" + +echo "Generating checksums for binaries in $DIST_DIR" + +cd "$DIST_DIR" +shasum -a 256 archon-* > checksums.txt + +echo "Checksums written to $CHECKSUM_FILE:" +cat checksums.txt +``` + +#### 5.1.3: Update root package.json + +Add build scripts: + +```json +{ + "scripts": { + "build:binaries": "bash scripts/build-binaries.sh", + "build:checksums": "bash scripts/checksums.sh" + } +} +``` + +### Success Criteria (5.1): + +#### Automated Verification: + +- [x] `bun run build:binaries` completes without errors +- [x] Four binaries exist in `dist/binaries/`: + - `archon-darwin-arm64` + - `archon-darwin-x64` + - `archon-linux-x64` + - `archon-linux-arm64` +- [x] `bun run build:checksums` generates `dist/binaries/checksums.txt` +- [x] Each binary is executable: `chmod +x` and file shows correct architecture + +#### Manual Verification: + +- [x] On macOS ARM64: `./dist/binaries/archon-darwin-arm64 version` works +- [ ] Binary runs without Bun installed (test in clean environment or Docker) (deferred - manual verification) + +**Implementation Note**: Complete this phase before proceeding. The build scripts are the foundation for all distribution methods. + +--- + +### Phase 5.2: GitHub Actions Release Workflow + +**Goal**: Automate binary building and release on git tags. + +**Files Created:** + +- `.github/workflows/release.yml` - Build and release binaries + +**Changes:** + +#### 5.2.1: Create release.yml + +````yaml +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to release (e.g., v0.3.0)' + required: true + +permissions: + contents: write + +jobs: + build: + name: Build binaries + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build binaries + run: bun run build:binaries + + - name: Generate checksums + run: bun run build:checksums + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: binaries + path: dist/binaries/* + retention-days: 7 + + release: + name: Create GitHub Release + needs: build + runs-on: ubuntu-latest + + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: binaries + path: dist/binaries + + - name: Get version + id: version + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "version=${{ inputs.tag }}" >> $GITHUB_OUTPUT + else + echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.version.outputs.version }} + name: Archon ${{ steps.version.outputs.version }} + draft: false + prerelease: ${{ contains(steps.version.outputs.version, '-') }} + files: | + dist/binaries/archon-darwin-arm64 + dist/binaries/archon-darwin-x64 + dist/binaries/archon-linux-x64 + dist/binaries/archon-linux-arm64 + dist/binaries/checksums.txt + body: | + ## Installation + + ### Quick Install (macOS/Linux) + ```bash + curl -fsSL https://raw.githubusercontent.com/${{ github.repository }}/main/scripts/install.sh | bash + ``` + + ### Manual Download + Download the appropriate binary for your platform below, then: + ```bash + chmod +x archon-* + sudo mv archon-* /usr/local/bin/archon + ``` + + ### Verify Checksums + ```bash + shasum -a 256 -c checksums.txt + ``` + + ## What's New + See [CHANGELOG.md](https://github.com/${{ github.repository }}/blob/main/CHANGELOG.md) for details. +```` + +### Success Criteria (5.2): + +#### Automated Verification: + +- [x] Workflow file created at `.github/workflows/release.yml` +- [ ] Push a test tag (e.g., `v0.2.1-test`) triggers the workflow (deferred - manual test) +- [ ] Workflow completes successfully (deferred - requires push) +- [ ] GitHub Release is created with all 5 files (4 binaries + checksums) (deferred - requires push) + +#### Manual Verification: + +- [ ] Download a binary from the release and verify it runs (deferred - requires release) +- [ ] Checksums match: `shasum -a 256 -c checksums.txt` (deferred - requires release) +- [ ] Delete test release and tag after verification (deferred - requires release) + +**Implementation Note**: Test with a pre-release tag first before creating a real release. + +--- + +### Phase 5.3: Curl Install Script + +**Goal**: Create an install script that detects OS/architecture and installs the correct binary. + +**Files Created:** + +- `scripts/install.sh` - Universal install script + +**Changes:** + +#### 5.3.1: Create install.sh + +```bash +#!/usr/bin/env bash +# scripts/install.sh +# Install Archon CLI - downloads the correct binary for your platform +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/scripts/install.sh | bash +# +# Environment variables: +# ARCHON_VERSION - Specific version to install (default: latest) +# ARCHON_INSTALL_DIR - Installation directory (default: /usr/local/bin) + +set -euo pipefail + +# Configuration +# NOTE: Update this when repository moves to public location +REPO="${ARCHON_REPO:-dynamous-community/remote-coding-agent}" +INSTALL_DIR="${ARCHON_INSTALL_DIR:-/usr/local/bin}" +BINARY_NAME="archon" + +# Colors for output (if terminal supports it) +if [[ -t 1 ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + BLUE='\033[0;34m' + NC='\033[0m' # No Color +else + RED='' + GREEN='' + YELLOW='' + BLUE='' + NC='' +fi + +info() { echo -e "${BLUE}[INFO]${NC} $1"; } +success() { echo -e "${GREEN}[OK]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1" >&2; exit 1; } + +# Detect OS +detect_os() { + local os + os="$(uname -s)" + case "$os" in + Darwin) echo "darwin" ;; + Linux) echo "linux" ;; + MINGW* | MSYS* | CYGWIN*) + error "Windows is not supported via this script. Please see: https://github.com/$REPO#windows-installation" + ;; + *) error "Unsupported operating system: $os" ;; + esac +} + +# Detect architecture +detect_arch() { + local arch + arch="$(uname -m)" + case "$arch" in + x86_64 | amd64) echo "x64" ;; + arm64 | aarch64) echo "arm64" ;; + *) error "Unsupported architecture: $arch" ;; + esac +} + +# Get latest version from GitHub releases +get_latest_version() { + curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | + grep '"tag_name"' | + head -1 | + cut -d'"' -f4 +} + +# Download and install +main() { + info "Archon CLI Installer" + echo "" + + # Detect platform + local os arch + os="$(detect_os)" + arch="$(detect_arch)" + info "Detected platform: $os-$arch" + + # Get version + local version + version="${ARCHON_VERSION:-$(get_latest_version)}" + if [[ -z "$version" ]]; then + error "Could not determine version to install. Set ARCHON_VERSION or check GitHub releases." + fi + info "Installing version: $version" + + # Construct download URL + local binary_name="archon-${os}-${arch}" + local download_url="https://github.com/$REPO/releases/download/${version}/${binary_name}" + + # Create temp directory + local tmp_dir + tmp_dir="$(mktemp -d)" + trap 'rm -rf "$tmp_dir"' EXIT + + # Download binary + info "Downloading $binary_name..." + if ! curl -fsSL "$download_url" -o "$tmp_dir/archon"; then + error "Failed to download binary from: $download_url" + fi + + # Make executable + chmod +x "$tmp_dir/archon" + + # Verify binary works + info "Verifying binary..." + if ! "$tmp_dir/archon" version > /dev/null 2>&1; then + error "Binary verification failed. The downloaded file may be corrupted." + fi + + # Install to destination + info "Installing to $INSTALL_DIR..." + if [[ -w "$INSTALL_DIR" ]]; then + mv "$tmp_dir/archon" "$INSTALL_DIR/$BINARY_NAME" + else + warn "Need sudo to install to $INSTALL_DIR" + sudo mv "$tmp_dir/archon" "$INSTALL_DIR/$BINARY_NAME" + fi + + echo "" + success "Archon CLI $version installed successfully!" + echo "" + info "Run 'archon help' to get started" + info "Documentation: https://github.com/$REPO" +} + +main "$@" +``` + +### Success Criteria (5.3): + +#### Automated Verification: + +- [x] Script syntax is valid: `bash -n scripts/install.sh` +- [x] Script is executable: `chmod +x scripts/install.sh` +- [ ] `shellcheck scripts/install.sh` passes (optional - shellcheck not installed) + +#### Manual Verification: + +- [ ] Test on macOS (after releasing binaries) (deferred - requires release): + ```bash + curl -fsSL https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/scripts/install.sh | bash + archon version + ``` +- [ ] Test on Linux (Docker) (deferred - requires release): + ```bash + docker run --rm -it ubuntu:22.04 bash -c " + apt-get update && apt-get install -y curl + curl -fsSL https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/scripts/install.sh | bash + /usr/local/bin/archon version + " + ``` +- [ ] Verify correct binary is downloaded for each platform/architecture (deferred - requires release) +- [ ] Verify error message shows for Windows detection (deferred - requires Windows) + +**Implementation Note**: The install script must be committed to `main` before it can be used via raw.githubusercontent.com URL. + +--- + +### Phase 5.4: Homebrew Formula (Main Repo) + +**Goal**: Create a Homebrew formula in the main repository for users who prefer Homebrew. + +**Files Created:** + +- `Formula/archon.rb` - Homebrew formula + +**Changes:** + +#### 5.4.1: Create Formula directory and formula + +```ruby +# Formula/archon.rb +# Homebrew formula for Archon CLI +# Install: brew install --HEAD https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/Formula/archon.rb + +class Archon < Formula + desc "AI-powered coding assistant CLI - run workflows from the command line" + homepage "https://github.com/dynamous-community/remote-coding-agent" + license "MIT" + + # Version-specific bottles (populated by release workflow) + # For now, we use HEAD installation which downloads from releases + + on_macos do + on_arm do + url "https://github.com/dynamous-community/remote-coding-agent/releases/latest/download/archon-darwin-arm64" + sha256 :no_check # Will be populated in versioned releases + + def install + bin.install "archon-darwin-arm64" => "archon" + end + end + + on_intel do + url "https://github.com/dynamous-community/remote-coding-agent/releases/latest/download/archon-darwin-x64" + sha256 :no_check # Will be populated in versioned releases + + def install + bin.install "archon-darwin-x64" => "archon" + end + end + end + + on_linux do + on_arm do + url "https://github.com/dynamous-community/remote-coding-agent/releases/latest/download/archon-linux-arm64" + sha256 :no_check # Will be populated in versioned releases + + def install + bin.install "archon-linux-arm64" => "archon" + end + end + + on_intel do + url "https://github.com/dynamous-community/remote-coding-agent/releases/latest/download/archon-linux-x64" + sha256 :no_check # Will be populated in versioned releases + + def install + bin.install "archon-linux-x64" => "archon" + end + end + end + + test do + assert_match "Archon CLI", shell_output("#{bin}/archon version") + end +end +``` + +**Note**: This formula uses `sha256 :no_check` which Homebrew allows but warns about. For production, we'll want to either: + +1. Create versioned formulas with actual checksums (Phase 6 with Homebrew tap) +2. Or use the curl script as the primary installation method + +#### 5.4.2: Alternative - Simple HEAD formula + +If the above approach has issues, here's a simpler HEAD-only formula: + +```ruby +# Formula/archon.rb +class Archon < Formula + desc "AI-powered coding assistant CLI" + homepage "https://github.com/dynamous-community/remote-coding-agent" + head "https://github.com/dynamous-community/remote-coding-agent.git", branch: "main" + license "MIT" + + depends_on "oven-sh/bun/bun" => :build + + def install + system "bun", "install", "--frozen-lockfile" + system "bun", "build", "--compile", "--outfile=archon", "packages/cli/src/cli.ts" + bin.install "archon" + end + + test do + assert_match "Archon CLI", shell_output("#{bin}/archon version") + end +end +``` + +This builds from source, which works but is slower. Recommend using the first approach with pre-built binaries. + +### Success Criteria (5.4): + +#### Automated Verification: + +- [x] Formula file created at `Formula/archon.rb` +- [ ] Formula syntax is valid: `brew audit Formula/archon.rb` (deferred - requires release with binaries) +- [ ] `brew style Formula/archon.rb` passes (deferred - requires release with binaries) + +#### Manual Verification: + +- [ ] Install works (deferred - requires release with binaries): + ```bash + brew install --HEAD Formula/archon.rb + archon version + ``` +- [ ] Or via URL (deferred - requires release with binaries): + ```bash + brew install https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/Formula/archon.rb + ``` + +**Implementation Note**: Homebrew formula with pre-built binaries is the preferred approach. The HEAD formula (building from source) is a fallback. + +--- + +### Phase 5.5: Windows Documentation + +**Goal**: Document the Windows installation process clearly for Windows users. + +**Files Modified:** + +- `README.md` - Add installation section + +**Changes:** + +#### 5.5.1: Add Installation section to README.md + +Add the following section to the README (adjust location as appropriate): + +````markdown +## Installation + +### macOS / Linux (Recommended) + +Install with a single command: + +```bash +curl -fsSL https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/scripts/install.sh | bash +``` +```` + +Or download directly from [GitHub Releases](https://github.com/dynamous-community/remote-coding-agent/releases/latest). + +### macOS with Homebrew + +```bash +brew install https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/Formula/archon.rb +``` + +### Windows + +Windows binary distribution is not yet available. To use Archon on Windows: + +1. **Install Bun** (required): + + ```powershell + powershell -c "irm bun.sh/install.ps1 | iex" + ``` + +2. **Clone the repository**: + + ```powershell + git clone https://github.com/dynamous-community/remote-coding-agent.git + cd archon + ``` + +3. **Install dependencies**: + + ```powershell + bun install + ``` + +4. **Run the CLI**: + ```powershell + bun run cli workflow list + bun run cli workflow run assist "Hello world" + ``` + +**Tip**: Create an alias for easier usage: + +```powershell +# Add to your PowerShell profile ($PROFILE) +function archon { bun run cli $args } +``` + +### Verify Installation + +After installation, verify it works: + +```bash +archon version +archon help +``` + +### Updating + +**macOS/Linux (curl install)**: + +```bash +curl -fsSL https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/scripts/install.sh | bash +``` + +**Homebrew**: + +```bash +brew upgrade archon +``` + +**Windows**: + +```powershell +cd archon +git pull +bun install +``` + +```` + +### Success Criteria (5.5): + +#### Automated Verification: +- [x] README.md updated with CLI Installation section +- [x] All code blocks have language specified + +#### Manual Verification: +- [x] Instructions are clear and follow correctly +- [ ] Links to releases and repository work (deferred - requires release) +- [ ] Windows instructions work on a Windows machine (or VM) (deferred - requires Windows) + +--- + +### Phase 5.6: Version Command Update + +**Goal**: Ensure the version command shows useful information for installed binaries. + +**Files Modified:** +- `packages/cli/src/commands/version.ts` + +**Changes:** + +#### 5.6.1: Enhance version command + +The version command should show: +- CLI version (from package.json) +- Platform and architecture +- Installation method hint (binary vs bun) +- Database backend in use + +```typescript +// packages/cli/src/commands/version.ts +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import { getDatabaseType } from '@archon/core'; + +/** + * Get version from package.json + */ +function getVersion(): string { + try { + // Try to read from package.json (works in development) + const __dirname = dirname(fileURLToPath(import.meta.url)); + const pkgPath = join(__dirname, '..', '..', 'package.json'); + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); + return pkg.version ?? 'unknown'; + } catch { + // Fallback for compiled binary + return process.env.ARCHON_VERSION ?? '0.2.0'; + } +} + +/** + * Detect if running as compiled binary + */ +function isBinaryBuild(): boolean { + // Bun compiled binaries don't have Bun.main set the same way + // A simple heuristic: if process.execPath doesn't contain 'bun', it's a binary + return !process.execPath.toLowerCase().includes('bun'); +} + +/** + * Display version information + */ +export async function versionCommand(): Promise { + const version = getVersion(); + const platform = process.platform; + const arch = process.arch; + const dbType = getDatabaseType(); + const buildType = isBinaryBuild() ? 'binary' : 'source (bun)'; + + console.log(`Archon CLI v${version}`); + console.log(` Platform: ${platform}-${arch}`); + console.log(` Build: ${buildType}`); + console.log(` Database: ${dbType}`); +} +```` + +#### 5.6.2: Add getDatabaseType to core exports + +In `packages/core/src/index.ts`, ensure `getDatabaseType` is exported: + +```typescript +export { getDatabaseType } from './db/connection'; +``` + +In `packages/core/src/db/connection.ts`, add: + +```typescript +/** + * Get the current database type + */ +export function getDatabaseType(): 'postgresql' | 'sqlite' { + return process.env.DATABASE_URL ? 'postgresql' : 'sqlite'; +} +``` + +### Success Criteria (5.6): + +#### Automated Verification: + +- [x] `bun run type-check` passes +- [x] `bun run test` passes (tests updated for new version output format) + +#### Manual Verification: + +- [x] `bun run cli version` shows correct info: + ``` + Archon CLI v0.2.0 + Platform: darwin-arm64 + Build: source (bun) + Database: sqlite + ``` +- [ ] Compiled binary shows `Build: binary` (deferred - tested locally earlier) + +--- + +### Phase 5.7: Developer Release Guide + +**Goal**: Document the release process for maintainers so anyone can create a release. + +**Files Created:** + +- `docs/releasing.md` - Developer guide for creating releases + +**Changes:** + +#### 5.7.1: Create docs/releasing.md + +````markdown +# Releasing Archon CLI + +This guide explains how to create new releases of the Archon CLI binary. + +## Overview + +Archon CLI releases are automated via GitHub Actions. When you push a version tag (e.g., `v0.3.0`), the workflow: + +1. Builds binaries for all supported platforms (macOS ARM64/x64, Linux ARM64/x64) +2. Generates SHA256 checksums +3. Creates a GitHub Release with all artifacts +4. Updates the release notes + +## Prerequisites + +- Push access to the repository +- Git configured with your credentials + +## Release Process + +### 1. Prepare the Release + +Before releasing, ensure: + +```bash +# All tests pass +bun run validate + +# You're on the main branch with latest changes +git checkout main +git pull origin main +``` +```` + +### 2. Update Version Number + +Update the version in `package.json` (root): + +```bash +# Edit package.json and update "version" field +# Example: "0.2.0" → "0.3.0" +``` + +Also update `packages/cli/package.json` if it has its own version. + +### 3. Update CHANGELOG.md + +Add a new section for the release: + +```markdown +## [0.3.0] - 2026-01-21 + +### Added + +- New feature X +- New feature Y + +### Changed + +- Improved Z + +### Fixed + +- Bug fix A +``` + +### 4. Commit Version Bump + +```bash +git add package.json packages/cli/package.json CHANGELOG.md +git commit -m "chore: bump version to 0.3.0" +git push origin main +``` + +### 5. Create and Push Tag + +```bash +# Create annotated tag +git tag -a v0.3.0 -m "Release v0.3.0" + +# Push tag to trigger release workflow +git push origin v0.3.0 +``` + +### 6. Monitor Release Workflow + +1. Go to **Actions** tab in GitHub +2. Watch the "Release" workflow +3. Verify all jobs complete successfully + +### 7. Verify Release + +Once the workflow completes: + +1. Check [GitHub Releases](../../releases) for the new release +2. Verify all 5 files are attached: + - `archon-darwin-arm64` + - `archon-darwin-x64` + - `archon-linux-arm64` + - `archon-linux-x64` + - `checksums.txt` +3. Test installation: + ```bash + curl -fsSL https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/scripts/install.sh | bash + archon version + ``` + +## Manual Release (If Workflow Fails) + +If the GitHub Actions workflow fails, you can build and release manually: + +### Build Binaries Locally + +```bash +# Build all platform binaries +bun run build:binaries + +# Generate checksums +bun run build:checksums + +# Verify binaries exist +ls -la dist/binaries/ +``` + +### Create Release Manually + +1. Go to **Releases** → **Draft a new release** +2. Choose the tag you created +3. Set release title: `Archon v0.3.0` +4. Upload all files from `dist/binaries/` +5. Add release notes (copy from CHANGELOG.md) +6. Publish release + +## Pre-release / Beta Releases + +For pre-release versions: + +```bash +# Use semantic versioning pre-release format +git tag -a v0.3.0-beta.1 -m "Beta release v0.3.0-beta.1" +git push origin v0.3.0-beta.1 +``` + +Pre-releases (tags containing `-`) are automatically marked as pre-release in GitHub. + +## Hotfix Releases + +For urgent fixes: + +```bash +# Create hotfix branch from the release tag +git checkout -b hotfix/0.3.1 v0.3.0 + +# Make fixes, commit +git commit -m "fix: critical bug" + +# Merge to main +git checkout main +git merge hotfix/0.3.1 + +# Tag and release +git tag -a v0.3.1 -m "Hotfix release v0.3.1" +git push origin main v0.3.1 +``` + +## Rollback a Release + +If a release has critical issues: + +1. **Do NOT delete the release** (users may have already downloaded it) +2. Create a new patch release with the fix +3. Mark the broken release as pre-release (edit in GitHub UI) +4. Update release notes to warn about the issue + +## Versioning Guidelines + +We follow [Semantic Versioning](https://semver.org/): + +- **MAJOR** (1.0.0): Breaking changes to CLI interface or behavior +- **MINOR** (0.1.0): New features, backward compatible +- **PATCH** (0.0.1): Bug fixes, backward compatible + +Examples: + +- Adding a new command: MINOR +- Changing command output format: MAJOR (if scripts depend on it) +- Fixing a bug: PATCH +- Adding new optional flags: MINOR + +## Troubleshooting + +### Workflow fails on "Build binaries" + +Check if Bun can compile for all targets: + +```bash +bun build --compile --target=bun-darwin-arm64 packages/cli/src/cli.ts --outfile=test +``` + +### Binary doesn't run on target platform + +Ensure you're using the correct target. Test in Docker for Linux: + +```bash +docker run --rm -v $(pwd)/dist/binaries:/bins ubuntu:22.04 /bins/archon-linux-x64 version +``` + +### Checksums don't match + +Regenerate checksums and re-upload: + +```bash +cd dist/binaries +shasum -a 256 archon-* > checksums.txt +``` + +```` + +### Success Criteria (5.7): + +#### Automated Verification: +- [x] `docs/releasing.md` exists and is valid markdown +- [x] All commands in the guide are correct (test locally) + +#### Manual Verification: +- [x] Guide is clear and complete enough for a new maintainer to follow +- [ ] All links work (GitHub releases, repository URLs) (deferred - requires release) + +--- + +## Testing Strategy + +### Unit Tests + +- Update version command tests if output format changed +- No other new unit tests needed (distribution doesn't change behavior) + +### Integration Tests + +Test the full installation flow: + +```bash +# 1. Build binaries locally +bun run build:binaries + +# 2. Test each binary on appropriate platform +./dist/binaries/archon-darwin-arm64 version +./dist/binaries/archon-darwin-arm64 help +./dist/binaries/archon-darwin-arm64 workflow list + +# 3. Test without DATABASE_URL (should use SQLite) +unset DATABASE_URL +./dist/binaries/archon-darwin-arm64 workflow list + +# 4. Test curl script (after first release) +curl -fsSL https://raw.githubusercontent.com/dynamous-community/remote-coding-agent/main/scripts/install.sh | bash +archon version +```` + +### Manual Testing Steps + +1. **Local binary test** (before release): + - Build all binaries: `bun run build:binaries` + - Test on current machine + - Test in Docker container (for Linux) + +2. **Release test**: + - Create pre-release tag: `git tag v0.2.1-test && git push --tags` + - Verify GitHub Actions workflow runs + - Download binary from release + - Test on fresh machine (VM or Docker) + +3. **Install script test**: + - Run curl script on macOS + - Run curl script in Ubuntu Docker container + - Run curl script in Alpine Docker container + - Verify correct binary downloaded for each platform + +4. **Homebrew test**: + - Install formula on macOS + - Verify binary works after install + +--- + +## Risk Mitigation + +| Risk | Likelihood | Impact | Mitigation | +| ---------------------------------- | ---------- | ------ | -------------------------------------------------------- | +| Bun compile fails for some target | Low | High | Test build for all targets locally before workflow | +| Binary too large (>100MB) | Medium | Low | Accept size; Bun runtime is included. Can optimize later | +| curl script fails on some shells | Low | Medium | Use POSIX-compatible bash; test on zsh, bash, sh | +| Homebrew formula rejected by audit | Medium | Low | Use HEAD build as fallback; proper tap in Phase 6 | +| Install script permission issues | Low | Medium | Detect and prompt for sudo when needed | +| Old releases linger | Low | Low | Document upgrade process clearly | + +--- + +## Performance Considerations + +**Binary size**: Expect 50-100MB per binary. This includes: + +- JavaScriptCore runtime (Bun's JS engine) +- All bundled dependencies +- Source code + +This is standard for Bun-compiled binaries and acceptable for the benefit of zero-dependency installation. + +**Startup time**: Compiled binaries start faster than `bun run cli` since there's no dependency resolution at runtime. + +--- + +## Migration Notes + +**For existing users running from source**: + +- No migration needed - `bun run cli` continues to work +- Can optionally switch to binary for convenience + +**For new users**: + +- Use curl script or Homebrew for easiest installation +- Binary uses SQLite by default (no database setup required) + +--- + +## Future Work (Phase 6+) + +1. **Homebrew tap**: Create `homebrew-archon` repository for cleaner install experience (`brew install archon/archon/archon`) +2. **Windows binary**: Investigate Windows binary distribution (PowerShell install script, Scoop/Chocolatey) +3. **npm package**: Publish to npm for Node.js ecosystem users +4. **Custom domain**: Set up `get.archon.dev` for shorter curl URL +5. **Auto-update**: Add `archon update` command to self-update binary + +--- + +## References + +- Research document: `thoughts/shared/research/2026-01-20-cli-first-refactor-feasibility.md` +- Phase 3 plan (SQLite): `thoughts/shared/plans/2026-01-20-phase-3-database-abstraction-cli-isolation.md` +- Phase 4 plan (Hono): `thoughts/shared/plans/2026-01-21-phase-4-express-to-hono-migration.md` +- Bun compile documentation: https://bun.sh/docs/bundler/executables +- GitHub Actions release: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release +- Homebrew formula cookbook: https://docs.brew.sh/Formula-Cookbook + +--- + +## Estimated Effort + +| Phase | Task | Estimate | +| ------- | -------------------------- | ---------- | +| 5.0 | Bundle defaults for binary | 2-3 hours | +| 5.1 | Build scripts | 1-2 hours | +| 5.2 | GitHub Actions workflow | 2-3 hours | +| 5.3 | Curl install script | 1-2 hours | +| 5.4 | Homebrew formula | 1 hour | +| 5.5 | Windows documentation | 30 minutes | +| 5.6 | Version command update | 30 minutes | +| 5.7 | Developer release guide | 30 minutes | +| Testing | End-to-end verification | 2-3 hours | + +**Total: 1.5-2.5 days** + +**Note**: Phase 5.0 depends on Issue #322 being completed first. The issue establishes runtime loading architecture; Phase 5.0 adds the bundled defaults for binary distribution. diff --git a/thoughts/shared/research/2026-01-20-cli-first-refactor-feasibility.md b/thoughts/shared/research/2026-01-20-cli-first-refactor-feasibility.md index 15cfc95e..e1147bcd 100644 --- a/thoughts/shared/research/2026-01-20-cli-first-refactor-feasibility.md +++ b/thoughts/shared/research/2026-01-20-cli-first-refactor-feasibility.md @@ -995,17 +995,19 @@ Phase 1: Monorepo Structure + Core Package [COMPLETE] ↓ Phase 2: CLI Entry Point + Basic Commands [COMPLETE] ↓ -Phase 3: Database Abstraction + CLI Isolation ← SQLite support here! +Phase 3: Database Abstraction + CLI Isolation [COMPLETE] ├── Part A: Database adapter layer (SQLite + PostgreSQL) └── Part B: CLI isolation (--branch, --no-worktree) ↓ -Phase 4: Express → Hono Migration +Phase 4: Express → Hono Migration [COMPLETE] ↓ Phase 5: CLI Binary Distribution ← Distribution only, no features ↓ -Phase 6: Svelte 5 Dashboard (future) +Phase 6: CLI Auto-Update Command ← Self-update capability ↓ -Phase 7: Visual Workflow Builder (future) +Phase 7: Svelte 5 Dashboard (future) + ↓ +Phase 8: Visual Workflow Builder (future) ``` **Key insight**: Database abstraction (SQLite) is in Phase 3, NOT Phase 5. @@ -1341,7 +1343,85 @@ CLI Isolation: --- -### Phase 6: Svelte 5 Dashboard (Future) +### Phase 6: CLI Auto-Update Command + +**Goal**: Add `archon update` command that allows the CLI to update itself to the latest version. + +**Why this after Phase 5**: Once binary distribution is working, users need an easy way to update without re-running the install script manually. + +**How updates work without this**: + +- Users must manually re-run: `curl -fsSL https://.../install.sh | bash` +- No notification when new versions are available +- No way to check current vs latest version + +**Scope**: + +1. Add `archon update` command: + + ```bash + archon update # Update to latest version + archon update --check # Check for updates without installing + ``` + +2. Implementation: + + ```typescript + // packages/cli/src/commands/update.ts + export async function updateCommand(options: { check?: boolean }): Promise { + const currentVersion = getVersion(); + const latestVersion = await fetchLatestVersion(); // GitHub API + + if (currentVersion === latestVersion) { + console.log(`Already up to date (v${currentVersion})`); + return; + } + + console.log(`Update available: v${currentVersion} → v${latestVersion}`); + + if (options.check) { + return; // Just checking, don't install + } + + // Download and replace binary + const platform = detectPlatform(); // darwin-arm64, linux-x64, etc. + const binaryUrl = `https://github.com/.../releases/download/v${latestVersion}/archon-${platform}`; + + await downloadAndReplace(binaryUrl); + console.log(`Updated to v${latestVersion}`); + } + ``` + +3. Self-replacement strategy: + - Download new binary to temp location + - Verify checksum + - Replace current binary (may need sudo on some systems) + - Verify new binary works before completing + +4. Optional: Version check on startup (non-blocking) + - Check for updates in background on `archon` invocation + - Show subtle message if update available: `(update available: v0.4.0)` + - Don't block or slow down CLI startup + +**Success criteria**: + +- `archon update --check` shows if update is available +- `archon update` downloads and installs latest version +- Update works on macOS and Linux +- Handles permission errors gracefully (prompts for sudo if needed) +- Verifies download integrity before replacing + +**Estimated effort**: 1-2 days + +**Reference implementations**: + +- `gh` (GitHub CLI): `gh upgrade` +- `rustup`: `rustup update` +- `brew`: `brew upgrade` + +--- + +### Phase 7: Svelte 5 Dashboard (Future) **Goal**: Create a web dashboard for stats, settings, and monitoring. @@ -1361,7 +1441,7 @@ CLI Isolation: --- -### Phase 7: Visual Workflow Builder (Future) +### Phase 8: Visual Workflow Builder (Future) **Goal**: Drag-and-drop workflow editor that outputs YAML. @@ -1391,16 +1471,16 @@ CLI Isolation: ▼ ▼ ▼ ┌────────────────┐ ┌────────────┐ ┌────────────┐ │ Phase 2 │ │ Phase 4 │ │ (future) │ - │ CLI Entry │ │ Hono │ │ Phase 6 │ - │ [COMPLETE] │ └────────────┘ │ Dashboard │ - └───────┬────────┘ └─────┬──────┘ + │ CLI Entry │ │ Hono │ │ Phase 7 │ + │ [COMPLETE] │ │ [COMPLETE] │ │ Dashboard │ + └───────┬────────┘ └────────────┘ └─────┬──────┘ │ │ ▼ ▼ ┌────────────────┐ ┌────────────┐ - │ Phase 3 │ │ Phase 7 │ + │ Phase 3 │ │ Phase 8 │ │ DB Abstraction│ │ Workflow │ │ + Isolation │ │ Builder │ - │ (SQLite here!)│ └────────────┘ + │ [COMPLETE] │ └────────────┘ └───────┬────────┘ │ ▼ @@ -1408,30 +1488,35 @@ CLI Isolation: │ Phase 5 │ │ Distribution │ │ (binary only) │ + └───────┬────────┘ + │ + ▼ + ┌────────────────┐ + │ Phase 6 │ + │ Auto-Update │ + │ (archon update)│ └────────────────┘ ``` **Notes**: -- **Phase 3 includes SQLite** - This is critical. Database abstraction happens here, not Phase 5. -- Phase 4 (Hono) can happen in parallel with Phase 3 -- Phase 5 depends on Phase 3 completing (SQLite required for standalone binary) -- Phase 6/7 are independent of Phase 5, can start earlier if desired +- **Phases 1-4 are complete** - Core CLI functionality is working +- Phase 5 is about distribution (binaries, install scripts, Homebrew) +- Phase 6 adds self-update capability (`archon update`) +- Phase 7/8 (Dashboard, Workflow Builder) are independent and can start earlier if desired - Each phase can be a separate PR for easier review --- ## Implementation Recommendations -1. **Start with Phase 1** - This is the foundation. Don't skip it or do it half-way. +1. **Phases 1-4 are complete** - The foundation is solid. -2. **Phase 2 + 3 can be combined** if the implementer prefers, but isolation adds complexity so separating them is cleaner. +2. **Phase 5 is next** - Binary distribution makes the CLI accessible to users without Bun installed. -3. **Phase 4 is low-risk** - Express layer is already thin. Could even be done first as a warm-up. +3. **Phase 6 follows Phase 5** - Auto-update requires distribution to be working first. -4. **Phase 5 can wait** - Local `bun run` works fine for development. Distribution is for wider adoption. - -5. **Phases 6-7 are optional** - CLI-first means CLI is the primary interface. Dashboard is secondary. +4. **Phases 7-8 are optional** - CLI-first means CLI is the primary interface. Dashboard is secondary. --- @@ -1446,4 +1531,5 @@ CLI Isolation: | 3b | Isolation state conflicts | Use same `isolation_environments` table, transactions | | 4 | Webhook signature verification | Test with real GitHub webhooks before merging | | 5 | Binary size too large | Accept ~50-100MB, Bun runtime is included | -| 6-7 | Scope creep | Keep MVP focused, iterate later | +| 6 | Self-replacement permissions | Detect and prompt for sudo; verify binary before replacing | +| 7-8 | Scope creep | Keep MVP focused, iterate later | diff --git a/tsconfig.json b/tsconfig.json index 5ee31fad..6138759c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,5 +17,6 @@ "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "types": ["bun-types"] - } + }, + "include": ["global.d.ts"] }