From 32650d2cb3a9935d3926eb375e3109d6ab7daf34 Mon Sep 17 00:00:00 2001 From: David Hagerty Date: Mon, 9 Feb 2026 09:13:41 -0500 Subject: [PATCH] feat(cli): add project subcommand (add/list/show/remove) Implement Task 6: Wire up project CLI subcommand with nested actions. - ProjectAction::Add: registers a new project with auto-generated ID - ProjectAction::List: displays all registered projects in tabular format - ProjectAction::Show: displays details for a specific project - ProjectAction::Remove: deletes a project registration Includes db_path() helper and resolve_project() helper for future use. Co-Authored-By: Claude Opus 4.6 --- src/main.rs | 116 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 5c2928b..2bba51e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use rustagent::{config, logging, planning, ralph}; +use rustagent::{config, logging, planning, ralph, project, db}; use clap::{Parser, Subcommand, CommandFactory}; use std::path::PathBuf; @@ -7,6 +7,10 @@ use std::path::PathBuf; #[command(name = "rustagent")] #[command(about = "A Rust-based AI agent for task execution", long_about = None)] struct Cli { + /// Project name (if omitted, resolves from current directory) + #[arg(long, global = true)] + project: Option, + #[command(subcommand)] command: Option, } @@ -33,6 +37,34 @@ enum Commands { #[arg(long)] max_iterations: Option, }, + /// Manage projects + Project { + #[command(subcommand)] + action: ProjectAction, + }, +} + +#[derive(Subcommand)] +enum ProjectAction { + /// Register a project + Add { + /// Friendly name for the project + name: String, + /// Path to the project directory + path: String, + }, + /// List all registered projects + List, + /// Show project details + Show { + /// Project name + name: String, + }, + /// Remove a registered project + Remove { + /// Project name + name: String, + }, } /// Find config file in standard locations @@ -64,6 +96,33 @@ fn find_config_path() -> anyhow::Result { ) } +/// Get the database path in XDG data directory +fn db_path() -> anyhow::Result { + let data_dir = dirs::data_dir() + .ok_or_else(|| anyhow::anyhow!("Could not determine XDG data directory"))?; + let db_dir = data_dir.join("rustagent"); + std::fs::create_dir_all(&db_dir)?; + Ok(db_dir.join("rustagent.db")) +} + +/// Resolve project from --project flag or current working directory +#[allow(dead_code)] +async fn resolve_project( + db: &db::Database, + project_name: Option<&str>, +) -> anyhow::Result> { + let store = project::ProjectStore::new(db.clone()); + + if let Some(name) = project_name { + // Look up by name + store.get_by_name(name).await + } else { + // Look up by current working directory + let cwd = std::env::current_dir()?; + store.get_by_path(&cwd).await + } +} + #[tokio::main] async fn main() -> anyhow::Result<()> { let _log_guard = logging::init_logging()?; @@ -130,6 +189,61 @@ async fn main() -> anyhow::Result<()> { let ralph = ralph::RalphLoop::new(config, spec_file.clone(), max_iterations)?; ralph.run().await?; } + Commands::Project { action } => { + // Open database + let db_path = db_path()?; + let database = db::Database::open(&db_path).await?; + let store = project::ProjectStore::new(database); + + match action { + ProjectAction::Add { name, path } => { + let proj = store.add(&name, std::path::Path::new(&path)).await?; + println!("Registered project '{}' ({}) at {}", + proj.name, proj.id, proj.path.display()); + } + ProjectAction::List => { + let projects = store.list().await?; + if projects.is_empty() { + println!("No projects registered"); + } else { + println!("{:<20} {:<10} {:<40}", "Name", "ID", "Path"); + println!("{}", "=".repeat(70)); + for proj in projects { + let path_str = proj.path.display().to_string(); + let path_display = if path_str.len() > 40 { + format!("{}...", &path_str[..37]) + } else { + path_str + }; + println!("{:<20} {:<10} {:<40}", proj.name, proj.id, path_display); + } + } + } + ProjectAction::Show { name } => { + match store.get_by_name(&name).await? { + Some(proj) => { + println!("Project: {}", proj.name); + println!(" ID: {}", proj.id); + println!(" Path: {}", proj.path.display()); + println!(" Registered: {}", proj.registered_at); + } + None => { + println!("Project '{}' not found", name); + } + } + } + ProjectAction::Remove { name } => { + match store.remove(&name).await? { + true => { + println!("Removed project '{}'", name); + } + false => { + println!("Project '{}' not found", name); + } + } + } + } + } } Ok(()) -- 2.54.0