Skip to content

Commit d8560fb

Browse files
committed
feat: smart command validation with fallback (ADR-010)
Implements intelligent runner selection based on command support: - Validates if runner supports the requested command before selecting - npm/yarn/pnpm/bun: Parses package.json scripts section - cargo: Checks against built-in subcommands list - make: Parses Makefile targets - composer: Parses composer.json scripts - gradle: Built-in tasks + custom tasks from build.gradle - dotnet: Built-in commands list Selection priority: Supported > Unknown > NotSupported Falls back to Unknown runners if none explicitly support the command. Example: 'run precommit' in Rust project with Makefile now correctly runs 'make precommit' instead of failing with 'cargo precommit'. New files: - src/validators.rs: Command validation logic Test summary: - 78 unit tests (+5 for validators) - 60 integration tests (+4 for fallback behavior) - 16 proptest tests - Total: 154 tests passing ADR-010 documents this architectural decision. Fixes the issue where Cargo was selected for Makefile targets.
1 parent db45c62 commit d8560fb

9 files changed

Lines changed: 591 additions & 6 deletions

File tree

ADR.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,21 @@
9999
- **tempfile** for isolated test environments
100100
- All tests run via `cargo test` and `make precommit`
101101
**Consequences**: Comprehensive coverage (145+ tests) with fast feedback. Property tests catch edge cases that example-based tests miss.
102+
103+
### ADR-010: Smart Command Validation with Fallback
104+
105+
**Status**: ✅ Accepted
106+
**Context**: When multiple runners are detected (e.g., Cargo.toml + Makefile), the tool should intelligently select the runner that actually supports the requested command.
107+
**Decision**:
108+
- Before selecting a runner, validate if it supports the requested command
109+
- For **npm/yarn/pnpm/bun**: Parse `package.json` and check if script exists
110+
- For **cargo**: Check against list of built-in subcommands (build, test, clippy, etc.)
111+
- For **make**: Parse Makefile and extract target names
112+
- For **composer**: Parse `composer.json` scripts section
113+
- For **gradle**: Check built-in tasks + parse `build.gradle` for custom tasks
114+
- For **dotnet**: Check against built-in commands
115+
- Commands return `Supported`, `NotSupported`, or `Unknown` status
116+
- Selection priority: `Supported` > `Unknown` > skip `NotSupported`
117+
- Fallback to first `Unknown` if none explicitly support the command
118+
**Example**: `run precommit` in a Rust project with Makefile correctly runs `make precommit` instead of failing with `cargo precommit`.
119+
**Consequences**: Smarter runner selection that matches user intent. Eliminates need for `--ignore` flag in common fallback scenarios.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "run-cli"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
edition = "2021"
55
authors = ["Verseles"]
66
description = "Universal task runner for modern development - automatically detects and runs project commands"

ROADMAP.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ Description: Comprehensive testing and code quality improvements
123123
- [x] 9.08 Set up code coverage reporting with cargo-tarpaulin - Commit: (pending)
124124
- [x] 9.09 Add property-based tests with proptest (16 tests) - Commit: (pending)
125125

126+
### Feature 9.5: Smart Command Validation
127+
Description: Validate if runner supports the command before selecting it
128+
129+
- [x] 9.5.01 Create validators module with CommandSupport enum - Commit: (pending)
130+
- [x] 9.5.02 Implement npm/yarn/pnpm/bun validation (parse package.json scripts) - Commit: (pending)
131+
- [x] 9.5.03 Implement cargo validation (built-in commands list) - Commit: (pending)
132+
- [x] 9.5.04 Implement make validation (parse Makefile targets) - Commit: (pending)
133+
- [x] 9.5.05 Implement composer/gradle/dotnet validation - Commit: (pending)
134+
- [x] 9.5.06 Add select_runner function with fallback logic - Commit: (pending)
135+
- [x] 9.5.07 Add integration tests for fallback behavior - Commit: (pending)
136+
- [x] 9.5.08 Document in ADR-010 - Commit: (pending)
137+
126138
### Feature 10: MVP Polish - Documentation & Demos
127139
Description: Visual demos and enhanced documentation
128140

src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ pub enum RunError {
3131
#[error("Tool not installed: {0}")]
3232
ToolNotInstalled(String),
3333

34+
#[error("Command '{0}' not supported by any detected runner ({1:?})")]
35+
CommandNotSupported(String, Vec<String>),
36+
3437
#[error("Command execution failed: {0}")]
3538
CommandFailed(String),
3639

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub mod error;
2323
pub mod output;
2424
pub mod runner;
2525
pub mod update;
26+
pub mod validators;
2627

2728
pub use cli::Cli;
2829
pub use config::Config;

src/main.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use run_cli::cli::{Cli, Commands};
1515
use run_cli::config::Config;
1616
use run_cli::error::exit_codes;
1717
use run_cli::output;
18-
use run_cli::runner::{check_conflicts, execute, search_runners};
18+
use run_cli::runner::{check_conflicts, execute, search_runners, select_runner};
1919
use run_cli::update;
2020
use std::env;
2121
use std::io;
@@ -108,9 +108,15 @@ fn main() {
108108
}
109109
};
110110

111-
// Check for conflicts and select runner
111+
// Check for conflicts and select runner based on command support
112112
let runner = match check_conflicts(&runners, verbose) {
113-
Ok(r) => r,
113+
Ok(_) => match select_runner(&runners, &command, &working_dir, verbose) {
114+
Ok(r) => r,
115+
Err(e) => {
116+
output::error(&e.to_string());
117+
process::exit(e.exit_code());
118+
}
119+
},
114120
Err(e) => {
115121
output::error(&e.to_string());
116122
process::exit(e.exit_code());

src/runner.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use crate::detectors::{detect_all, is_tool_installed, DetectedRunner, Ecosystem};
1313
use crate::error::RunError;
1414
use crate::output;
15+
use crate::validators::CommandSupport;
1516
use std::collections::HashMap;
1617
use std::path::{Path, PathBuf};
1718
use std::process::{Command, ExitStatus, Stdio};
@@ -138,6 +139,55 @@ pub fn check_conflicts(
138139
Ok(runners[0].clone())
139140
}
140141

142+
pub fn select_runner(
143+
runners: &[DetectedRunner],
144+
command: &str,
145+
working_dir: &Path,
146+
verbose: bool,
147+
) -> Result<DetectedRunner, RunError> {
148+
if runners.is_empty() {
149+
return Err(RunError::RunnerNotFound(0));
150+
}
151+
152+
let mut supported_runners: Vec<&DetectedRunner> = Vec::new();
153+
let mut unknown_runners: Vec<&DetectedRunner> = Vec::new();
154+
155+
for runner in runners {
156+
match runner.supports_command(command, working_dir) {
157+
CommandSupport::Supported => {
158+
if verbose {
159+
output::info(&format!("{} supports command '{}'", runner.name, command));
160+
}
161+
supported_runners.push(runner);
162+
}
163+
CommandSupport::NotSupported => {
164+
if verbose {
165+
output::info(&format!(
166+
"{} does not support command '{}'",
167+
runner.name, command
168+
));
169+
}
170+
}
171+
CommandSupport::Unknown => {
172+
unknown_runners.push(runner);
173+
}
174+
}
175+
}
176+
177+
if let Some(runner) = supported_runners.first() {
178+
return Ok((*runner).clone());
179+
}
180+
181+
if let Some(runner) = unknown_runners.first() {
182+
return Ok((*runner).clone());
183+
}
184+
185+
Err(RunError::CommandNotSupported(
186+
command.to_string(),
187+
runners.iter().map(|r| r.name.clone()).collect(),
188+
))
189+
}
190+
141191
/// Execute a command with the detected runner
142192
pub fn execute(
143193
runner: &DetectedRunner,

0 commit comments

Comments
 (0)