feat: RFC 0061 Phase 1 - implement blue init
- blue init now creates .blue/ directory and SQLite database - Added --force flag to reinitialize existing projects - Prints helpful output showing created paths - Idempotent: running twice shows "already initialized" message Phase 1 of RFC 0061 (CLI Database Parity). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
093abcf4a1
commit
69ba55e5a9
2 changed files with 335 additions and 3 deletions
299
.blue/docs/rfcs/0061-cli-database-parity.draft.md
Normal file
299
.blue/docs/rfcs/0061-cli-database-parity.draft.md
Normal file
|
|
@ -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<ProjectState> {
|
||||
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<Value>) -> Result<Value, ServerError> {
|
||||
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<Value, ServerError> {
|
||||
// ... implementation (no self, takes state directly)
|
||||
}
|
||||
|
||||
// In server.rs
|
||||
fn handle_rfc_create(&mut self, args: &Option<Value>) -> Result<Value, ServerError> {
|
||||
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 <title>` |
|
||||
| `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
|
||||
|
|
@ -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.");
|
||||
|
|
|
|||
Loading…
Reference in a new issue