-
Notifications
You must be signed in to change notification settings - Fork 3.1k
feat: add archon serve command for one-command web UI install
#1011
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9987fb7
feafc6b
2fc6af7
a1b9096
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -78,6 +78,7 @@ import { continueCommand } from './commands/continue'; | |||||||||||||||||||||||||||||
| import { chatCommand } from './commands/chat'; | ||||||||||||||||||||||||||||||
| import { setupCommand } from './commands/setup'; | ||||||||||||||||||||||||||||||
| import { validateWorkflowsCommand, validateCommandsCommand } from './commands/validate'; | ||||||||||||||||||||||||||||||
| import { serveCommand } from './commands/serve'; | ||||||||||||||||||||||||||||||
| import { closeDatabase } from '@archon/core'; | ||||||||||||||||||||||||||||||
| import { setLogLevel, createLogger } from '@archon/paths'; | ||||||||||||||||||||||||||||||
| import * as git from '@archon/git'; | ||||||||||||||||||||||||||||||
|
|
@@ -110,6 +111,7 @@ Commands: | |||||||||||||||||||||||||||||
| isolation cleanup --merged Remove environments with branches merged into main | ||||||||||||||||||||||||||||||
| continue <branch> [msg] Continue work on an existing worktree with prior context | ||||||||||||||||||||||||||||||
| complete <branch> [...] Complete branch lifecycle (remove worktree + branches) | ||||||||||||||||||||||||||||||
| serve Start the web UI server (downloads web UI on first run) | ||||||||||||||||||||||||||||||
| validate workflows [name] Validate workflow definitions and their references | ||||||||||||||||||||||||||||||
| validate commands [name] Validate command files | ||||||||||||||||||||||||||||||
| version Show version info | ||||||||||||||||||||||||||||||
|
|
@@ -130,6 +132,8 @@ Options: | |||||||||||||||||||||||||||||
| --allow-env-keys Grant env-key consent during auto-registration | ||||||||||||||||||||||||||||||
| (bypasses the env-leak gate for this codebase; | ||||||||||||||||||||||||||||||
| logs an audit entry) | ||||||||||||||||||||||||||||||
| --port <port> Override server port for 'serve' (default: 3090) | ||||||||||||||||||||||||||||||
| --download-only Download web UI without starting the server | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Examples: | ||||||||||||||||||||||||||||||
| archon chat "What does the orchestrator do?" | ||||||||||||||||||||||||||||||
|
|
@@ -194,6 +198,8 @@ async function main(): Promise<number> { | |||||||||||||||||||||||||||||
| workflow: { type: 'string' }, | ||||||||||||||||||||||||||||||
| 'no-context': { type: 'boolean' }, | ||||||||||||||||||||||||||||||
| 'allow-env-keys': { type: 'boolean' }, | ||||||||||||||||||||||||||||||
| port: { type: 'string' }, | ||||||||||||||||||||||||||||||
| 'download-only': { type: 'boolean' }, | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| allowPositionals: true, | ||||||||||||||||||||||||||||||
| strict: false, // Allow unknown flags to pass through | ||||||||||||||||||||||||||||||
|
|
@@ -228,7 +234,7 @@ async function main(): Promise<number> { | |||||||||||||||||||||||||||||
| const subcommand = positionals[1]; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Commands that don't require git repo validation | ||||||||||||||||||||||||||||||
| const noGitCommands = ['version', 'help', 'setup', 'chat', 'continue']; | ||||||||||||||||||||||||||||||
| const noGitCommands = ['version', 'help', 'setup', 'chat', 'continue', 'serve']; | ||||||||||||||||||||||||||||||
| const requiresGitRepo = !noGitCommands.includes(command ?? ''); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
|
|
@@ -534,6 +540,12 @@ async function main(): Promise<number> { | |||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| case 'serve': { | ||||||||||||||||||||||||||||||
| const servePort = values.port !== undefined ? Number(values.port) : undefined; | ||||||||||||||||||||||||||||||
| const downloadOnly = Boolean(values['download-only']); | ||||||||||||||||||||||||||||||
| return await serveCommand({ port: servePort, downloadOnly }); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
Comment on lines
+543
to
+547
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing validation for invalid If the user passes a non-numeric port (e.g., 🔧 Proposed fix with validation case 'serve': {
const servePort = values.port ? Number(values.port) : undefined;
+ if (servePort !== undefined && (Number.isNaN(servePort) || servePort <= 0 || servePort > 65535)) {
+ console.error('Error: --port must be a valid port number (1-65535)');
+ return 1;
+ }
const downloadOnly = Boolean(values['download-only']);
return await serveCommand({ port: servePort, downloadOnly });
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||
| if (command === undefined) { | ||||||||||||||||||||||||||||||
| console.error('Missing command'); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from 'bun:test'; | ||
|
|
||
| // Mock @archon/paths BEFORE importing the module under test. | ||
| // This sets BUNDLED_IS_BINARY = false (dev mode) so serveCommand rejects. | ||
| const mockLogger = { | ||
| fatal: mock(() => undefined), | ||
| error: mock(() => undefined), | ||
| warn: mock(() => undefined), | ||
| info: mock(() => undefined), | ||
| debug: mock(() => undefined), | ||
| trace: mock(() => undefined), | ||
| }; | ||
| mock.module('@archon/paths', () => ({ | ||
| createLogger: mock(() => mockLogger), | ||
| getWebDistDir: mock((version: string) => `/tmp/test-archon/web-dist/${version}`), | ||
| BUNDLED_IS_BINARY: false, | ||
| BUNDLED_VERSION: 'dev', | ||
| })); | ||
|
|
||
| import { serveCommand, parseChecksum } from './serve'; | ||
|
|
||
| describe('parseChecksum', () => { | ||
| const validHash = 'a'.repeat(64); | ||
|
|
||
| it('should extract hash for matching filename', () => { | ||
| const checksums = [ | ||
| `${'b'.repeat(64)} archon-linux-x64`, | ||
| `${validHash} archon-web.tar.gz`, | ||
| `${'c'.repeat(64)} archon-darwin-arm64`, | ||
| ].join('\n'); | ||
|
|
||
| expect(parseChecksum(checksums, 'archon-web.tar.gz')).toBe(validHash); | ||
| }); | ||
|
|
||
| it('should handle single-space separator', () => { | ||
| const checksums = `${validHash} archon-web.tar.gz\n`; | ||
| expect(parseChecksum(checksums, 'archon-web.tar.gz')).toBe(validHash); | ||
| }); | ||
|
|
||
| it('should throw for missing filename', () => { | ||
| const checksums = `${validHash} archon-linux-x64\n`; | ||
| expect(() => parseChecksum(checksums, 'archon-web.tar.gz')).toThrow( | ||
| 'Checksum not found for archon-web.tar.gz' | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw for empty checksums text', () => { | ||
| expect(() => parseChecksum('', 'archon-web.tar.gz')).toThrow('Checksum not found'); | ||
| }); | ||
|
|
||
| it('should skip blank lines', () => { | ||
| const checksums = `\n${validHash} archon-web.tar.gz\n\n`; | ||
| expect(parseChecksum(checksums, 'archon-web.tar.gz')).toBe(validHash); | ||
| }); | ||
|
|
||
| it('should throw for malformed hash (not 64 hex chars)', () => { | ||
| const checksums = 'short_hash archon-web.tar.gz\n'; | ||
| expect(() => parseChecksum(checksums, 'archon-web.tar.gz')).toThrow( | ||
| 'Malformed checksum entry for archon-web.tar.gz' | ||
| ); | ||
| }); | ||
|
|
||
| it('should throw for uppercase hex hash', () => { | ||
| const checksums = `${'A'.repeat(64)} archon-web.tar.gz\n`; | ||
| expect(() => parseChecksum(checksums, 'archon-web.tar.gz')).toThrow( | ||
| 'Malformed checksum entry for archon-web.tar.gz' | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('serveCommand', () => { | ||
| let consoleErrorSpy: ReturnType<typeof spyOn>; | ||
|
|
||
| beforeEach(() => { | ||
| consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {}); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| consoleErrorSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('should reject in dev mode (non-binary)', async () => { | ||
| const exitCode = await serveCommand({}); | ||
| expect(exitCode).toBe(1); | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| 'Error: `archon serve` is for compiled binaries only.' | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject with downloadOnly in dev mode', async () => { | ||
| const exitCode = await serveCommand({ downloadOnly: true }); | ||
| expect(exitCode).toBe(1); | ||
| }); | ||
|
|
||
| it('should reject invalid port (NaN)', async () => { | ||
| const exitCode = await serveCommand({ port: NaN }); | ||
| expect(exitCode).toBe(1); | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining('--port must be an integer between 1 and 65535') | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject port out of range', async () => { | ||
| const exitCode = await serveCommand({ port: 99999 }); | ||
| expect(exitCode).toBe(1); | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining('--port must be an integer between 1 and 65535') | ||
| ); | ||
| }); | ||
|
|
||
| it('should reject port 0', async () => { | ||
| const exitCode = await serveCommand({ port: 0 }); | ||
| expect(exitCode).toBe(1); | ||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| expect.stringContaining('--port must be an integer between 1 and 65535') | ||
| ); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate checksum entry for
archon-web.tar.gz.The glob
archon-*already matchesarchon-web.tar.gz(since it starts with "archon-"), so explicitly adding it results in a duplicate line inchecksums.txt. This could cause issues with checksum verification if the parser doesn't handle duplicates gracefully.🔧 Proposed fix
- name: Generate checksums run: | cd dist - sha256sum archon-* archon-web.tar.gz > checksums.txt + sha256sum archon-* > checksums.txt cat checksums.txt🤖 Prompt for AI Agents