diff --git a/CLAUDE.md b/CLAUDE.md index 12d5ffb6..608c96bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -206,10 +206,10 @@ docker-compose --profile with-db down ### CLI (Command Line) -Run workflows directly from the command line without needing the server: +Run workflows directly from the command line without needing the server. Workflow and isolation commands require running from within a git repository (subdirectories work - resolves to repo root). ```bash -# List available workflows +# List available workflows (requires git repo) bun run cli workflow list # Run a workflow diff --git a/README.md b/README.md index eb4d05ca..d1b0190b 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ archon workflow run assist --cwd /path/to/your/repo "What does this codebase do? bun run cli workflow list --cwd /path/to/your/repo ``` +**Note:** Workflow and isolation commands must be run from within a git repository. Running from subdirectories works (resolves to repo root). Version and help commands work anywhere. + **Detailed documentation:** [CLI User Guide](docs/cli-user-guide.md) --- diff --git a/docs/cli-developer-guide.md b/docs/cli-developer-guide.md index 7586c6e3..3a8cc553 100644 --- a/docs/cli-developer-guide.md +++ b/docs/cli-developer-guide.md @@ -38,7 +38,14 @@ packages/cli/ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ -│ cli.ts:154-228 Route to command handler │ +│ cli.ts:154-170 Git repository check │ +│ Skip for version/help, validate and resolve to │ +│ repo root for workflow/isolation commands │ +└─────────────────────────────────┬───────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ cli.ts:172-246 Route to command handler │ │ switch(command) → workflow | isolation | version│ └─────────────────────────────────┬───────────────────────────────┘ │ @@ -48,7 +55,13 @@ packages/cli/ └─────────────────────────────────────────────────────────────────┘ ``` -**Code:** `packages/cli/src/cli.ts:106-241` +**Code:** `packages/cli/src/cli.ts:106-259` + +**Git repository check:** +- Commands `workflow` and `isolation` require running from a git repository +- Commands `version` and `help` bypass this check +- When in a subdirectory, automatically resolves to repository root +- Exit code 1 if not in a git repository --- @@ -278,7 +291,7 @@ Worktrees stored at: `~/.archon/worktrees///` | Code | Meaning | |------|---------| | 0 | Success | -| 1 | Error (logged to stderr) | +| 1 | Error (logged to stderr, including not in git repo) | --- diff --git a/docs/cli-user-guide.md b/docs/cli-user-guide.md index 8695f9f9..1989b1c8 100644 --- a/docs/cli-user-guide.md +++ b/docs/cli-user-guide.md @@ -30,7 +30,7 @@ Run AI-powered workflows from your terminal. ## Quick Start ```bash -# List available workflows +# List available workflows (requires git repository) archon workflow list --cwd /path/to/repo # Run a workflow @@ -40,6 +40,8 @@ archon workflow run assist --cwd /path/to/repo "Explain the authentication flow" archon workflow run plan --cwd /path/to/repo --branch feature-auth "Add OAuth support" ``` +**Note:** Workflow and isolation commands require running from within a git repository. Running from subdirectories automatically resolves to the repo root. The `version` and `help` commands work anywhere. + ## Commands ### `workflow list` @@ -118,6 +120,8 @@ The CLI determines where to run based on: 1. `--cwd` flag (if provided) 2. Current directory (default) +Running from a subdirectory (e.g., `/repo/packages/cli`) automatically resolves to the git repository root (e.g., `/repo`). + When using `--branch`, workflows run inside the worktree directory. ## Environment diff --git a/packages/cli/src/cli.test.ts b/packages/cli/src/cli.test.ts index 590e0ca4..34d581cb 100644 --- a/packages/cli/src/cli.test.ts +++ b/packages/cli/src/cli.test.ts @@ -6,6 +6,7 @@ */ import { describe, it, expect } from 'bun:test'; import { parseArgs } from 'util'; +import * as git from '@archon/core/utils/git'; // Test the argument parsing logic used in cli.ts describe('CLI argument parsing', () => { @@ -131,3 +132,117 @@ describe('Conversation ID generation', () => { expect(ids.size).toBe(100); }); }); + +describe('CLI git repo check', () => { + /** + * These tests verify the command categorization logic used in cli.ts. + * The CLI uses: requiresGitRepo = !noGitCommands.includes(command ?? '') + * where noGitCommands = ['version', 'help'] + */ + describe('command categorization', () => { + // Mirror the actual noGitCommands array from cli.ts + const noGitCommands = ['version', 'help']; + + // Helper that mirrors the CLI's logic + const requiresGitRepo = (command: string | undefined): boolean => { + return !noGitCommands.includes(command ?? ''); + }; + + describe('commands that bypass git check', () => { + it('version command should not require git repo', () => { + expect(requiresGitRepo('version')).toBe(false); + }); + + it('help command should not require git repo', () => { + expect(requiresGitRepo('help')).toBe(false); + }); + }); + + describe('commands that require git repo', () => { + it('workflow command should require git repo', () => { + expect(requiresGitRepo('workflow')).toBe(true); + }); + + it('isolation command should require git repo', () => { + expect(requiresGitRepo('isolation')).toBe(true); + }); + + it('undefined command should require git repo (fail with unknown command later)', () => { + expect(requiresGitRepo(undefined)).toBe(true); + }); + + it('unknown commands should require git repo', () => { + expect(requiresGitRepo('unknown')).toBe(true); + }); + }); + }); + + describe('findRepoRoot behavior', () => { + // Test the actual git.findRepoRoot function with real directories + it('should find repo root from current test directory', async () => { + // This test file is inside a git repo, so findRepoRoot should work + const result = await git.findRepoRoot(process.cwd()); + expect(result).not.toBeNull(); + // The repo root should contain a .git directory or be the worktree root + expect(result).toMatch(/remote-coding-agent/); + }); + + it('should find repo root from a subdirectory', async () => { + // Use __dirname which is the directory containing this test file + // This is a real subdirectory (packages/cli/src) that should resolve to repo root + const subdirectory = import.meta.dir; + const result = await git.findRepoRoot(subdirectory); + + // Should resolve to repo root (remote-coding-agent), not packages/cli/src + expect(result).not.toBeNull(); + expect(result).toMatch(/remote-coding-agent$/); + expect(result).not.toContain('/packages/cli/src'); + }); + + it('should return null for system directories outside any git repo', async () => { + // /tmp is typically not inside a git repo + // Note: This test may need adjustment if /tmp happens to be inside a repo + const result = await git.findRepoRoot('/tmp'); + expect(result).toBeNull(); + }); + }); + + describe('path validation', () => { + // The CLI now validates that the path exists before calling findRepoRoot + // This tests the logic pattern used in cli.ts + const { existsSync } = require('fs'); + + it('should detect existing directories', () => { + expect(existsSync(process.cwd())).toBe(true); + expect(existsSync('/tmp')).toBe(true); + }); + + it('should detect non-existent directories', () => { + expect(existsSync('/this/path/definitely/does/not/exist/12345')).toBe(false); + }); + }); + + describe('error messages', () => { + // Verify the exact error messages used in cli.ts for documentation purposes + const ERROR_MESSAGES = { + notGitRepo: [ + 'Error: Not in a git repository.', + 'The Archon CLI must be run from within a git repository.', + 'Either navigate to a git repo or use --cwd to specify one.', + ], + dirNotExist: (path: string) => `Error: Directory does not exist: ${path}`, + }; + + it('should have actionable git repo error message', () => { + // Verify the messages include guidance + expect(ERROR_MESSAGES.notGitRepo[0]).toContain('Not in a git repository'); + expect(ERROR_MESSAGES.notGitRepo[2]).toContain('--cwd'); + }); + + it('should have clear directory error message', () => { + const msg = ERROR_MESSAGES.dirNotExist('/nonexistent'); + expect(msg).toContain('Directory does not exist'); + expect(msg).toContain('/nonexistent'); + }); + }); +}); diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index d454597a..ffbfe79a 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -49,6 +49,7 @@ import { } from './commands/workflow'; import { isolationListCommand, isolationCleanupCommand } from './commands/isolation'; import { closeDatabase } from '@archon/core'; +import * as git from '@archon/core/utils/git'; /** * Print usage information @@ -150,7 +151,31 @@ async function main(): Promise { const command = positionals[0]; const subcommand = positionals[1]; + // Commands that don't require git repo validation + const noGitCommands = ['version', 'help']; + const requiresGitRepo = !noGitCommands.includes(command ?? ''); + try { + // Validate working directory exists + let effectiveCwd = cwd; + if (requiresGitRepo) { + if (!existsSync(cwd)) { + console.error(`Error: Directory does not exist: ${cwd}`); + return 1; + } + + // Validate git repository and resolve to root + const repoRoot = await git.findRepoRoot(cwd); + if (!repoRoot) { + console.error('Error: Not in a git repository.'); + console.error('The Archon CLI must be run from within a git repository.'); + console.error('Either navigate to a git repo or use --cwd to specify one.'); + return 1; + } + // Use repo root as working directory (handles subdirectory case) + effectiveCwd = repoRoot; + } + switch (command) { case 'version': await versionCommand(); @@ -163,7 +188,7 @@ async function main(): Promise { case 'workflow': switch (subcommand) { case 'list': - await workflowListCommand(cwd); + await workflowListCommand(effectiveCwd); break; case 'run': { @@ -175,7 +200,7 @@ async function main(): Promise { const userMessage = positionals.slice(3).join(' ') || ''; // Conditionally construct options to satisfy discriminated union const options = branchName !== undefined ? { branchName, noWorktree } : {}; - await workflowRunCommand(cwd, workflowName, userMessage, options); + await workflowRunCommand(effectiveCwd, workflowName, userMessage, options); break; }