use std::path::{Path, PathBuf};
use crate::cli::{SkillArgs, SkillTarget};
use crate::context::Context;
use crate::error::{CliError, CliResult, ExitCode};
const MANAGED_MARKER: &str = "dbmd-managed-skill:v1";
pub fn install(ctx: &Context, args: &SkillArgs) -> CliResult {
let home = home_dir()?;
let target = resolve_target(args.target, &home);
let (dir, file) = paths_for(target, &home);
std::fs::create_dir_all(&dir).map_err(|e| io_err("creating skill directory", &dir, e))?;
let body = match target {
SkillTarget::ClaudeCode => CLAUDE_CODE_SKILL,
SkillTarget::Codex => CODEX_SKILL,
};
ensure_installable(&file)?;
std::fs::write(&file, body).map_err(|e| io_err("writing skill", &file, e))?;
emit(ctx, target, &file, "installed");
Ok(())
}
pub fn uninstall(ctx: &Context, args: &SkillArgs) -> CliResult {
let home = home_dir()?;
let target = resolve_target(args.target, &home);
let (dir, file) = paths_for(target, &home);
if !file.exists() {
emit(ctx, target, &file, "noop");
return Ok(());
}
ensure_managed(&file, "uninstall")?;
let removed = match target {
SkillTarget::ClaudeCode => {
std::fs::remove_file(&file).and_then(|()| match std::fs::remove_dir(&dir) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::DirectoryNotEmpty => Ok(()),
Err(e) => Err(e),
})
}
SkillTarget::Codex => std::fs::remove_file(&file),
};
removed.map_err(|e| io_err("removing skill", &file, e))?;
emit(ctx, target, &file, "uninstalled");
Ok(())
}
fn ensure_installable(file: &Path) -> CliResult {
if !file.exists() {
return Ok(());
}
ensure_managed(file, "install")
}
fn ensure_managed(file: &Path, action: &str) -> CliResult {
let body = std::fs::read_to_string(file).map_err(|e| io_err("reading skill", file, e))?;
if body.contains(MANAGED_MARKER) {
return Ok(());
}
Err(CliError::new(
ExitCode::Collision,
"SKILL_NOT_MANAGED",
format!(
"refusing to {action} unmanaged db.md skill file at {}",
file.display()
),
)
.with_hint("move the file aside, or remove it yourself if it is not managed by dbmd"))
}
fn resolve_target(explicit: Option<SkillTarget>, home: &Path) -> SkillTarget {
if let Some(t) = explicit {
return t;
}
if home.join(".claude").is_dir() {
return SkillTarget::ClaudeCode;
}
if home.join(".codex").is_dir() {
return SkillTarget::Codex;
}
SkillTarget::ClaudeCode
}
fn paths_for(target: SkillTarget, home: &Path) -> (PathBuf, PathBuf) {
match target {
SkillTarget::ClaudeCode => {
let dir = home.join(".claude").join("skills").join("db-md");
let file = dir.join("SKILL.md");
(dir, file)
}
SkillTarget::Codex => {
let dir = home.join(".codex").join("instructions");
let file = dir.join("db-md.md");
(dir, file)
}
}
}
fn home_dir() -> Result<PathBuf, CliError> {
std::env::var_os("HOME")
.filter(|h| !h.is_empty())
.map(PathBuf::from)
.ok_or_else(|| {
CliError::new(
ExitCode::Runtime,
"NO_HOME",
"cannot resolve the home directory ($HOME is unset)",
)
.with_hint("set $HOME so the skill can be written under ~/.claude or ~/.codex")
})
}
fn io_err(action: &str, path: &Path, e: std::io::Error) -> CliError {
CliError::new(
ExitCode::Runtime,
"IO_ERROR",
format!("{action} {}: {e}", path.display()),
)
}
fn emit(ctx: &Context, target: SkillTarget, file: &Path, action: &str) {
let target = target.as_str();
let path = file.display();
if ctx.json {
let out = serde_json::json!({
"target": target,
"path": path.to_string(),
"action": action,
});
println!("{out}");
return;
}
match action {
"installed" => println!(
"Installed the db.md skill for {target} at {path}.\n\
Start a new {target} session and it will know how to operate a db.md store with `dbmd`."
),
"uninstalled" => println!("Removed the db.md skill for {target} ({path})."),
"noop" => println!("No db.md skill installed for {target} ({path}); nothing to remove."),
other => println!("{other}: {target} ({path})"),
}
}
const CLAUDE_CODE_SKILL: &str = r##"---
name: db-md
description: Operate a db.md store — the open database in plain files — with the `dbmd` CLI. Use when reading, writing, searching, validating, or curating a folder that has a DB.md at its root. Run `dbmd spec` for the full contract.
---
<!-- dbmd-managed-skill:v1 -->
# db.md (the `dbmd` CLI)
You have the `dbmd` binary on PATH. It operates a **db.md store**: a database
that is a plain directory — raw evidence in `sources/`, atomic typed data in
`records/`, curator-synthesized narrative in `wiki/`, all governed by a single
`DB.md` at the root. `dbmd` is deterministic file/data plumbing; **you are the
curator** — the reasoning, synthesis, and judgment are yours.
**Before anything else: load the contract once per session.**
```
dbmd spec # prints the canonical SPEC — the curator contract
```
Then read the store's own `DB.md` for its identity, policies, and schemas;
`DB.md` overrides defaults, so read it before you write.
## Cheat sheet (grouped by session phase)
```
# Open — load the standard, then this store's rules
dbmd spec # the contract (once per session)
dbmd fm get DB.md scope # this store's identity / policies / schemas
# Warm up — orient
dbmd tree # the directory at a glance
dbmd stats # counts, sizes, orphans, top types
dbmd index show # the curated root catalog
# Read — find and hydrate context (every command takes --json)
dbmd search "(renewal|contract|ARR)" --in records # ripgrep; the regex IS your query expansion (no embeddings)
dbmd query --type contact --where company=Acme # structured frontmatter query via the sidecar
dbmd graph neighborhood records/contacts/sarah-chen --hops 2 # context in one call
dbmd links records/contacts/sarah-chen # who points here (blast radius)
# Write — create and connect (frontmatter is composed for you)
dbmd write records/meetings/standup.md --type meeting --summary "weekly sync"
dbmd fm set <file> <key>=<value> # update one field, atomically
dbmd link <from> <to> # append a wiki-link
# Validate — before you close
dbmd validate # the working set (changed files)
dbmd validate --all # full-store sweep
# Maintain / close — record what you did
dbmd index rebuild # repair the catalog if needed
dbmd log <kind> <object> -m "<note>" # append to the store timeline
```
## Output contract (memorize)
```
--json on every command # machine-parseable; errors print {"error":{code,message,hint}} on stderr
exit: 0 ok · 1 runtime · 2 usage · 3 not-a-store · 4 policy refusal · 5 collision · 6 validation-failed
```
The full, authoritative reference is always `dbmd spec`. This skill is a pointer,
not a copy — when in doubt, run `dbmd spec` and read the store's `DB.md`.
"##;
const CODEX_SKILL: &str = r##"<!-- dbmd-managed-skill:v1 -->
# db.md (the `dbmd` CLI)
You have the `dbmd` binary on PATH. It operates a **db.md store**: a database
that is a plain directory — `sources/` (raw evidence), `records/` (atomic typed
data), `wiki/` (curator synthesis), governed by a single `DB.md` at the root.
`dbmd` is deterministic file/data plumbing; you are the curator.
Load the contract once per session, then read the store's `DB.md`:
```
dbmd spec # the canonical SPEC — the curator contract
```
Most-used commands (every command takes `--json`):
```
# Orient
dbmd tree · dbmd stats · dbmd index show
# Read
dbmd search "(renewal|contract)" --in records
dbmd query --type contact --where company=Acme
dbmd graph neighborhood <seed> --hops 2
dbmd links <file>
# Write (frontmatter composed for you)
dbmd write <path> --type <t> --summary "<s>"
dbmd fm set <file> <key>=<value>
dbmd link <from> <to>
# Validate / record
dbmd validate # working set
dbmd validate --all # full sweep
dbmd log <kind> <object> -m "<note>"
```
Exit codes: 0 ok · 1 runtime · 2 usage · 3 not-a-store · 4 policy · 5 collision ·
6 validation-failed. Errors print `{"error":{...}}` on stderr under `--json`.
The full reference is always `dbmd spec`.
"##;