1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// SPDX-License-Identifier: GPL-3.0-only
mod entity;
mod install;
mod root;
mod skills;
mod slice;
use std::path::PathBuf;
use clap::{Parser, Subcommand};
/// doctrine — project tooling.
#[derive(Parser)]
#[command(name = "doctrine", about = "doctrine CLI")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Install doctrine files into a project.
Install {
/// Explicit project root (default: auto-detect by walking up
/// from CWD looking for .git, .jj, .project, etc.).
#[arg(short = 'p', long)]
path: Option<PathBuf>,
/// Print the plan and exit without making changes.
#[arg(long)]
dry_run: bool,
/// Skip the confirmation prompt.
#[arg(short = 'y', long)]
yes: bool,
},
/// Manage agent skills.
Skills {
#[command(subcommand)]
command: SkillsCommand,
},
/// Create and list slices — the unit of intentional change.
Slice {
#[command(subcommand)]
command: SliceCommand,
},
}
#[derive(Subcommand)]
enum SliceCommand {
/// Allocate the next id and scaffold a new slice.
New {
/// Slice title (prompted for if omitted).
title: Option<String>,
/// Explicit slug (default: derived from the title).
#[arg(long)]
slug: Option<String>,
/// Explicit project root (default: auto-detect).
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
/// Scaffold a design-doc sibling into an existing slice.
Design {
/// Slice id to attach the design doc to.
id: u32,
/// Explicit project root (default: auto-detect).
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
/// List slices by id: id, status, slug, title.
List {
/// Filter to a single status.
#[arg(long)]
status: Option<String>,
/// Explicit project root (default: auto-detect).
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum SkillsCommand {
/// List available skills and their install status.
List {
/// Agent to report status for (default: claude).
#[arg(short = 'a', long)]
agent: Option<String>,
/// Only show skills already installed.
#[arg(long)]
installed: bool,
},
/// Install skills into agents.
Install {
/// Explicit project root (default: auto-detect).
#[arg(short = 'p', long)]
path: Option<PathBuf>,
/// Target agent(s); repeatable. Default: auto-detect claude.
#[arg(short = 'a', long)]
agent: Vec<String>,
/// Skill id(s) to install; repeatable. Default: all.
#[arg(short = 's', long)]
skill: Vec<String>,
/// Domain(s) to install; repeatable. Default: all.
#[arg(short = 'd', long)]
domain: Vec<String>,
/// Install to the user directory instead of the project.
#[arg(short = 'g', long)]
global: bool,
/// Print the plan and exit without making changes.
#[arg(long)]
dry_run: bool,
/// Skip the confirmation prompt.
#[arg(short = 'y', long)]
yes: bool,
},
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Install { path, dry_run, yes } => install::run(path, dry_run, yes),
Command::Skills { command } => match command {
SkillsCommand::List { agent, installed } => {
skills::run_list(agent.as_deref(), installed)
}
SkillsCommand::Install {
path,
agent,
skill,
domain,
global,
dry_run,
yes,
} => skills::run_install(path, &agent, &skill, &domain, global, dry_run, yes),
},
Command::Slice { command } => match command {
SliceCommand::New { title, slug, path } => slice::run_new(path, title, slug),
SliceCommand::Design { id, path } => slice::run_design(path, id),
SliceCommand::List { status, path } => slice::run_list(path, status.as_deref()),
},
}
}