Skip to content
137 changes: 123 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,13 +462,24 @@ fn main() -> Result<()> {

log::debug!("cli.json = {}", cli.json);

// Check for orphaned nested meta repo and warn the user
check_and_warn_orphan();

// Discover plugins early to handle --help requests and plugin listing
let mut subprocess_plugins = SubprocessPluginManager::new();
subprocess_plugins.discover_plugins(cli.verbose)?;

// External help is metadata-only. Let the matched plugin answer it before
// workspace discovery so malformed or absent config cannot hide help.
let external_help_request = matches!(
cli.command.as_ref(),
Some(Commands::External(args))
if cli.help
|| contains_help_before_separator(args)
|| (args.len() == 1
&& subprocess_plugins.is_bare_help_command(&args[0]))
);
if !external_help_request {
check_and_warn_orphan();
}

// Handle --help flag at top level
if cli.help && cli.command.is_none() {
print_help_with_plugins(&subprocess_plugins, false);
Expand Down Expand Up @@ -538,12 +549,17 @@ fn main() -> Result<()> {
let mut args = args;
extract_global_flags(&mut args, &mut cli);

let has_forwarded_help = contains_help_before_separator(&args);
if cli.help && !has_forwarded_help {
print_help_with_plugins(&subprocess_plugins, false);
return Ok(());
}

// Keep root plugin help fast and plugin-aware, but let nested help
// requests reach the matched plugin command implementation.
if let Some(first) = args.first() {
let wants_help = args.iter().any(|a| a == "--help" || a == "-h");
let is_bare = args.len() == 1;
let is_root_help = wants_help
let is_root_help = has_forwarded_help
&& args.len() == 2
&& matches!(args.get(1).map(String::as_str), Some("--help" | "-h"));

Expand All @@ -554,14 +570,15 @@ fn main() -> Result<()> {
.collect();
let is_promoted = promoted_commands.contains(&first.to_string());

if is_root_help || (is_bare && !is_promoted) {
let declared_bare_help = subprocess_plugins.is_bare_help_command(first);
if is_root_help || (is_bare && (!is_promoted || declared_bare_help)) {
if let Some(help_text) = subprocess_plugins.get_plugin_help(first) {
println!("{help_text}");
return Ok(());
}
}

if wants_help {
if has_forwarded_help {
let command_str = args.join(" ");
let options = PluginRequestOptions {
json_output: cli.json,
Expand Down Expand Up @@ -785,10 +802,11 @@ fn handle_command_dispatch(
return Ok(());
}

// No config found — degraded legacy path with warning
// No config found — worktree paths are still authoritative for
// plugin dispatch, but config-backed tags/dependencies are unavailable.
if cli.verbose {
eprintln!(
"{} No .meta config found for worktree '{}'. Tags, plugins, and dependency features unavailable.",
"{} No .meta config found for worktree '{}'. Tags and dependency features unavailable.",
"warning:".yellow().bold(),
task_name
);
Expand All @@ -801,10 +819,10 @@ fn handle_command_dispatch(
let exclude_opt = none_if_empty(exclude_filters);

let config = loop_lib::LoopConfig {
directories,
directories: directories.clone(),
ignore: vec![],
include_filters: include_opt,
exclude_filters: exclude_opt,
include_filters: include_opt.clone(),
exclude_filters: exclude_opt.clone(),
verbose: cli.verbose,
silent: cli.silent,
parallel, // Use the determined parallel mode, not hardcoded false
Expand All @@ -817,7 +835,36 @@ fn handle_command_dispatch(
root_dir: None, // Worktree paths don't use "." convention
};

run(&config, &command_str)?;
let subprocess_options = PluginRequestOptions {
json_output: cli.json,
verbose: cli.verbose,
parallel,
dry_run,
silent: cli.silent,
recursive,
depth,
include_filters: include_opt,
exclude_filters: exclude_opt,
strict: cli.strict,
};

if plugins.execute(
&command_str,
&command_args,
&directories,
subprocess_options,
)? {
if cli.verbose {
println!(
"{}",
"Command handled by subprocess plugin (worktree without config).".green()
);
}
} else if is_explicit_exec {
run(&config, &command_str)?;
} else {
unrecognized_command_error(&command_args, &command_str, plugins);
}
return Ok(());
}
}
Expand Down Expand Up @@ -920,10 +967,11 @@ fn handle_command_dispatch(
strict: cli.strict,
};

if plugins.execute(
if plugins.execute_with_root(
&command_str,
&command_args,
&project_paths,
Some(meta_dir),
subprocess_options,
)? {
log::info!("Command was handled by subprocess plugin");
Expand Down Expand Up @@ -1256,7 +1304,16 @@ fn handle_plugin_command(
/// subcommands (e.g. `worktree prune --dry-run`, `worktree exec --parallel`)
/// define their own versions and need to see them.
fn extract_global_flags(args: &mut Vec<String>, cli: &mut Cli) {
let mut after_separator = false;
args.retain(|arg| {
if after_separator {
return true;
}
if arg == "--" {
after_separator = true;
return true;
}

match arg.as_str() {
"--json" => {
cli.json = true;
Expand Down Expand Up @@ -1287,6 +1344,13 @@ fn extract_global_flags(args: &mut Vec<String>, cli: &mut Cli) {
});
}

/// Whether a help flag appears before the command's `--` separator.
fn contains_help_before_separator(args: &[String]) -> bool {
args.iter()
.take_while(|arg| arg.as_str() != "--")
.any(|arg| matches!(arg.as_str(), "--help" | "-h"))
}

/// Check whether a project's tags match a comma-separated tag filter string.
fn matches_tag_filter(tags: &[String], filter: &str) -> bool {
let requested: Vec<&str> = filter.split(',').map(|s| s.trim()).collect();
Expand Down Expand Up @@ -1423,6 +1487,51 @@ mod tests {
use std::io::Write;
use tempfile::NamedTempFile;

fn empty_cli() -> Cli {
Cli::try_parse_from(["meta"]).unwrap()
}

fn strings(values: &[&str]) -> Vec<String> {
values.iter().map(|value| (*value).to_string()).collect()
}

#[test]
fn test_external_global_extraction_stops_at_separator() {
let mut cli = empty_cli();
let mut args = strings(&[
"tool",
"run",
"--verbose",
"--recursive",
"--",
"--json",
"--strict",
"--recursive",
]);

extract_global_flags(&mut args, &mut cli);

assert!(cli.verbose);
assert!(cli.recursive);
assert!(!cli.json);
assert!(!cli.strict);
assert_eq!(
args,
strings(&["tool", "run", "--", "--json", "--strict", "--recursive"])
);
}

#[test]
fn test_forwarded_help_classification_stops_at_separator() {
assert!(contains_help_before_separator(&strings(&[
"tool", "run", "--help", "--", "payload"
])));
assert!(contains_help_before_separator(&strings(&["tool", "-h"])));
assert!(!contains_help_before_separator(&strings(&[
"tool", "run", "--", "--help"
])));
}

#[test]
fn test_parse_meta_config_valid_simple_format() {
let mut file = NamedTempFile::new().unwrap();
Expand Down
Loading
Loading