Skip to content

Commit f2eee5e

Browse files
fohteclaude
andauthored
feat(cli): add --dry-run and --verbose options to exec/check subcommands (#59)
Co-authored-by: Claude Opus 4.6 <[email protected]>
1 parent 2efb529 commit f2eee5e

File tree

7 files changed

+603
-182
lines changed

7 files changed

+603
-182
lines changed

src/adapter/exec_adapter.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,34 @@ impl Endpoint for ExecAdapter {
122122
eprintln!("runok: error: {}", error);
123123
1
124124
}
125+
126+
fn handle_dry_run(&self, result: ActionResult) -> Result<i32, anyhow::Error> {
127+
match &result.action {
128+
Action::Allow => {
129+
eprintln!("runok: dry-run: command would be allowed");
130+
}
131+
Action::Deny(deny_response) => {
132+
let msg = deny_response
133+
.message
134+
.as_deref()
135+
.unwrap_or("command would be denied");
136+
eprintln!("runok: dry-run: {}", msg);
137+
if let Some(suggestion) = &deny_response.fix_suggestion {
138+
eprintln!("runok: dry-run: suggestion: {}", suggestion);
139+
}
140+
}
141+
Action::Ask(message) => {
142+
let msg = message
143+
.as_deref()
144+
.unwrap_or("command would require confirmation");
145+
eprintln!("runok: dry-run: {}", msg);
146+
}
147+
Action::Default => {
148+
eprintln!("runok: dry-run: no matching rule (default behavior)");
149+
}
150+
}
151+
Ok(0)
152+
}
125153
}
126154

127155
#[cfg(test)]
@@ -498,4 +526,63 @@ mod tests {
498526
let captured = executed_command.lock().unwrap();
499527
assert_eq!(*captured, Some(expected));
500528
}
529+
530+
// --- handle_dry_run ---
531+
532+
#[rstest]
533+
#[case::allow(Action::Allow, 0)]
534+
#[case::deny(
535+
Action::Deny(DenyResponse {
536+
message: Some("dangerous".to_string()),
537+
fix_suggestion: Some("use safer command".to_string()),
538+
matched_rule: "rm *".to_string(),
539+
}),
540+
0
541+
)]
542+
#[case::ask(
543+
Action::Ask(Some("please confirm".to_string())),
544+
0
545+
)]
546+
#[case::default_action(Action::Default, 0)]
547+
fn handle_dry_run_always_returns_exit_0(
548+
#[case] action: Action,
549+
#[case] expected_exit_code: i32,
550+
) {
551+
let adapter = ExecAdapter::new(
552+
vec!["git".into(), "status".into()],
553+
None,
554+
Box::new(MockExecutor::new(42)),
555+
);
556+
let result = adapter
557+
.handle_dry_run(ActionResult {
558+
action,
559+
sandbox: SandboxInfo::Preset(None),
560+
})
561+
.unwrap();
562+
assert_eq!(result, expected_exit_code);
563+
}
564+
565+
#[rstest]
566+
fn handle_dry_run_does_not_execute_command() {
567+
let captured: std::sync::Arc<std::sync::Mutex<Option<CommandInput>>> =
568+
std::sync::Arc::new(std::sync::Mutex::new(None));
569+
570+
let adapter = ExecAdapter::new(
571+
vec!["git".into(), "status".into()],
572+
None,
573+
Box::new(CapturingExecutor {
574+
captured: captured.clone(),
575+
}),
576+
);
577+
578+
adapter
579+
.handle_dry_run(ActionResult {
580+
action: Action::Allow,
581+
sandbox: SandboxInfo::Preset(None),
582+
})
583+
.unwrap();
584+
585+
// Command should NOT be executed in dry-run mode
586+
assert!(captured.lock().unwrap().is_none());
587+
}
501588
}

0 commit comments

Comments
 (0)