diff --git a/apps/blue-cli/src/main.rs b/apps/blue-cli/src/main.rs index c663500..8d815da 100644 --- a/apps/blue-cli/src/main.rs +++ b/apps/blue-cli/src/main.rs @@ -590,14 +590,46 @@ enum RfcCommands { Create { /// RFC title title: String, + + /// Problem statement + #[arg(long)] + problem: Option, + + /// Source spike (links RFC to spike) + #[arg(long)] + source_spike: Option, + }, + /// List all RFCs + List { + /// Filter by status (draft, accepted, in-progress, implemented) + #[arg(long)] + status: Option, + }, + /// Get RFC details + Get { + /// RFC title + title: String, + }, + /// Update RFC status + Status { + /// RFC title + title: String, + + /// New status (draft, accepted, in-progress, implemented, superseded) + #[arg(long)] + set: String, }, /// Create a plan for an RFC Plan { /// RFC title title: String, + + /// Tasks (can be specified multiple times) + #[arg(long, num_args = 1..)] + task: Vec, }, - /// Get RFC details - Get { + /// Mark RFC as complete + Complete { /// RFC title title: String, }, @@ -870,9 +902,45 @@ async fn tokio_main() -> Result<()> { } match cli.command { - None | Some(Commands::Status) => { + None => { println!("{}", blue_core::voice::welcome()); } + Some(Commands::Status) => { + match get_project_state() { + Ok(state) => { + let args = serde_json::json!({}); + match blue_mcp::handlers::status::handle_status(&state, &args) { + Ok(result) => { + let project = result.get("project").and_then(|v| v.as_str()).unwrap_or("unknown"); + let active = result.get("active").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); + let ready = result.get("ready").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); + let stalled = result.get("stalled").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); + let drafts = result.get("drafts").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); + let hint = result.get("hint").and_then(|v| v.as_str()).unwrap_or(""); + + println!("Project: {}", project); + println!("Active: {} RFC(s)", active); + println!("Ready: {} RFC(s)", ready); + if stalled > 0 { + println!("Stalled: {} RFC(s)", stalled); + } + println!("Drafts: {} RFC(s)", drafts); + println!(); + println!("{}", hint); + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + } + Err(_) => { + println!("{}", blue_core::voice::welcome()); + println!(); + println!("Run 'blue init' to get started."); + } + } + } Some(Commands::Init { force }) => { let cwd = std::env::current_dir()?; let blue_dir = cwd.join(".blue"); @@ -907,8 +975,23 @@ async fn tokio_main() -> Result<()> { println!(" blue status"); } Some(Commands::Next) => { - println!("Looking at what's ready. One moment."); - // TODO: Implement next + let state = get_project_state()?; + let args = serde_json::json!({}); + match blue_mcp::handlers::status::handle_next(&state, &args) { + Ok(result) => { + if let Some(recs) = result.get("recommendations").and_then(|v| v.as_array()) { + for rec in recs { + if let Some(s) = rec.as_str() { + println!("{}", s); + } + } + } + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } } Some(Commands::Mcp { .. }) => { blue_mcp::run().await?; @@ -922,34 +1005,12 @@ async fn tokio_main() -> Result<()> { Some(Commands::Session { command }) => { handle_session_command(command).await?; } - Some(Commands::Rfc { command }) => match command { - RfcCommands::Create { title } => { - println!("{}", blue_core::voice::success( - &format!("Created RFC '{}'", title), - Some("Want me to help fill in the details?"), - )); - } - RfcCommands::Plan { title } => { - println!("{}", blue_core::voice::ask( - &format!("Ready to plan '{}'", title), - "What are the tasks", - )); - } - RfcCommands::Get { title } => { - println!("Looking for '{}'.", title); - } - }, - Some(Commands::Worktree { command }) => match command { - WorktreeCommands::Create { title } => { - println!("Creating worktree for '{}'.", title); - } - WorktreeCommands::List => { - println!("Listing worktrees."); - } - WorktreeCommands::Remove { title } => { - println!("Removing worktree for '{}'.", title); - } - }, + Some(Commands::Rfc { command }) => { + handle_rfc_command(command).await?; + } + Some(Commands::Worktree { command }) => { + handle_local_worktree_command(command).await?; + } Some(Commands::Pr { command }) => match command { PrCommands::Create { title } => { println!("Creating PR: {}", title); @@ -3918,3 +3979,183 @@ async fn handle_reminder_command(command: ReminderCommands) -> Result<()> { } Ok(()) } + +/// Handle RFC subcommands (RFC 0061) +async fn handle_rfc_command(command: RfcCommands) -> Result<()> { + let mut state = get_project_state()?; + + match command { + RfcCommands::Create { title, problem, source_spike } => { + let mut args = json!({ "title": title }); + if let Some(p) = problem { + args["problem"] = json!(p); + } + if let Some(s) = source_spike { + args["source_spike"] = json!(s); + } + match blue_mcp::handlers::rfc::handle_create(&mut state, &args) { + Ok(result) => { + if let Some(msg) = result.get("message").and_then(|v| v.as_str()) { + println!("{}", msg); + } + if let Some(file) = result.get("file").and_then(|v| v.as_str()) { + println!("File: {}", file); + } + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + } + RfcCommands::List { status } => { + let args = match status { + Some(s) => json!({ "status": s }), + None => json!({}), + }; + match blue_mcp::handlers::rfc::handle_list(&state, &args) { + Ok(result) => { + if let Some(rfcs) = result.get("rfcs").and_then(|v| v.as_array()) { + if rfcs.is_empty() { + println!("No RFCs found."); + } else { + for r in rfcs { + let number = r.get("number").and_then(|v| v.as_i64()).unwrap_or(0); + let title = r.get("title").and_then(|v| v.as_str()).unwrap_or("?"); + let status = r.get("status").and_then(|v| v.as_str()).unwrap_or("?"); + println!(" {:04} {} [{}]", number, title, status); + } + } + } + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + } + RfcCommands::Get { title } => { + let args = json!({ "title": title }); + match blue_mcp::handlers::rfc::handle_get(&state, &args) { + Ok(result) => { + println!("{}", serde_json::to_string_pretty(&result)?); + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + } + RfcCommands::Status { title, set } => { + let args = json!({ "title": title, "status": set }); + match blue_mcp::handlers::rfc::handle_update_status(&state, &args) { + Ok(result) => { + if let Some(msg) = result.get("message").and_then(|v| v.as_str()) { + println!("{}", msg); + } + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + } + RfcCommands::Plan { title, task } => { + let args = json!({ "title": title, "tasks": task }); + match blue_mcp::handlers::rfc::handle_plan(&state, &args) { + Ok(result) => { + if let Some(msg) = result.get("message").and_then(|v| v.as_str()) { + println!("{}", msg); + } + if let Some(file) = result.get("plan_file").and_then(|v| v.as_str()) { + println!("Plan file: {}", file); + } + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + } + RfcCommands::Complete { title } => { + let args = json!({ "title": title }); + match blue_mcp::handlers::rfc::handle_complete(&state, &args) { + Ok(result) => { + if let Some(msg) = result.get("message").and_then(|v| v.as_str()) { + println!("{}", msg); + } + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + } + } + Ok(()) +} + +/// Handle local worktree subcommands (RFC 0061) +async fn handle_local_worktree_command(command: WorktreeCommands) -> Result<()> { + let state = get_project_state()?; + + match command { + WorktreeCommands::Create { title } => { + let args = json!({ "rfc_title": title }); + match blue_mcp::handlers::worktree::handle_create(&state, &args) { + Ok(result) => { + if let Some(msg) = result.get("message").and_then(|v| v.as_str()) { + println!("{}", msg); + } + if let Some(path) = result.get("worktree_path").and_then(|v| v.as_str()) { + println!("Worktree: {}", path); + } + if let Some(branch) = result.get("branch").and_then(|v| v.as_str()) { + println!("Branch: {}", branch); + } + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + } + WorktreeCommands::List => { + match blue_mcp::handlers::worktree::handle_list(&state) { + Ok(result) => { + if let Some(worktrees) = result.get("worktrees").and_then(|v| v.as_array()) { + if worktrees.is_empty() { + println!("No worktrees found."); + } else { + for w in worktrees { + let name = w.get("name").and_then(|v| v.as_str()).unwrap_or("?"); + let branch = w.get("branch").and_then(|v| v.as_str()).unwrap_or("?"); + let path = w.get("path").and_then(|v| v.as_str()).unwrap_or("?"); + println!(" {} ({}) -> {}", name, branch, path); + } + } + } + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + } + WorktreeCommands::Remove { title } => { + let args = json!({ "name": title }); + match blue_mcp::handlers::worktree::handle_remove(&state, &args) { + Ok(result) => { + if let Some(msg) = result.get("message").and_then(|v| v.as_str()) { + println!("{}", msg); + } + } + Err(e) => { + eprintln!("Error: {}", e); + std::process::exit(1); + } + } + } + } + Ok(()) +}