feat: Phase 5 - CLI binary distribution (#325)

* docs: Add Phase 5 CLI binary distribution plan

- Create detailed implementation plan for binary distribution
- Add Phase 5.0: Bundle defaults for binary (depends on #322)
- Add Phase 5.1-5.7: Build scripts, GitHub Actions, curl install,
  Homebrew formula, Windows docs, version command, release guide
- Update research doc with Phase 6 (auto-update command)
- Renumber dashboard to Phase 7, workflow builder to Phase 8
- Mark Phases 1-4 as complete in research doc

* feat: Phase 5 - CLI binary distribution

Implement standalone binary distribution for Archon CLI:

- Bundle default commands and workflows into binaries at compile time
- Add build scripts for cross-platform compilation (macOS/Linux, ARM64/x64)
- Create GitHub Actions release workflow triggered on version tags
- Add curl install script with checksum verification
- Create Homebrew formula for macOS/Linux installation
- Update version command to show platform, build type, and database info
- Add developer release guide documentation
- Update README with CLI installation instructions

Binary compilation uses Bun's --compile flag to create standalone
executables that include the Bun runtime and all dependencies.
Default workflows and commands are imported as text at compile time
and embedded directly into the binary.

* fix: Pin Dockerfile to Bun 1.3.4 to match lockfile version

The Docker build was failing because oven/bun:1-slim resolved to 1.3.6
while the lockfile was created with 1.3.4, causing --frozen-lockfile to fail.

* docs: Clarify binary vs source builds for default commands/workflows

* fix: Address PR review issues for CLI binary distribution

Security fixes:
- install.sh: Require SKIP_CHECKSUM=true to bypass checksum verification
  instead of silently skipping (addresses security vulnerability)
- install.sh: Show actual error output when version check fails instead
  of falsely reporting success

Validation improvements:
- checksums.sh: Validate all 4 expected binaries exist before generating
  checksums to prevent releasing incomplete builds
- build-binaries.sh: Verify binary exists and has reasonable size (>1MB)
  after each build step
- update-homebrew.sh: Validate extracted checksums are non-empty and
  look like valid SHA256 hashes (64 hex chars)
- update-homebrew.sh: Fix sed patterns to use URL context for updating
  checksums on subsequent runs

Bug fixes:
- homebrew/archon.rb: Fix test to expect exit code 0 (success) instead
  of 1 for `archon version`
- loader.ts: Log error when bundled workflow fails to parse (indicates
  build-time corruption)

Test coverage:
- Add bundled-defaults.test.ts for isBinaryBuild() and content validation
- Add connection.test.ts for getDatabaseType() function
- Add binary build bundled workflow tests to loader.test.ts
- Add binary build bundled command tests to executor.test.ts

All 959 tests pass.
This commit is contained in:
Rasmus Widing 2026-01-21 23:51:51 +02:00 committed by GitHub
parent 090e5fd812
commit 68e7db0466
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 3321 additions and 81 deletions

137
.github/workflows/release.yml vendored Normal file
View file

@ -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
```

View file

@ -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

View file

@ -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"

117
README.md
View file

@ -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

162
docs/releasing.md Normal file
View file

@ -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
```

View file

@ -17,6 +17,7 @@ export default tseslint.config(
'**/*.js',
'*.mjs',
'**/*.test.ts',
'*.d.ts', // Root-level declaration files (not in tsconfig project scope)
],
},

54
homebrew/archon.rb Normal file
View file

@ -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

View file

@ -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",

View file

@ -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';

View file

@ -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)');
});
});

View file

@ -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<void> {
// 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<void> {
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<void> {
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}`);
}

View file

@ -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"

View file

@ -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');
});
});
});

View file

@ -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
*/

View file

@ -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);
}
});
});
});

View file

@ -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<string, string> = {
'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<string, string> = {
'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');
}

View file

@ -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';

View file

@ -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;
}

View file

@ -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

View file

@ -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<typeof bundledDefaults.isBinaryBuild>;
let loadConfigSpy: Mock<typeof configLoader.loadConfig>;
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<typeof mock>).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<typeof mock>).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<typeof mock>).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);
});
});
});

View file

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

View file

@ -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<typeof bundledDefaults.isBinaryBuild>;
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();
});
});
});

View file

@ -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<Map<string, Workfl
return workflows;
}
/**
* Load bundled default workflows (for binary distribution)
* Returns a Map of filename -> 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<string, WorkflowDefinition> {
const workflows = new Map<string, WorkflowDefinition>();
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<WorkflowDefinition[]> {
// Map of filename -> workflow for deduplication
@ -269,21 +301,34 @@ export async function discoverWorkflows(cwd: string): Promise<WorkflowDefinition
// 1. Load from app's bundled defaults (unless opted out)
const loadDefaultWorkflows = config.defaults?.loadDefaultWorkflows ?? true;
if (loadDefaultWorkflows) {
const appDefaultsPath = archonPaths.getDefaultWorkflowsPath();
console.log(`[WorkflowLoader] Loading app defaults from: ${appDefaultsPath}`);
try {
await access(appDefaultsPath);
const appWorkflows = await loadWorkflowsFromDir(appDefaultsPath);
for (const [filename, workflow] of appWorkflows) {
if (isBinaryBuild()) {
// Binary: load from embedded bundled content
console.log('[WorkflowLoader] Loading bundled default workflows (binary mode)');
const bundledWorkflows = loadBundledWorkflows();
for (const [filename, workflow] of bundledWorkflows) {
workflowsByFile.set(filename, workflow);
}
console.log(`[WorkflowLoader] Loaded ${String(appWorkflows.size)} app default workflows`);
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code !== 'ENOENT') {
console.warn(`[WorkflowLoader] Could not access app defaults: ${err.message}`);
} else {
console.log(`[WorkflowLoader] No app defaults directory found at: ${appDefaultsPath}`);
console.log(
`[WorkflowLoader] Loaded ${String(bundledWorkflows.size)} bundled default workflows`
);
} else {
// Bun: load from filesystem (development mode)
const appDefaultsPath = archonPaths.getDefaultWorkflowsPath();
console.log(`[WorkflowLoader] Loading app defaults from: ${appDefaultsPath}`);
try {
await access(appDefaultsPath);
const appWorkflows = await loadWorkflowsFromDir(appDefaultsPath);
for (const [filename, workflow] of appWorkflows) {
workflowsByFile.set(filename, workflow);
}
console.log(`[WorkflowLoader] Loaded ${String(appWorkflows.size)} app default workflows`);
} catch (error) {
const err = error as NodeJS.ErrnoException;
if (err.code !== 'ENOENT') {
console.warn(`[WorkflowLoader] Could not access app defaults: ${err.message}`);
} else {
console.log(`[WorkflowLoader] No app defaults directory found at: ${appDefaultsPath}`);
}
}
}
}

View file

@ -3,6 +3,6 @@
"compilerOptions": {
"noEmit": true
},
"include": ["src/**/*"],
"include": ["src/**/*", "src/defaults/text-imports.d.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

79
scripts/build-binaries.sh Executable file
View file

@ -0,0 +1,79 @@
#!/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}"
# Update bundled version in source before compiling
BUNDLED_VERSION_FILE="packages/cli/src/commands/bundled-version.ts"
echo "Updating bundled version to ${VERSION}..."
cat > "$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"

48
scripts/checksums.sh Executable file
View file

@ -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

254
scripts/install.sh Executable file
View file

@ -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 "$@"

105
scripts/update-homebrew.sh Executable file
View file

@ -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 <version>"
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"

File diff suppressed because it is too large Load diff

View file

@ -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<void> {
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 |

View file

@ -17,5 +17,6 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"types": ["bun-types"]
}
},
"include": ["global.d.ts"]
}