use autom8::commands::{
all_sessions_status_command, clean_command, config_display_command, config_reset_command,
config_set_command, default_command, describe_command, global_status_command, gui_command,
improve_command, init_command, list_command, monitor_command, pr_review_command,
projects_command, resume_command, run_command, run_with_file, status_command, CleanOptions,
ConfigScope, ConfigSubcommand,
};
use autom8::completion::{print_completion_script, ShellType, SUPPORTED_SHELLS};
use autom8::output::{print_error, print_header};
use autom8::Runner;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "autom8")]
#[command(
version,
about = "CLI automation tool for orchestrating Claude-powered development",
after_help = "EXAMPLES:
# Start a new run from a spec file
autom8 spec.json
autom8 run --spec feature.json
# Run multiple features in parallel using worktrees
autom8 run --worktree --spec feature-a.json # Terminal 1
autom8 run --worktree --spec feature-b.json # Terminal 2
# Check status of all parallel sessions
autom8 status --all
# Resume a specific session
autom8 resume --list # See resumable sessions
autom8 resume --session abc123 # Resume by session ID
# Clean up after completing work
autom8 clean # Remove completed sessions
autom8 clean --worktrees # Also remove worktree directories"
)]
struct Cli {
file: Option<PathBuf>,
#[arg(short, long, global = true)]
verbose: bool,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
#[command(after_help = "EXAMPLES:
autom8 run --spec feature.json # Run on current branch
autom8 run --worktree # Create dedicated worktree for parallel execution
autom8 run --worktree --spec feature.json # Run in worktree with specific spec
WORKTREE MODE:
When --worktree is enabled, autom8 creates a separate worktree directory
at <repo-parent>/<repo>-wt-<branch>/ allowing multiple specs to run in parallel.
Each worktree has its own isolated session state.")]
Run {
#[arg(long, default_value = "./spec.json", conflicts_with = "self_test")]
spec: PathBuf,
#[arg(long)]
skip_review: bool,
#[arg(long, conflicts_with = "no_worktree")]
worktree: bool,
#[arg(long, conflicts_with = "worktree")]
no_worktree: bool,
#[arg(long, conflicts_with = "spec")]
self_test: bool,
},
#[command(after_help = "EXAMPLES:
autom8 status # Show current session status
autom8 status --all # Show all sessions for this project
autom8 status --global # Show status across all projects
autom8 status --project myapp --all # Show all sessions for a specific project
SESSION STATUS:
Sessions are shown with: session ID, worktree path, branch name,
current state (e.g., RunningClaude, Reviewing), and current story.
The current session (matching CWD) is highlighted.")]
Status {
#[arg(short = 'a', long = "all")]
all: bool,
#[arg(short = 'g', long = "global")]
global: bool,
#[arg(short, long)]
project: Option<String>,
},
#[command(after_help = "EXAMPLES:
autom8 resume # Resume current session (auto-detected from CWD)
autom8 resume --list # List all resumable sessions
autom8 resume --session abc123 # Resume a specific session by ID
BEHAVIOR:
In the main repo with multiple incomplete sessions: prompts for selection.
In a worktree: automatically resumes that worktree's session.
With --session: changes to the worktree directory before resuming.")]
Resume {
#[arg(short, long)]
session: Option<String>,
#[arg(short, long)]
list: bool,
},
#[command(after_help = "EXAMPLES:
autom8 clean # Remove completed/failed session state
autom8 clean --worktrees # Also remove associated worktree directories
autom8 clean --all # Remove ALL sessions (with confirmation)
autom8 clean --session abc123 # Remove a specific session
autom8 clean --orphaned # Remove orphaned sessions only
autom8 clean --worktrees --force # Remove even with uncommitted changes
autom8 clean --project myapp # Clean a specific project by name
WHAT GETS CLEANED:
By default, cleans completed and failed sessions (preserves in-progress).
Session state is archived to runs/ directory before deletion.
Worktrees with uncommitted changes are preserved unless --force is used.")]
Clean {
#[arg(short, long)]
worktrees: bool,
#[arg(short, long)]
all: bool,
#[arg(short, long)]
session: Option<String>,
#[arg(short, long)]
orphaned: bool,
#[arg(short, long)]
force: bool,
#[arg(short, long)]
project: Option<String>,
},
#[command(after_help = "EXAMPLES:
autom8 config # Show both global and project config
autom8 config --global # Show only global config
autom8 config --project # Show only project config
autom8 config set review false # Set a value in project config
autom8 config set --global commit true # Set a value in global config
autom8 config reset # Reset project config to defaults
autom8 config reset --global # Reset global config to defaults
CONFIG FILES:
Global: ~/.config/autom8/config.toml
Project: ~/.config/autom8/<project>/config.toml
The project config takes precedence over global config when both exist.
If a config file doesn't exist, defaults are shown with a note.
VALID KEYS:
review - Enable code review step (true/false)
commit - Enable auto-commit (true/false)
pull_request - Enable auto-PR creation (true/false)
worktree - Enable worktree mode (true/false)
worktree_path_pattern - Pattern for worktree names (string)
worktree_cleanup - Auto-cleanup worktrees (true/false)
SUBCOMMANDS:
set Set a configuration value
reset Reset configuration to default values
Run 'autom8 config <subcommand> --help' for more details on each subcommand.")]
Config {
#[arg(short, long, conflicts_with = "project")]
global: bool,
#[arg(short, long, conflicts_with = "global")]
project: bool,
#[command(subcommand)]
subcommand: Option<ConfigSubcommand>,
},
Init,
Projects,
List,
Describe {
project_name: Option<String>,
},
PrReview,
Monitor,
Gui,
#[command(after_help = "EXAMPLES:
autom8 improve # Gather context and spawn Claude session
autom8 improve -v # Same, with verbose output (future use)
BEHAVIOR:
The improve command auto-detects everything from the current git branch:
1. Gathers git context (branch, commits, file changes)
2. Loads spec if found (from session or by branch name)
3. Extracts session knowledge (decisions, patterns, files, work summaries)
4. Displays a brief summary of loaded context
5. Spawns an interactive Claude session with the context
Context is gathered additively - git context is always available,
spec and session knowledge are included when a matching session exists.")]
Improve,
#[command(hide = true)]
Completions {
shell: String,
},
}
fn main() {
let cli = Cli::parse();
let result = match (&cli.file, &cli.command) {
(
None,
Some(Commands::Config {
global,
project,
subcommand,
}),
) => {
match subcommand {
None => {
let scope = match (global, project) {
(true, false) => ConfigScope::Global,
(false, true) => ConfigScope::Project,
_ => ConfigScope::Both,
};
config_display_command(scope)
}
Some(ConfigSubcommand::Set {
global: g,
key,
value,
}) => config_set_command(key, value, *g),
Some(ConfigSubcommand::Reset { global: g, yes }) => config_reset_command(*g, *yes),
}
}
(None, Some(Commands::Completions { shell })) => match ShellType::from_name(shell) {
Ok(shell_type) => {
print_completion_script(shell_type);
Ok(())
}
Err(e) => {
print_error(&format!(
"{}\nSupported shells: {}",
e,
SUPPORTED_SHELLS.join(", ")
));
std::process::exit(1);
}
},
_ => {
let runner = match Runner::new() {
Ok(r) => r.with_verbose(cli.verbose),
Err(e) => {
print_error(&format!("Failed to initialize runner: {}", e));
std::process::exit(1);
}
};
match (&cli.file, &cli.command) {
(Some(file), _) => run_with_file(&runner, file),
(
None,
Some(Commands::Run {
spec,
skip_review,
worktree,
no_worktree,
self_test,
}),
) => run_command(
cli.verbose,
spec,
*skip_review,
*worktree,
*no_worktree,
*self_test,
),
(
None,
Some(Commands::Status {
all,
global,
project,
}),
) => {
print_header();
if *global {
global_status_command()
} else if *all {
all_sessions_status_command(project.as_deref())
} else {
status_command(&runner)
}
}
(None, Some(Commands::Resume { session, list })) => {
resume_command(session.as_deref(), *list)
}
(
None,
Some(Commands::Clean {
worktrees,
all,
session,
orphaned,
force,
project,
}),
) => clean_command(CleanOptions {
worktrees: *worktrees,
all: *all,
session: session.clone(),
orphaned: *orphaned,
force: *force,
project: project.clone(),
}),
(None, Some(Commands::Config { .. })) => unreachable!(),
(None, Some(Commands::Init)) => init_command(),
(None, Some(Commands::Projects)) => projects_command(),
(None, Some(Commands::List)) => list_command(),
(None, Some(Commands::Describe { project_name })) => {
describe_command(project_name.as_deref().unwrap_or(""))
}
(None, Some(Commands::PrReview)) => {
print_header();
pr_review_command(cli.verbose)
}
(None, Some(Commands::Monitor)) => monitor_command(),
(None, Some(Commands::Gui)) => gui_command(),
(None, Some(Commands::Improve)) => improve_command(cli.verbose),
(None, Some(Commands::Completions { .. })) => unreachable!(),
(None, None) => default_command(cli.verbose),
}
}
};
if let Err(e) = result {
print_error(&e.to_string());
std::process::exit(1);
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn test_default_flow_and_file_argument() {
let cli = Cli::try_parse_from(["autom8"]).unwrap();
assert!(cli.file.is_none());
assert!(cli.command.is_none());
let cli = Cli::try_parse_from(["autom8", "my-spec.json"]).unwrap();
assert_eq!(cli.file.unwrap().to_string_lossy(), "my-spec.json");
assert!(cli.command.is_none());
}
#[test]
fn test_all_commands_recognized() {
let commands = [
"run",
"resume",
"status",
"clean",
"init",
"projects",
"list",
"describe",
"config",
"monitor",
"gui",
"improve",
"pr-review",
];
for cmd in commands {
let result = Cli::try_parse_from(["autom8", cmd]);
assert!(result.is_ok(), "Command '{}' should parse", cmd);
}
assert!(Cli::try_parse_from(["autom8", "completions", "bash"]).is_ok());
}
#[test]
fn test_version_flag() {
let result = Cli::try_parse_from(["autom8", "--version"]);
assert_eq!(
result.err().unwrap().kind(),
clap::error::ErrorKind::DisplayVersion
);
}
#[test]
fn test_config_command_parsing() {
let cli = Cli::try_parse_from(["autom8", "config"]).unwrap();
if let Some(Commands::Config { subcommand, .. }) = cli.command {
assert!(subcommand.is_none());
}
let cli =
Cli::try_parse_from(["autom8", "config", "set", "-g", "review", "false"]).unwrap();
if let Some(Commands::Config { subcommand, .. }) = cli.command {
if let Some(ConfigSubcommand::Set { global, key, value }) = subcommand {
assert!(global);
assert_eq!(key, "review");
assert_eq!(value, "false");
}
}
assert!(Cli::try_parse_from(["autom8", "config", "set"]).is_err());
assert!(Cli::try_parse_from(["autom8", "config", "set", "review"]).is_err());
}
#[test]
fn test_worktree_flags_mutual_exclusivity() {
let cli = Cli::try_parse_from(["autom8", "run", "--worktree"]).unwrap();
if let Some(Commands::Run {
worktree,
no_worktree,
..
}) = cli.command
{
assert!(worktree);
assert!(!no_worktree);
}
let cli = Cli::try_parse_from(["autom8", "run", "--no-worktree"]).unwrap();
if let Some(Commands::Run {
worktree,
no_worktree,
..
}) = cli.command
{
assert!(!worktree);
assert!(no_worktree);
}
assert!(Cli::try_parse_from(["autom8", "run", "--worktree", "--no-worktree"]).is_err());
}
#[test]
fn test_self_test_flag() {
let cli = Cli::try_parse_from(["autom8", "run", "--self-test"]).unwrap();
if let Some(Commands::Run { self_test, .. }) = cli.command {
assert!(self_test);
}
assert!(
Cli::try_parse_from(["autom8", "run", "--self-test", "--spec", "test.json"]).is_err()
);
}
#[test]
fn test_status_command_flags() {
let cli = Cli::try_parse_from(["autom8", "status", "-a", "--project", "myproj"]).unwrap();
if let Some(Commands::Status {
all,
global,
project,
}) = cli.command
{
assert!(all);
assert!(!global);
assert_eq!(project, Some("myproj".to_string()));
}
let cli = Cli::try_parse_from(["autom8", "status", "-g"]).unwrap();
if let Some(Commands::Status { global, .. }) = cli.command {
assert!(global);
}
}
#[test]
fn test_resume_command_flags() {
let cli = Cli::try_parse_from(["autom8", "resume", "-s", "abc123", "-l"]).unwrap();
if let Some(Commands::Resume { session, list }) = cli.command {
assert_eq!(session, Some("abc123".to_string()));
assert!(list);
}
}
#[test]
fn test_describe_command() {
let cli = Cli::try_parse_from(["autom8", "describe", "my-project"]).unwrap();
if let Some(Commands::Describe { project_name }) = cli.command {
assert_eq!(project_name, Some("my-project".to_string()));
}
let cli = Cli::try_parse_from(["autom8", "describe"]).unwrap();
if let Some(Commands::Describe { project_name }) = cli.command {
assert!(project_name.is_none());
}
}
#[test]
fn test_state_manager_load_save_clear() {
use autom8::state::{RunState, StateManager};
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let sm = StateManager::with_dir(temp_dir.path().to_path_buf());
assert!(sm.load_current().unwrap().is_none());
let state = RunState::new(PathBuf::from("test.json"), "feature/test".to_string());
sm.save(&state).unwrap();
let loaded = sm.load_current().unwrap().unwrap();
assert_eq!(loaded.branch, "feature/test");
sm.clear_current().unwrap();
assert!(sm.load_current().unwrap().is_none());
}
#[test]
fn test_state_archive_workflow() {
use autom8::state::{RunState, StateManager};
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let sm = StateManager::with_dir(temp_dir.path().to_path_buf());
let state1 = RunState::new(PathBuf::from("spec1.json"), "feature/first".to_string());
let state2 = RunState::new(PathBuf::from("spec2.json"), "feature/second".to_string());
let archive1 = sm.archive(&state1).unwrap();
let archive2 = sm.archive(&state2).unwrap();
assert!(archive1.exists());
assert!(archive2.exists());
assert_eq!(sm.list_archived().unwrap().len(), 2);
}
#[test]
fn test_config_defaults() {
let config = autom8::config::Config::default();
assert!(config.review);
assert!(config.commit);
assert!(config.pull_request);
assert!(config.worktree);
}
}