mirror of
https://github.com/coleam00/Archon
synced 2026-04-21 13:37:41 +00:00
feat(cli): Add git repository safety check (#328)
* feat(cli): Add git repository safety check Add validation to ensure CLI workflows only run from within a git repository. If the user runs from a subdirectory, the CLI resolves to the repo root. Running from a non-git directory produces a clear error message with guidance. - Add git.findRepoRoot() check before workflow commands - Bypass check for version/help commands - Add tests for git repo validation behavior * docs: Update CLI documentation for git repository requirement * fix(cli): Improve error handling and tests for git repo check - Move git.findRepoRoot() inside try-catch block to ensure proper error handling and database cleanup on unexpected errors - Add path existence validation before git check for clearer error messages - Improve tests: test real git.findRepoRoot behavior instead of mocks - Add tests for path validation and command categorization logic
This commit is contained in:
parent
006fa119a9
commit
c142dbbc54
6 changed files with 167 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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/<repo>/<branch-slug>/`
|
|||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success |
|
||||
| 1 | Error (logged to stderr) |
|
||||
| 1 | Error (logged to stderr, including not in git repo) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<number> {
|
|||
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<number> {
|
|||
case 'workflow':
|
||||
switch (subcommand) {
|
||||
case 'list':
|
||||
await workflowListCommand(cwd);
|
||||
await workflowListCommand(effectiveCwd);
|
||||
break;
|
||||
|
||||
case 'run': {
|
||||
|
|
@ -175,7 +200,7 @@ async function main(): Promise<number> {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue