seshat_cli/args.rs
1//! Command-line argument definitions via `clap` derive.
2//!
3//! All CLI types live here so that `seshat-bin` stays thin — it only
4//! parses args via [`Cli::parse()`] and delegates to this crate.
5
6use std::path::PathBuf;
7
8use clap::{Parser, Subcommand, ValueEnum};
9
10use seshat_storage::DecisionState;
11
12/// Full version string including git hash: "0.1.0 (abc1234)".
13const VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), " (", env!("GIT_HASH"), ")");
14
15/// Seshat — the operating manual for your codebase, written for AI agents.
16#[derive(Debug, Parser)]
17#[command(
18 name = "seshat",
19 version = VERSION,
20 about = "The operating manual for your codebase — written for AI agents",
21 long_about = None,
22)]
23pub struct Cli {
24 /// The subcommand to execute.
25 #[command(subcommand)]
26 pub command: Command,
27}
28
29/// Top-level subcommands.
30#[derive(Debug, Subcommand)]
31pub enum Command {
32 /// Scan a project directory and display analysis report.
33 Scan {
34 /// Path to the project directory to scan.
35 path: PathBuf,
36
37 /// Show verbose output: skipped files, detector details, timing.
38 #[arg(long, short)]
39 verbose: bool,
40
41 /// Show only errors and final summary.
42 #[arg(long, short)]
43 quiet: bool,
44
45 /// Exclude submodules from scanning (they are scanned by default).
46 #[arg(long)]
47 exclude_submodules: bool,
48 },
49
50 /// Start the MCP server for AI agent connections.
51 Serve {
52 /// Repository directory path or project name.
53 /// Auto-detected from current working directory if omitted.
54 repo: Option<PathBuf>,
55
56 /// Host to bind the HTTP/SSE transport to (overrides config).
57 #[arg(long)]
58 host: Option<String>,
59
60 /// Port for the HTTP/SSE transport (overrides config).
61 #[arg(long)]
62 port: Option<u16>,
63
64 /// Log MCP tool calls to JSONL file for analysis.
65 /// Default: $XDG_DATA_HOME/seshat/call-log.jsonl
66 #[arg(long, value_name = "PATH", num_args = 0..=1, default_missing_value = "")]
67 call_log: Option<PathBuf>,
68 },
69
70 /// Show indexed projects, submodules, and database info.
71 Status {
72 /// Show full database paths and additional detail.
73 #[arg(long, short)]
74 verbose: bool,
75 },
76
77 /// Interactive convention review.
78 ///
79 /// On startup, compares the active branch's `last_scanned_commit` against
80 /// `git rev-parse HEAD` and runs an incremental sync to the current HEAD
81 /// before opening the TUI, so the review queue reflects the on-disk state.
82 Review {
83 /// Skip the pre-TUI freshness check and incremental sync.
84 ///
85 /// Use for emergency / debug access to the existing snapshot when
86 /// sync would be slow or undesirable. Implies the queue may be stale.
87 #[arg(long)]
88 no_sync: bool,
89 },
90
91 /// Generate MCP configuration for detected AI clients.
92 ///
93 /// Auto-detects installed AI coding clients. By default uses smart scope:
94 /// project-level config if it already exists, global config otherwise.
95 /// For JSON configs, offers to auto-patch with backup. For JSONC, shows
96 /// a copy-paste snippet.
97 Init {
98 /// Specific client to configure. Auto-detects all if omitted.
99 /// Supported: claude-code, claude-desktop, opencode, cursor
100 client: Option<String>,
101
102 /// Always use project-level configs (in CWD / git root).
103 /// Writes to .claude/settings.local.json, ./opencode.json, etc.
104 #[arg(long, conflicts_with = "global")]
105 project: bool,
106
107 /// Always use global user configs (default fallback behaviour).
108 #[arg(long, conflicts_with = "project")]
109 global: bool,
110
111 /// Show what would be done without writing any files.
112 #[arg(long)]
113 dry_run: bool,
114
115 /// Only write MCP config; skip agent instructions, skills, and hooks.
116 #[arg(long)]
117 skip_instructions: bool,
118 },
119
120 /// Check for newer versions or upgrade the seshat binary.
121 Update {
122 /// Only check whether a newer version exists (no installation).
123 #[arg(long)]
124 check: bool,
125 },
126
127 /// Print a shell completion script to stdout.
128 ///
129 /// If the shell is omitted, it is auto-detected from the `$SHELL`
130 /// environment variable (or PowerShell on Windows). Pipe into the
131 /// shell's completion directory or `eval` it from a shell rc file.
132 /// Examples:
133 ///
134 /// seshat completions # auto-detect
135 /// seshat completions bash > /etc/bash_completion.d/seshat
136 /// seshat completions zsh > "${fpath\[1\]}/_seshat"
137 /// seshat completions fish > ~/.config/fish/completions/seshat.fish
138 Completions {
139 /// Target shell. Auto-detected from `$SHELL` if omitted.
140 #[arg(value_enum)]
141 shell: Option<clap_complete::Shell>,
142 },
143
144 /// Debug: print all conventions with real evidence snippets from the DB.
145 ///
146 /// Reads conventions from the current project's database and prints
147 /// description, nature, confidence, adoption stats, and full snippet
148 /// text for each evidence item. Use for debugging snippet extraction.
149 #[command(hide = true)]
150 DebugSnippets {
151 /// Path to project directory. Auto-detected from CWD if omitted.
152 path: Option<PathBuf>,
153 },
154
155 /// Manage user-recorded decisions stored in the project database.
156 ///
157 /// Decisions are project-wide records of approve/reject/partial/recorded
158 /// outcomes for conventions. They survive branch deletion and are the
159 /// source of truth for the review queue's exclusion filter.
160 Decisions {
161 /// The decisions subcommand to execute.
162 #[command(subcommand)]
163 command: DecisionsCommand,
164 },
165
166 /// Remove all Seshat configuration from detected AI clients.
167 ///
168 /// Reverses `seshat init`: removes MCP entries, instruction sections,
169 /// skill directories, and hook scripts. Does NOT remove the binary or DB files.
170 Uninstall {
171 /// Specific client to uninstall. Auto-detects all if omitted.
172 /// Supported: claude-code, claude-desktop, opencode, cursor
173 client: Option<String>,
174
175 /// Only uninstall from project-level configs.
176 #[arg(long, conflicts_with = "global")]
177 project: bool,
178
179 /// Only uninstall from global user configs.
180 #[arg(long, conflicts_with = "project")]
181 global: bool,
182
183 /// Show what would be removed without making changes.
184 #[arg(long)]
185 dry_run: bool,
186 },
187}
188
189/// Subcommands for `seshat decisions`.
190#[derive(Debug, Subcommand)]
191pub enum DecisionsCommand {
192 /// List recorded decisions, optionally filtered by state and branch.
193 List {
194 /// Restrict to decisions in a single state.
195 #[arg(long, value_enum)]
196 state: Option<DecisionStateFilter>,
197
198 /// Restrict to decisions whose `decided_on_branch` matches.
199 #[arg(long, value_name = "BRANCH")]
200 branch: Option<String>,
201
202 /// Output format.
203 #[arg(long, value_enum, default_value_t = DecisionsListFormat::Table)]
204 format: DecisionsListFormat,
205 },
206
207 /// Remove a recorded decision so the convention re-enters the review queue
208 /// on the next scan.
209 ///
210 /// Lookup accepts either the full `description_hash` or an unambiguous
211 /// prefix of at least 4 characters. The matched decision is printed for
212 /// confirmation; pass `--yes` to skip the interactive prompt (useful for
213 /// scripts).
214 Forget {
215 /// Full `description_hash` or an unambiguous prefix (≥4 chars).
216 hash: String,
217
218 /// Skip the confirmation prompt and remove the decision unattended.
219 #[arg(long)]
220 yes: bool,
221 },
222
223 /// Export the project's decisions table to a JSON file (backup or share).
224 ///
225 /// Writes the full project-wide decisions table as a pretty-printed JSON
226 /// array. The shape matches `seshat decisions list --format json` so a
227 /// round-trip back through `seshat decisions import` is lossless.
228 Export {
229 /// Output file path. Created (or overwritten) with the JSON array.
230 file: PathBuf,
231 },
232
233 /// Import a decisions JSON file produced by `seshat decisions export`.
234 ///
235 /// Each row in the input file is UPSERTed into the project's decisions
236 /// table. On hash conflicts the row with the larger `decided_at` wins
237 /// silently; pass `--strict` to fail (no writes) on any conflict instead.
238 Import {
239 /// Input file path containing the decisions JSON array.
240 file: PathBuf,
241
242 /// Fail on any hash conflict instead of silently keeping the newer
243 /// decision (useful for CI / audit pipelines that want to surface
244 /// divergences before merging).
245 #[arg(long)]
246 strict: bool,
247 },
248}
249
250/// CLI-facing alias for [`DecisionState`] that derives [`ValueEnum`].
251///
252/// Kept separate from the storage enum so the storage crate stays independent
253/// of `clap`. Convert with `DecisionState::from(filter)`.
254#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
255pub enum DecisionStateFilter {
256 Approved,
257 Rejected,
258 Partial,
259 Recorded,
260}
261
262impl From<DecisionStateFilter> for DecisionState {
263 fn from(value: DecisionStateFilter) -> Self {
264 match value {
265 DecisionStateFilter::Approved => DecisionState::Approved,
266 DecisionStateFilter::Rejected => DecisionState::Rejected,
267 DecisionStateFilter::Partial => DecisionState::Partial,
268 DecisionStateFilter::Recorded => DecisionState::Recorded,
269 }
270 }
271}
272
273/// Output format for `seshat decisions list`.
274#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
275pub enum DecisionsListFormat {
276 /// Human-readable aligned table (default).
277 Table,
278 /// JSON array of decision objects.
279 Json,
280}