1use std::path::PathBuf;
2
3use clap::{Args, Parser, Subcommand, ValueEnum};
4
5use crate::mode::Mode;
6
7#[derive(Debug, Clone, Copy, ValueEnum)]
9pub enum ModeArg {
10 Claude,
11 Gemini,
12 Cursor,
13 Codex,
14}
15
16impl ModeArg {
17 const fn to_mode(self) -> Mode {
18 match self {
19 Self::Claude => Mode::Claude,
20 Self::Gemini => Mode::Gemini,
21 Self::Cursor => Mode::Cursor,
22 Self::Codex => Mode::Codex,
23 }
24 }
25}
26
27#[derive(Parser, Debug)]
29#[command(
30 name = "rippy",
31 version,
32 about,
33 after_help = "\
34Reads a JSON hook payload from stdin and writes a verdict to stdout.\n\n\
35Exit codes: 0 = allow, 2 = ask/deny, 1 = error\n\n\
36Get started with a safety package:\n \
37rippy init # interactive package selection\n \
38rippy init --package develop # skip the prompt\n \
39rippy profile list # see available packages\n\n\
40Packages: review (full supervision), develop (balanced), autopilot (maximum autonomy)\n\n\
41Example hook usage:\n \
42echo '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"git status\"}}' | rippy --mode claude"
43)]
44pub struct Cli {
45 #[command(subcommand)]
46 pub command: Option<Command>,
47
48 #[command(flatten)]
49 pub hook_args: HookArgs,
50}
51
52#[derive(Subcommand, Debug)]
53pub enum Command {
54 Setup(SetupArgs),
56 Migrate(MigrateArgs),
58 Inspect(InspectArgs),
60 Stats(StatsArgs),
62 Allow(RuleArgs),
64 Deny(RuleArgs),
66 Ask(RuleArgs),
68 Suggest(SuggestArgs),
70 Init(InitArgs),
72 Discover(DiscoverArgs),
74 Trust(TrustArgs),
76 Debug(DebugArgs),
78 List(ListArgs),
80 Profile(ProfileArgs),
82}
83
84#[derive(Args, Debug)]
85pub struct ListArgs {
86 #[command(subcommand)]
87 pub target: ListTarget,
88}
89
90#[derive(Args, Debug)]
91pub struct ProfileArgs {
92 #[command(subcommand)]
93 pub target: ProfileTarget,
94}
95
96#[derive(Subcommand, Debug)]
97pub enum ProfileTarget {
98 List {
100 #[arg(long)]
102 json: bool,
103 },
104 Show {
106 name: String,
108 #[arg(long)]
110 json: bool,
111 },
112 Set {
114 name: String,
116 #[arg(long)]
118 project: bool,
119 },
120}
121
122#[derive(Subcommand, Debug)]
123pub enum ListTarget {
124 Safe,
126 Handlers,
128 Rules(ListRulesArgs),
130}
131
132#[derive(Args, Debug)]
133pub struct ListRulesArgs {
134 #[arg(long)]
136 pub filter: Option<String>,
137}
138
139#[derive(Args, Debug)]
140pub struct DiscoverArgs {
141 pub args: Vec<String>,
143
144 #[arg(long)]
146 pub all: bool,
147
148 #[arg(long)]
150 pub json: bool,
151}
152
153#[derive(Args, Debug)]
154pub struct InitArgs {
155 #[arg(long)]
157 pub global: bool,
158
159 #[arg(long)]
161 pub stdout: bool,
162
163 #[arg(long)]
166 pub package: Option<String>,
167}
168
169#[derive(Args, Debug)]
170pub struct StatsArgs {
171 #[arg(long)]
173 pub since: Option<String>,
174
175 #[arg(long)]
177 pub json: bool,
178
179 #[arg(long)]
181 pub db: Option<PathBuf>,
182}
183
184#[derive(Args, Debug)]
185pub struct RuleArgs {
186 pub pattern: String,
188 pub message: Option<String>,
190 #[arg(long)]
192 pub global: bool,
193}
194
195#[derive(Args, Debug)]
196#[allow(clippy::struct_excessive_bools)]
197pub struct SuggestArgs {
198 #[arg(long)]
200 pub from_command: Option<String>,
201
202 #[arg(long)]
204 pub since: Option<String>,
205
206 #[arg(long)]
208 pub json: bool,
209
210 #[arg(long)]
212 pub db: Option<PathBuf>,
213
214 #[arg(long)]
216 pub apply: bool,
217
218 #[arg(long)]
220 pub global: bool,
221
222 #[arg(long, default_value = "3")]
224 pub min_count: i64,
225
226 #[arg(long)]
228 pub sessions: bool,
229
230 #[arg(long)]
232 pub session_file: Option<PathBuf>,
233
234 #[arg(long)]
236 pub audit: bool,
237}
238
239#[derive(Args, Debug)]
240pub struct InspectArgs {
241 pub command: Option<String>,
243
244 #[arg(long)]
246 pub json: bool,
247
248 #[arg(long, env = "RIPPY_CONFIG")]
250 pub config: Option<PathBuf>,
251}
252
253#[derive(Args, Debug)]
255pub struct DebugArgs {
256 pub command: String,
258
259 #[arg(long)]
261 pub json: bool,
262
263 #[arg(long, env = "RIPPY_CONFIG")]
265 pub config: Option<PathBuf>,
266}
267
268#[derive(Args, Debug)]
269pub struct MigrateArgs {
270 pub path: Option<PathBuf>,
272
273 #[arg(long)]
275 pub stdout: bool,
276}
277
278#[derive(Args, Debug)]
279pub struct SetupArgs {
280 #[command(subcommand)]
281 pub target: SetupTarget,
282}
283
284#[derive(Subcommand, Debug)]
285pub enum SetupTarget {
286 Tokf(TokfSetupArgs),
288 ClaudeCode(DirectHookArgs),
290 Gemini(DirectHookArgs),
292 Cursor(DirectHookArgs),
294}
295
296#[derive(Args, Debug)]
297pub struct DirectHookArgs {
298 #[arg(long)]
300 pub global: bool,
301}
302
303#[derive(Args, Debug)]
304pub struct TokfSetupArgs {
305 #[arg(long)]
307 pub global: bool,
308
309 #[arg(long, value_delimiter = ',')]
313 pub install_hooks: Vec<String>,
314
315 #[arg(long)]
317 pub all_hooks: bool,
318}
319
320#[derive(Args, Debug)]
322#[allow(clippy::struct_excessive_bools)]
323pub struct TrustArgs {
324 #[arg(long)]
326 pub revoke: bool,
327
328 #[arg(long)]
330 pub status: bool,
331
332 #[arg(long)]
334 pub list: bool,
335
336 #[arg(long, short = 'y')]
338 pub yes: bool,
339}
340
341#[derive(Args, Debug)]
343pub struct HookArgs {
344 #[arg(long, value_enum)]
346 pub mode: Option<ModeArg>,
347
348 #[arg(long, env = "RIPPY_CONFIG")]
350 pub config: Option<PathBuf>,
351
352 #[arg(long)]
354 pub remote: bool,
355
356 #[arg(long, short = 'v')]
358 pub verbose: bool,
359}
360
361impl HookArgs {
362 #[must_use]
364 pub fn forced_mode(&self) -> Option<Mode> {
365 self.mode.map(ModeArg::to_mode)
366 }
367
368 #[must_use]
370 pub fn config_path(&self) -> Option<PathBuf> {
371 self.config
372 .clone()
373 .or_else(|| std::env::var_os("DIPPY_CONFIG").map(PathBuf::from))
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn forced_mode_claude() {
383 let args = HookArgs {
384 mode: Some(ModeArg::Claude),
385 config: None,
386 remote: false,
387 verbose: false,
388 };
389 assert_eq!(args.forced_mode(), Some(Mode::Claude));
390 }
391
392 #[test]
393 fn no_forced_mode() {
394 let args = HookArgs {
395 mode: None,
396 config: None,
397 remote: false,
398 verbose: false,
399 };
400 assert_eq!(args.forced_mode(), None);
401 }
402}