use crate::error::ProtoCliError;
use crate::session::{LoadToolOptions, ProtoSession};
use crate::workflows::{ExecCommandOptions, ExecWorkflow, ExecWorkflowParams};
use clap::Args;
use miette::IntoDiagnostic;
use proto_core::{ToolContext, ToolSpec};
use proto_shim::exec_command_and_replace;
use rustc_hash::{FxHashMap, FxHashSet};
use starbase::AppResult;
use starbase_shell::ShellType;
#[derive(Args, Clone, Debug)]
pub struct ExecArgs {
#[arg(help = "Tools to initialize")]
pub tools: Vec<String>,
#[arg(long, help = "Inherit tools to initialize from .prototools configs")]
pub tools_from_config: bool,
#[arg(long, help = "Execute the command as-is without quoting or escaping")]
pub raw: bool,
#[arg(long, help = "Shell to execute the command with")]
pub shell: Option<ShellType>,
#[arg(last = true, help = "The command to execute after initializing tools")]
pub command: Vec<String>,
}
#[tracing::instrument(skip_all)]
pub async fn exec(session: ProtoSession, args: ExecArgs) -> AppResult {
if args.command.is_empty() {
return Err(ProtoCliError::ExecMissingCommand.into());
}
let config = session.load_config()?;
let mut specs = FxHashMap::default();
for value in &args.tools {
let at_threshold = if value.contains(":@") { 2 } else { 1 };
let has_version = value.chars().filter(|c| *c == '@').count() == at_threshold;
if has_version && let Some(index) = value.rfind('@') {
specs.insert(
ToolContext::parse(&value[0..index])?,
Some(ToolSpec::parse(&value[index + 1..])?),
);
} else {
specs.insert(ToolContext::parse(value)?, None);
}
}
if args.tools_from_config {
for (context, spec) in &config.versions {
if !specs.contains_key(context) {
specs.insert(context.to_owned(), Some(spec.to_owned()));
}
}
}
let tools = if specs.is_empty() {
vec![]
} else {
session
.load_tools_with_options(LoadToolOptions {
contexts: FxHashSet::from_iter(specs.keys().cloned()),
..Default::default()
})
.await?
};
let mut workflow = ExecWorkflow::new(tools, config);
workflow
.prepare_environment(
specs
.into_iter()
.filter_map(|(ctx, spec)| spec.map(|s| (ctx, s)))
.collect(),
ExecWorkflowParams {
activate_environment: true,
fallback_any_spec: true,
pre_run_hook: true,
version_env_vars: true,
..Default::default()
},
)
.await?;
let command = workflow.create_command(
args.command,
args.shell,
ExecCommandOptions {
check_shell: true,
raw_args: args.raw,
},
)?;
exec_command_and_replace(command)
.into_diagnostic()
.map(|_| None)
}