feat: RFC 0061 Phase 3 - CLI parity for RFC and worktree commands

Add handle_rfc_command() and handle_local_worktree_command() functions
that call shared MCP handlers directly, avoiding code duplication.

RFC commands: create, list, get, status, plan, complete
Worktree commands: create, list, remove

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Garcia 2026-02-26 08:48:01 -05:00
parent 016c05e1ed
commit aec859b22e

View file

@ -590,14 +590,46 @@ enum RfcCommands {
Create { Create {
/// RFC title /// RFC title
title: String, title: String,
/// Problem statement
#[arg(long)]
problem: Option<String>,
/// Source spike (links RFC to spike)
#[arg(long)]
source_spike: Option<String>,
},
/// List all RFCs
List {
/// Filter by status (draft, accepted, in-progress, implemented)
#[arg(long)]
status: Option<String>,
},
/// 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 /// Create a plan for an RFC
Plan { Plan {
/// RFC title /// RFC title
title: String, title: String,
/// Tasks (can be specified multiple times)
#[arg(long, num_args = 1..)]
task: Vec<String>,
}, },
/// Get RFC details /// Mark RFC as complete
Get { Complete {
/// RFC title /// RFC title
title: String, title: String,
}, },
@ -870,9 +902,45 @@ async fn tokio_main() -> Result<()> {
} }
match cli.command { match cli.command {
None | Some(Commands::Status) => { None => {
println!("{}", blue_core::voice::welcome()); 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 }) => { Some(Commands::Init { force }) => {
let cwd = std::env::current_dir()?; let cwd = std::env::current_dir()?;
let blue_dir = cwd.join(".blue"); let blue_dir = cwd.join(".blue");
@ -907,8 +975,23 @@ async fn tokio_main() -> Result<()> {
println!(" blue status"); println!(" blue status");
} }
Some(Commands::Next) => { Some(Commands::Next) => {
println!("Looking at what's ready. One moment."); let state = get_project_state()?;
// TODO: Implement next 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 { .. }) => { Some(Commands::Mcp { .. }) => {
blue_mcp::run().await?; blue_mcp::run().await?;
@ -922,34 +1005,12 @@ async fn tokio_main() -> Result<()> {
Some(Commands::Session { command }) => { Some(Commands::Session { command }) => {
handle_session_command(command).await?; handle_session_command(command).await?;
} }
Some(Commands::Rfc { command }) => match command { Some(Commands::Rfc { command }) => {
RfcCommands::Create { title } => { handle_rfc_command(command).await?;
println!("{}", blue_core::voice::success( }
&format!("Created RFC '{}'", title), Some(Commands::Worktree { command }) => {
Some("Want me to help fill in the details?"), handle_local_worktree_command(command).await?;
)); }
}
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::Pr { command }) => match command { Some(Commands::Pr { command }) => match command {
PrCommands::Create { title } => { PrCommands::Create { title } => {
println!("Creating PR: {}", title); println!("Creating PR: {}", title);
@ -3918,3 +3979,183 @@ async fn handle_reminder_command(command: ReminderCommands) -> Result<()> {
} }
Ok(()) 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(())
}