use crate::context::resolve_project_ssh;
use crate::error::{Error, Result};
use crate::module::CliConfig;
use crate::project::Project;
use crate::shell;
use crate::ssh::{execute_local_command, execute_local_command_interactive, CommandOutput};
use std::process::Command;
pub fn execute_for_project(project: &Project, command: &str) -> Result<CommandOutput> {
if project.server_id.as_ref().is_none_or(|s| s.is_empty()) {
Ok(execute_local_command(command))
} else {
let ctx = resolve_project_ssh(&project.id)?;
Ok(ctx.client.execute(command))
}
}
pub fn execute_for_project_interactive(project: &Project, command: &str) -> Result<i32> {
if project.server_id.as_ref().is_none_or(|s| s.is_empty()) {
Ok(execute_local_command_interactive(command, None, None))
} else {
let ctx = resolve_project_ssh(&project.id)?;
Ok(ctx.client.execute_interactive(Some(command)))
}
}
pub fn execute_for_project_direct(
project: &Project,
cli_config: &CliConfig,
args: &[String],
target_domain: &str,
) -> Result<CommandOutput> {
let base_path = project
.base_path
.clone()
.filter(|p| !p.is_empty())
.ok_or_else(|| Error::config("Base path not configured".to_string()))?;
if let Ok(output) = try_execute_direct(base_path.clone(), cli_config, args, target_domain) {
return Ok(output);
}
let command = build_shell_command(&base_path, cli_config, args, target_domain)?;
execute_for_project(project, &command)
}
fn try_execute_direct(
base_path: String,
cli_config: &CliConfig,
args: &[String],
target_domain: &str,
) -> Result<CommandOutput> {
if requires_shell_execution(&cli_config.command_template) {
return Err(Error::other(
"Template requires shell execution".to_string(),
));
}
let parsed = parse_direct_template(base_path, cli_config, args, target_domain)?;
let mut cmd = Command::new(&parsed.program);
if let Some(dir) = &parsed.working_dir {
cmd.current_dir(dir);
}
if !parsed.args.is_empty() {
cmd.args(&parsed.args);
}
match cmd.output() {
Ok(out) => Ok(CommandOutput {
stdout: String::from_utf8_lossy(&out.stdout).to_string(),
stderr: String::from_utf8_lossy(&out.stderr).to_string(),
success: out.status.success(),
exit_code: out.status.code().unwrap_or(-1),
}),
Err(e) => Ok(CommandOutput {
stdout: String::new(),
stderr: format!("Command error: {}", e),
success: false,
exit_code: -1,
}),
}
}
fn requires_shell_execution(template: &str) -> bool {
let shell_operators = [
"&&", "||", ";", "|", ">", ">>", "<", "<<", "&", "`", "$(", "EOF",
];
for op in &shell_operators {
if template.contains(op) {
return true;
}
}
false
}
struct ParsedDirectCommand {
program: String,
args: Vec<String>,
working_dir: Option<String>,
}
fn parse_direct_template(
base_path: String,
cli_config: &CliConfig,
args: &[String],
target_domain: &str,
) -> Result<ParsedDirectCommand> {
let mut template = cli_config.command_template.clone();
let cli_path = cli_config
.default_cli_path
.clone()
.unwrap_or_else(|| cli_config.tool.clone());
template = template.replace("{{cliPath}}", &cli_path);
template = template.replace("--url={{domain}}", &format!("--url={}", target_domain));
let mut command_parts: Vec<String> =
template.split_whitespace().map(|s| s.to_string()).collect();
let final_args: Vec<String> =
if let Some(pos) = command_parts.iter().position(|p| p == "{{args}}") {
command_parts.remove(pos);
let mut result = Vec::new();
result.extend(command_parts);
result.extend(args.iter().cloned());
result
} else {
command_parts
};
let working_dir = cli_config.working_dir_template.as_ref().and_then(|t| {
if t == "{{sitePath}}" {
Some(base_path)
} else {
None
}
});
let program = final_args
.first()
.cloned()
.unwrap_or_else(|| cli_path.clone());
let args = if final_args.len() > 1 {
final_args[1..].to_vec()
} else {
vec![]
};
Ok(ParsedDirectCommand {
program,
args,
working_dir,
})
}
fn build_shell_command(
base_path: &str,
cli_config: &CliConfig,
args: &[String],
target_domain: &str,
) -> Result<String> {
let mut template = cli_config.command_template.clone();
let cli_path = cli_config
.default_cli_path
.clone()
.unwrap_or_else(|| cli_config.tool.clone());
template = template.replace("{{cliPath}}", &cli_path);
template = template.replace("{{sitePath}}", base_path);
template = template.replace("{{domain}}", target_domain);
let quoted_args = shell::quote_args(args);
template = template.replace("{{args}}", "ed_args);
Ok(template)
}