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)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Welcome home - initialize Blue in this directory
|
/// Welcome home - initialize Blue in this directory
|
||||||
Init,
|
Init {
|
||||||
|
/// Reinitialize even if .blue/ already exists
|
||||||
|
#[arg(long)]
|
||||||
|
force: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// Get project status
|
/// Get project status
|
||||||
Status,
|
Status,
|
||||||
|
|
@ -869,9 +873,38 @@ async fn tokio_main() -> Result<()> {
|
||||||
None | Some(Commands::Status) => {
|
None | Some(Commands::Status) => {
|
||||||
println!("{}", blue_core::voice::welcome());
|
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());
|
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) => {
|
Some(Commands::Next) => {
|
||||||
println!("Looking at what's ready. One moment.");
|
println!("Looking at what's ready. One moment.");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue