Skip to main content

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}