diff --git a/.blue/docs/rfcs/0061-cli-database-parity.draft.md b/.blue/docs/rfcs/0061-cli-database-parity.draft.md new file mode 100644 index 0000000..b56c1d3 --- /dev/null +++ b/.blue/docs/rfcs/0061-cli-database-parity.draft.md @@ -0,0 +1,299 @@ +# RFC 0061: CLI Database Parity + +| | | +|---|---| +| **Status** | Draft | +| **Date** | 2026-02-11 | +| **Relates To** | RFC 0003 (Per-Repo Structure), RFC 0057 (CLI Parity), RFC 0060 (Reliable Binary Installation) | + +--- + +## Summary + +Complete the CLI-MCP parity work by fixing stub commands to use the shared handler infrastructure already established in RFC 0057. + +## Problem Statement + +### The Pattern Already Exists + +RFC 0057 established the correct pattern for CLI commands: + +```rust +fn get_project_state() -> Result { + let cwd = std::env::current_dir()?; + let home = blue_core::detect_blue(&cwd)?; + let project = home.project_name.clone().unwrap_or_default(); + ProjectState::load(home, &project) +} + +async fn handle_adr_command(command: AdrCommands) -> Result<()> { + let mut state = get_project_state()?; + match command { + AdrCommands::Create { title } => { + let args = json!({ "title": title }); + match blue_mcp::handlers::adr::handle_create(&mut state, &args) { + // ... + } + } + } +} +``` + +This pattern: +1. Uses `get_project_state()` to load the database +2. Calls shared `blue_mcp::handlers::*` functions +3. Formats output for CLI consumption + +### Stub Commands That Don't Use This Pattern + +The following commands are stubs that just print messages: + +| Command | Current Behavior | Should Call | +|---------|-----------------|-------------| +| `blue init` | Prints welcome | `detect_blue()` + `ProjectState::load()` | +| `blue status` | Prints welcome | `blue_mcp::handlers::status` | +| `blue next` | Prints message | `blue_mcp::handlers::next` | +| `blue rfc create` | Prints message | `blue_mcp::handlers::rfc::handle_create` | +| `blue rfc get` | Prints message | `blue_mcp::handlers::rfc::handle_get` | +| `blue rfc plan` | Prints message | `blue_mcp::handlers::rfc::handle_plan` | +| `blue worktree create` | Prints message | `blue_mcp::handlers::worktree::handle_create` | +| `blue worktree list` | Prints message | `blue_mcp::handlers::worktree::handle_list` | +| `blue worktree remove` | Prints message | `blue_mcp::handlers::worktree::handle_remove` | +| `blue pr create` | Prints message | `blue_mcp::handlers::pr::handle_create` | +| `blue lint` | Prints message | `blue_mcp::handlers::lint::handle_lint` | + +### Commands Already Using Shared Handlers (RFC 0057) + +These are correctly implemented: + +| Command Group | Calls Into | +|---------------|------------| +| `blue dialogue *` | `blue_mcp::handlers::dialogue::*` | +| `blue adr *` | `blue_mcp::handlers::adr::*` | +| `blue spike *` | `blue_mcp::handlers::spike::*` | +| `blue audit *` | `blue_mcp::handlers::audit_doc::*` | +| `blue prd *` | `blue_mcp::handlers::prd::*` | +| `blue reminder *` | `blue_mcp::handlers::reminder::*` | + +## Proposal + +### 1. Implement `blue init` + +```rust +Some(Commands::Init) => { + let cwd = std::env::current_dir()?; + + if cwd.join(".blue").exists() { + println!("Blue already initialized."); + return Ok(()); + } + + // detect_blue auto-creates .blue/ per RFC 0003 + let home = blue_core::detect_blue(&cwd)?; + + // Load state to ensure database is created with schema + let project = home.project_name.clone().unwrap_or_default(); + let _state = ProjectState::load(home.clone(), &project)?; + + println!("{}", blue_core::voice::welcome()); + println!(); + println!("Initialized Blue:"); + println!(" Database: {}", home.db_path.display()); + println!(" Docs: {}", home.docs_path.display()); +} +``` + +### 2. Wire Up Core Commands + +Replace the stub implementations with calls to shared handlers: + +```rust +Some(Commands::Status) => { + let state = get_project_state()?; + let args = json!({}); + let result = blue_mcp::handlers::status::handle_status(&state, &args)?; + // Format for CLI output + println!("{}", format_status(&result)); +} + +Some(Commands::Rfc { command }) => { + handle_rfc_command(command).await?; +} +``` + +Add handler function following RFC 0057 pattern: + +```rust +async fn handle_rfc_command(command: RfcCommands) -> Result<()> { + let mut state = get_project_state()?; + + match command { + RfcCommands::Create { title } => { + let args = json!({ "title": title }); + 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); + } + } + } + // ... other subcommands + } + Ok(()) +} +``` + +### 3. Missing MCP Handler Functions + +Some handlers exist but lack the functions needed. Check and add: + +| Handler File | Needed Functions | +|--------------|------------------| +| `rfc.rs` | `handle_create`, `handle_get`, `handle_plan` | +| `worktree.rs` | `handle_create`, `handle_list`, `handle_remove` | +| `pr.rs` | `handle_create` | +| `lint.rs` | `handle_lint` | + +The server.rs has these as methods on `BlueServer`. They need to be refactored into standalone functions in the handler modules, similar to how RFC 0057 handlers work. + +### 4. Handler Refactoring Strategy + +Currently, MCP server methods look like: + +```rust +// In server.rs +fn handle_rfc_create(&mut self, args: &Option) -> Result { + self.ensure_state(args)?; // Loads state + let state = self.state.as_ref().unwrap(); + // ... implementation +} +``` + +Refactor to: + +```rust +// In handlers/rfc.rs +pub fn handle_create(state: &mut ProjectState, args: &Value) -> Result { + // ... implementation (no self, takes state directly) +} + +// In server.rs +fn handle_rfc_create(&mut self, args: &Option) -> Result { + self.ensure_state(args)?; + let state = self.state.as_mut().unwrap(); + let args = args.as_ref().cloned().unwrap_or(json!({})); + handlers::rfc::handle_create(state, &args) +} +``` + +This lets CLI call handlers directly without going through the MCP server. + +## Implementation Plan + +### Phase 1: `blue init` (Immediate) + +1. Implement `blue init` using `detect_blue()` + `ProjectState::load()` +2. Add `--force` flag to reinitialize +3. Print helpful output showing what was created + +### Phase 2: Extract Handler Functions + +For each handler file, extract the core logic into standalone functions: + +1. `handlers/rfc.rs`: `handle_create`, `handle_get`, `handle_plan`, `handle_update_status` +2. `handlers/worktree.rs`: `handle_create`, `handle_list`, `handle_remove` +3. `handlers/pr.rs`: `handle_create` +4. `handlers/lint.rs`: `handle_lint` +5. `handlers/status.rs` (new): `handle_status`, `handle_next` + +### Phase 3: Wire Up CLI Commands + +1. Add `handle_rfc_command()` following RFC 0057 pattern +2. Add `handle_worktree_command()` following RFC 0057 pattern +3. Add `handle_pr_command()` following RFC 0057 pattern +4. Implement `blue lint` calling shared handler +5. Implement `blue status` and `blue next` + +### Phase 4: Add Missing CLI Commands + +MCP has tools not yet exposed via CLI: + +| MCP Tool | Proposed CLI | +|----------|--------------| +| `blue_rfc_complete` | `blue rfc complete ` | +| `blue_rfc_validate` | `blue rfc validate <title>` | +| `blue_worktree_cleanup` | `blue worktree cleanup` | +| `blue_search` | Already exists: `blue search` | +| `blue_health_check` | `blue health` | +| `blue_sync` | `blue sync` | + +## Files Changed + +| File | Change | +|------|--------| +| `apps/blue-cli/src/main.rs` | Implement init, wire up commands | +| `crates/blue-mcp/src/handlers/rfc.rs` | Extract standalone functions | +| `crates/blue-mcp/src/handlers/worktree.rs` | Extract standalone functions | +| `crates/blue-mcp/src/handlers/pr.rs` | Extract standalone functions | +| `crates/blue-mcp/src/handlers/lint.rs` | Extract standalone functions | +| `crates/blue-mcp/src/handlers/status.rs` | New file for status/next | +| `crates/blue-mcp/src/server.rs` | Call extracted functions | + +## Architecture: No Code Duplication + +``` +┌─────────────────┐ ┌─────────────────┐ +│ CLI (clap) │ │ MCP Server │ +│ │ │ │ +│ get_project_ │ │ ensure_state() │ +│ state() │ │ │ +└────────┬────────┘ └────────┬────────┘ + │ │ + │ ProjectState │ ProjectState + ▼ ▼ +┌─────────────────────────────────────────────┐ +│ blue_mcp::handlers::* │ +│ │ +│ rfc::handle_create(state, args) │ +│ worktree::handle_create(state, args) │ +│ dialogue::handle_create(state, args) │ +│ ... │ +└─────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────┐ +│ blue_core::* │ +│ │ +│ DocumentStore, ProjectState, Rfc, etc. │ +└─────────────────────────────────────────────┘ +``` + +## Test Plan + +- [ ] `blue init` creates `.blue/` and `blue.db` +- [ ] `blue init` is idempotent +- [ ] `blue rfc create "Test"` creates RFC in database +- [ ] `blue rfc list` shows RFCs (add this command) +- [ ] `blue worktree create "Test"` creates git worktree +- [ ] `blue worktree list` shows worktrees +- [ ] `blue status` shows accurate project state +- [ ] `blue lint` runs validation checks +- [ ] All commands work in new project after `blue init` + +## Migration + +No migration needed. The change is purely internal refactoring to share code between CLI and MCP. + +--- + +*"One implementation, two interfaces."* + +-- Blue diff --git a/apps/blue-cli/src/main.rs b/apps/blue-cli/src/main.rs index 9b5aad4..c663500 100644 --- a/apps/blue-cli/src/main.rs +++ b/apps/blue-cli/src/main.rs @@ -195,7 +195,11 @@ struct Cli { #[derive(Subcommand)] enum Commands { /// Welcome home - initialize Blue in this directory - Init, + Init { + /// Reinitialize even if .blue/ already exists + #[arg(long)] + force: bool, + }, /// Get project status Status, @@ -869,9 +873,38 @@ async fn tokio_main() -> Result<()> { None | Some(Commands::Status) => { println!("{}", blue_core::voice::welcome()); } - Some(Commands::Init) => { + Some(Commands::Init { force }) => { + let cwd = std::env::current_dir()?; + let blue_dir = cwd.join(".blue"); + + // Check if already initialized + if blue_dir.exists() && !force { + println!("Blue already initialized in this directory."); + println!(" {}", blue_dir.display()); + println!(); + println!("Use --force to reinitialize."); + return Ok(()); + } + + // detect_blue auto-creates .blue/ per RFC 0003 + let home = blue_core::detect_blue(&cwd) + .map_err(|e| anyhow::anyhow!("Failed to initialize: {}", e))?; + + // Load state to ensure database is created with schema + let project = home.project_name.clone().unwrap_or_else(|| "default".to_string()); + let _state = blue_core::ProjectState::load(home.clone(), &project) + .map_err(|e| anyhow::anyhow!("Failed to create database: {}", e))?; + println!("{}", blue_core::voice::welcome()); - // TODO: Initialize .blue directory + println!(); + println!("Initialized Blue:"); + println!(" Root: {}", home.root.display()); + println!(" Database: {}", home.db_path.display()); + println!(" Docs: {}", home.docs_path.display()); + println!(); + println!("Next steps:"); + println!(" blue rfc create \"My First RFC\""); + println!(" blue status"); } Some(Commands::Next) => { println!("Looking at what's ready. One moment.");