mirror of
https://github.com/coleam00/Archon
synced 2026-04-21 13:37:41 +00:00
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:
parent
090e5fd812
commit
68e7db0466
31 changed files with 3321 additions and 81 deletions
137
.github/workflows/release.yml
vendored
Normal file
137
.github/workflows/release.yml
vendored
Normal 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
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
117
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
|
||||
|
|
|
|||
162
docs/releasing.md
Normal file
162
docs/releasing.md
Normal 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
|
||||
```
|
||||
|
|
@ -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
54
homebrew/archon.rb
Normal 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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
10
packages/cli/src/commands/bundled-version.ts
Normal file
10
packages/cli/src/commands/bundled-version.ts
Normal 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';
|
||||
|
|
@ -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)');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
57
packages/core/src/db/connection.test.ts
Normal file
57
packages/core/src/db/connection.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
164
packages/core/src/defaults/bundled-defaults.test.ts
Normal file
164
packages/core/src/defaults/bundled-defaults.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
91
packages/core/src/defaults/bundled-defaults.ts
Normal file
91
packages/core/src/defaults/bundled-defaults.ts
Normal 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');
|
||||
}
|
||||
8
packages/core/src/defaults/index.ts
Normal file
8
packages/core/src/defaults/index.ts
Normal 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';
|
||||
28
packages/core/src/defaults/text-imports.d.ts
vendored
Normal file
28
packages/core/src/defaults/text-imports.d.ts
vendored
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
79
scripts/build-binaries.sh
Executable 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
48
scripts/checksums.sh
Executable 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
254
scripts/install.sh
Executable 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
105
scripts/update-homebrew.sh
Executable 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"
|
||||
1459
thoughts/shared/plans/2026-01-21-phase-5-cli-binary-distribution.md
Normal file
1459
thoughts/shared/plans/2026-01-21-phase-5-cli-binary-distribution.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -17,5 +17,6 @@
|
|||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"types": ["bun-types"]
|
||||
}
|
||||
},
|
||||
"include": ["global.d.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue