From ba0efeb6229186bc789f8d43132a022683cec9f4 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Sun, 22 Feb 2026 18:29:17 -0800 Subject: [PATCH] feat(nbd): add claude-md command [fc444f] Adds `nbd claude-md` subcommand that prints a ready-to-paste CLAUDE.md snippet for adopting nbd in any project. The snippet is embedded at compile time via `include_str!` from `src/claude_md_snippet.md`, so it stays in sync with the installed binary automatically. - `nbd claude-md` prints raw markdown (suitable for `>> CLAUDE.md`) - `nbd claude-md --json` outputs `{"snippet": "..."}` for programmatic use - Works without a `.nbd/` store present (no find_nbd_root call) - 4 new integration tests covering all specified behaviours Co-Authored-By: Claude Sonnet 4.6 --- nbd/.nbd/tickets/fc444f.json | 3 +- nbd/README.md | 15 ++++++ nbd/src/claude_md_snippet.md | 53 ++++++++++++++++++++ nbd/src/main.rs | 44 +++++++++++++++++ nbd/tests/integration.rs | 93 ++++++++++++++++++++++++++++++++++++ 5 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 nbd/src/claude_md_snippet.md diff --git a/nbd/.nbd/tickets/fc444f.json b/nbd/.nbd/tickets/fc444f.json index 16a8121..0c51654 100644 --- a/nbd/.nbd/tickets/fc444f.json +++ b/nbd/.nbd/tickets/fc444f.json @@ -1,9 +1,8 @@ { - "id": "fc444f", "title": "nbd claude-md command", "body": "Add `nbd claude-md` subcommand that prints a ready-to-paste CLAUDE.md snippet for adopting `nbd` in any project. The snippet content is maintained as a source file and baked into the binary at compile time via `include_str!`.\n\n## Motivation\n\nEvery project that wants to use `nbd` needs the same boilerplate in its CLAUDE.md: what `nbd` is, how to invoke it, the create/update/done workflow, and the key guidelines. Without this command, that block has to be written by hand and tends to drift out of date as `nbd` evolves. By owning the canonical snippet in the `nbd` source tree and embedding it into the binary, the snippet stays in sync with the tool automatically — projects just run `nbd claude-md >> CLAUDE.md` when they adopt or upgrade `nbd`.\n\n## Snippet file\n\nCreate `src/claude_md_snippet.md`. This is a plain markdown file, checked into the repository, that contains the CLAUDE.md section any adopting project should paste in. It should cover:\n\n- One-sentence description of `nbd`.\n- Initialisation (`nbd init`).\n- The four core commands with examples (`create`, `list`, `read`, `update`).\n- The `nbd ready` command for finding unblocked tickets.\n- The workflow (create before starting → set in_progress → set done).\n- Guidelines: always `--json`, priority scale, type choices, dep usage.\n\nThe file is written for a project where `nbd` is installed in PATH (i.e. not via `cargo run`). It should be self-contained — no references to the `nbd` crate internals.\n\n## Binary embedding\n\nIn `main.rs`, embed the snippet at compile time:\n\n```rust\nconst CLAUDE_MD_SNIPPET: &str = include_str!(\"claude_md_snippet.md\");\n```\n\n`include_str!` resolves paths relative to the source file (`src/`), so this looks for `src/claude_md_snippet.md`. Cargo rebuilds the binary automatically when the file changes.\n\n## Command implementation\n\nAdd `ClaudeMd` variant to `Commands` in `main.rs`:\n\n```rust\n/// Print a CLAUDE.md snippet for adopting nbd in a project.\nClaudeMd,\n```\n\nThe `--json` global flag applies:\n- Without `--json`: `print!(\"{CLAUDE_MD_SNIPPET}\")` — raw markdown, suitable for redirect (`nbd claude-md >> CLAUDE.md`).\n- With `--json`: output `{\"snippet\": \"\"}` — for programmatic consumption.\n\nNo store access needed — this command is pure output from the embedded constant. It does not call `find_nbd_root()`.\n\n## No display.rs changes needed\n\nThe output is a single `print!` call in the command handler. No tabular formatting.\n\n## Tests\n\nIntegration tests (`tests/integration.rs`):\n- `nbd claude-md` exits zero and stdout is non-empty.\n- stdout contains key strings (`\"nbd\"`, `\"CLAUDE.md\"` or similar section header, `\"--json\"`).\n- `nbd claude-md --json` exits zero and stdout is valid JSON with a `\"snippet\"` key whose value is a non-empty string.\n- `nbd claude-md` works even when run from a directory with no `.nbd/` (no `find_nbd_root` call).\n\n## Files touched\n- `src/claude_md_snippet.md` — new file; the canonical snippet content\n- `src/main.rs` — `include_str!` constant, `ClaudeMd` command variant and handler\n- `tests/integration.rs` — integration tests\n- `README.md` — mention `nbd claude-md` in the Usage section", "priority": 6, - "status": "todo", + "status": "done", "dependencies": [], "ticket_type": "feature" } \ No newline at end of file diff --git a/nbd/README.md b/nbd/README.md index 15c7ef3..9bc0026 100644 --- a/nbd/README.md +++ b/nbd/README.md @@ -111,6 +111,19 @@ nbd ready nbd ready --json ``` +### Print a CLAUDE.md snippet + +Print a ready-to-paste section for adopting `nbd` in any project's CLAUDE.md: + +```sh +nbd claude-md # raw markdown, suitable for redirect +nbd claude-md >> CLAUDE.md # append to your project's CLAUDE.md +nbd claude-md --json # machine-readable {"snippet": "..."} +``` + +The snippet is embedded in the binary at compile time, so it is always in sync +with the installed version of `nbd`. + ### Migrate ticket files Re-serialise all ticket files through the current schema. Use this after @@ -136,6 +149,8 @@ cargo run -- read cargo run -- update --status in_progress cargo run -- archive cargo run -- list --json +cargo run -- claude-md +cargo run -- claude-md --json ``` ## Testing diff --git a/nbd/src/claude_md_snippet.md b/nbd/src/claude_md_snippet.md new file mode 100644 index 0000000..6703039 --- /dev/null +++ b/nbd/src/claude_md_snippet.md @@ -0,0 +1,53 @@ +## Task Tracking with nbd + +`nbd` is a CLI tool for managing work tickets, designed for agent workflows. + +### Initialisation + +```sh +nbd init +``` + +Run once in the project root. Creates `.nbd/tickets/`. Safe to run multiple times. + +### Core commands + +```sh +# Create a new ticket +nbd create --title "Add OAuth login" --type feature --priority 7 + +# List all open tickets (sorted by priority) +nbd list + +# Read a specific ticket +nbd read + +# Update a ticket +nbd update --status in_progress +nbd update --status done +``` + +### Finding what to work on + +```sh +# All tickets that are unblocked and ready to start +nbd ready + +# The single highest-priority unblocked ticket +nbd next +``` + +### Workflow + +1. **Before starting** — create a ticket: `nbd create --title "..." --json` +2. **When starting** — mark it in progress: `nbd update --status in_progress` +3. **When done** — mark it complete: `nbd update --status done` + +### Guidelines + +- **Always pass `--json`** to every command for structured, unambiguous output. +- Use `jq` to parse and transform JSON output when needed. +- Priority scale 0–10: use **7–9** for bugs, **5** for normal tasks, **3** for nice-to-haves. +- `--type` choices: `project`, `feature`, `task`, `bug`. +- Use `--deps id1,id2` to express blockers — tickets that must be done first. +- Create tickets *before* starting non-trivial tasks, not after. diff --git a/nbd/src/main.rs b/nbd/src/main.rs index 82a91a0..3521ff6 100644 --- a/nbd/src/main.rs +++ b/nbd/src/main.rs @@ -9,6 +9,15 @@ mod filter; mod store; mod ticket; +/// The CLAUDE.md snippet embedded at compile time from `src/claude_md_snippet.md`. +/// +/// Projects adopting `nbd` can append this to their own CLAUDE.md: +/// +/// ```sh +/// nbd claude-md >> CLAUDE.md +/// ``` +const CLAUDE_MD_SNIPPET: &str = include_str!("claude_md_snippet.md"); + #[cfg(test)] mod tests; @@ -163,6 +172,21 @@ enum Commands { id: String, }, + /// Print a CLAUDE.md snippet for adopting nbd in a project. + /// + /// The snippet is baked into the binary at compile time and covers + /// the nbd workflow, core commands, and usage guidelines. + /// + /// Append the snippet to your project's CLAUDE.md: + /// + /// ```sh + /// nbd claude-md >> CLAUDE.md + /// ``` + /// + /// With `--json`, outputs `{"snippet": "..."}` for programmatic use. + /// Does not require a `.nbd/` store to be present. + ClaudeMd, + /// Update fields of an existing ticket and print the result. /// /// Only the flags you supply are changed; all other fields retain their @@ -230,6 +254,8 @@ async fn dispatch(cli: Cli) -> store::Result<()> { Commands::Archive { id } => cmd_archive(id, cli.json).await, + Commands::ClaudeMd => cmd_claude_md(cli.json), + Commands::Read { id } => cmd_read(id, cli.json).await, Commands::List { filter, all } => cmd_list(filter, all, cli.json).await, @@ -590,6 +616,24 @@ async fn cmd_archive(id: String, json: bool) -> store::Result<()> { Ok(()) } +/// Print the embedded CLAUDE.md snippet to stdout. +/// +/// With `json = true`, wraps the snippet in `{"snippet": "..."}` for +/// programmatic consumption. Without `--json`, emits the raw markdown so the +/// caller can redirect it directly into a CLAUDE.md file. +/// +/// This command never calls [`find_nbd_root`] — it works even when no `.nbd/` +/// store exists. +fn cmd_claude_md(json: bool) -> store::Result<()> { + if json { + let value = serde_json::json!({ "snippet": CLAUDE_MD_SNIPPET }); + println!("{}", serde_json::to_string_pretty(&value)?); + } else { + print!("{CLAUDE_MD_SNIPPET}"); + } + Ok(()) +} + /// Update the specified fields of an existing ticket, persist it, and print it. /// /// Only the flags explicitly passed on the command line are applied; all other diff --git a/nbd/tests/integration.rs b/nbd/tests/integration.rs index 57800e7..74285d4 100644 --- a/nbd/tests/integration.rs +++ b/nbd/tests/integration.rs @@ -1255,6 +1255,99 @@ fn next_json_includes_id_field() { assert_eq!(id.len(), 6, "id should be 6 characters"); } +// ── nbd claude-md tests ─────────────────────────────────────────────────────── + +/// `nbd claude-md` exits zero and stdout is non-empty. +#[test] +fn claude_md_exits_zero_with_output() { + let tmp = tempfile::tempdir().expect("failed to create tempdir"); + let output = std::process::Command::new(env!("CARGO_BIN_EXE_nbd")) + .args(["claude-md"]) + .current_dir(tmp.path()) + .output() + .expect("failed to spawn nbd"); + + assert!( + output.status.success(), + "claude-md should exit zero, stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + !output.stdout.is_empty(), + "claude-md stdout should be non-empty" + ); +} + +/// `nbd claude-md` stdout contains key strings (`nbd`, `--json`). +#[test] +fn claude_md_contains_key_content() { + let tmp = tempfile::tempdir().expect("failed to create tempdir"); + let output = std::process::Command::new(env!("CARGO_BIN_EXE_nbd")) + .args(["claude-md"]) + .current_dir(tmp.path()) + .output() + .expect("failed to spawn nbd"); + + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("nbd"), + "output should mention 'nbd', got: {stdout}" + ); + assert!( + stdout.contains("--json"), + "output should mention '--json', got: {stdout}" + ); +} + +/// `nbd claude-md --json` exits zero and stdout is valid JSON with a `snippet` key. +#[test] +fn claude_md_json_flag_produces_valid_json() { + let tmp = tempfile::tempdir().expect("failed to create tempdir"); + let output = std::process::Command::new(env!("CARGO_BIN_EXE_nbd")) + .args(["claude-md", "--json"]) + .current_dir(tmp.path()) + .output() + .expect("failed to spawn nbd"); + + assert!( + output.status.success(), + "claude-md --json should exit zero, stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("claude-md --json output should be valid JSON"); + assert!( + parsed.get("snippet").is_some(), + "JSON should have a 'snippet' key, got: {stdout}" + ); + let snippet = parsed["snippet"].as_str().unwrap(); + assert!(!snippet.is_empty(), "snippet value should be non-empty"); +} + +/// `nbd claude-md` works even from a directory with no `.nbd/` store. +#[test] +fn claude_md_works_without_nbd_store() { + let tmp = tempfile::tempdir().expect("failed to create tempdir"); + // Deliberately do NOT create a .nbd/ directory. + let output = std::process::Command::new(env!("CARGO_BIN_EXE_nbd")) + .args(["claude-md"]) + .current_dir(tmp.path()) + .output() + .expect("failed to spawn nbd"); + + assert!( + output.status.success(), + "claude-md should succeed even without a .nbd/ store, stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + !output.stdout.is_empty(), + "claude-md should produce output even without .nbd/" + ); +} + /// `nbd next --filter priority=99 --json` returns `{"next": null}` when no /// ticket has the requested priority. #[test]