use async_trait::async_trait;
use cuenv_core::Result;
use cuenv_events::{emit_stderr, emit_stdout};
use crate::cli::{ShellType, StatusFormat};
use crate::events::Event;
use super::{CommandExecutor, ci, env, exec, export, hooks, sync, task};
#[async_trait]
pub trait CommandHandler: Send + Sync {
fn command_name(&self) -> &'static str;
async fn execute(&self, executor: &CommandExecutor) -> Result<String>;
fn should_print_output(&self) -> bool {
true
}
}
#[async_trait]
pub trait CommandRunner {
async fn run_command<C: CommandHandler>(&self, cmd: C) -> Result<()>;
}
#[async_trait]
impl CommandRunner for CommandExecutor {
async fn run_command<C: CommandHandler>(&self, cmd: C) -> Result<()> {
let name = cmd.command_name();
self.send_event(Event::CommandStart {
command: name.to_string(),
});
match cmd.execute(self).await {
Ok(output) => {
if cmd.should_print_output() && !output.is_empty() {
emit_stdout!(&output);
}
self.send_event(Event::CommandComplete {
command: name.to_string(),
success: true,
output,
});
Ok(())
}
Err(e) => {
emit_stderr!(format!("Error: {e}"));
self.send_event(Event::CommandComplete {
command: name.to_string(),
success: false,
output: format!("Error: {e}"),
});
Err(e)
}
}
}
}
pub struct EnvPrintHandler {
pub path: String,
pub package: String,
pub format: String,
pub environment: Option<String>,
}
#[async_trait]
impl CommandHandler for EnvPrintHandler {
fn command_name(&self) -> &'static str {
"env print"
}
fn should_print_output(&self) -> bool {
false }
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
env::execute_env_print(
&self.path,
&self.format,
self.environment.as_deref(),
executor,
)
.await
}
}
pub struct EnvListHandler {
pub path: String,
pub package: String,
pub format: String,
}
#[async_trait]
impl CommandHandler for EnvListHandler {
fn command_name(&self) -> &'static str {
"env list"
}
fn should_print_output(&self) -> bool {
false
}
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
env::execute_env_list(&self.path, &self.format, executor).await
}
}
pub struct EnvLoadHandler {
pub path: String,
pub package: String,
}
#[async_trait]
impl CommandHandler for EnvLoadHandler {
fn command_name(&self) -> &'static str {
"env load"
}
fn should_print_output(&self) -> bool {
false
}
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
hooks::execute_env_load(&self.path, &self.package, Some(executor)).await
}
}
pub struct EnvStatusHandler {
pub path: String,
pub package: String,
pub wait: bool,
pub timeout: u64,
pub format: StatusFormat,
}
#[async_trait]
impl CommandHandler for EnvStatusHandler {
fn command_name(&self) -> &'static str {
"env status"
}
fn should_print_output(&self) -> bool {
false
}
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
hooks::execute_env_status(
&self.path,
&self.package,
self.wait,
self.timeout,
self.format,
Some(executor),
)
.await
}
}
pub struct EnvCheckHandler {
pub path: String,
pub package: String,
pub shell: ShellType,
}
#[async_trait]
impl CommandHandler for EnvCheckHandler {
fn command_name(&self) -> &'static str {
"env check"
}
fn should_print_output(&self) -> bool {
false
}
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
hooks::execute_env_check(&self.path, &self.package, self.shell, Some(executor)).await
}
}
pub struct EnvInspectHandler {
pub path: String,
pub package: String,
}
#[async_trait]
impl CommandHandler for EnvInspectHandler {
fn command_name(&self) -> &'static str {
"env inspect"
}
fn should_print_output(&self) -> bool {
false
}
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
hooks::execute_env_inspect(&self.path, &self.package, Some(executor)).await
}
}
pub struct AllowHandler {
pub path: String,
pub package: String,
pub note: Option<String>,
pub yes: bool,
}
#[async_trait]
impl CommandHandler for AllowHandler {
fn command_name(&self) -> &'static str {
"allow"
}
fn should_print_output(&self) -> bool {
false
}
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
hooks::execute_allow(
&self.path,
&self.package,
self.note.clone(),
self.yes,
Some(executor),
)
.await
}
}
pub struct DenyHandler {
pub path: String,
pub package: String,
}
#[async_trait]
impl CommandHandler for DenyHandler {
fn command_name(&self) -> &'static str {
"deny"
}
fn should_print_output(&self) -> bool {
false
}
async fn execute(&self, _executor: &CommandExecutor) -> Result<String> {
hooks::execute_deny(&self.path, &self.package).await
}
}
pub struct ExportHandler {
pub shell: Option<String>,
pub path: String,
pub package: String,
}
#[async_trait]
impl CommandHandler for ExportHandler {
fn command_name(&self) -> &'static str {
"export"
}
fn should_print_output(&self) -> bool {
false
}
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
export::execute_export(
self.shell.as_deref(),
&self.path,
&self.package,
Some(executor),
)
.await
}
}
pub struct ExecHandler {
pub path: String,
pub package: String,
pub command: String,
pub args: Vec<String>,
pub environment: Option<String>,
}
#[async_trait]
impl CommandHandler for ExecHandler {
fn command_name(&self) -> &'static str {
"exec"
}
fn should_print_output(&self) -> bool {
false
}
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
let request = exec::ExecRequest {
path: &self.path,
package: &self.package,
command: &self.command,
args: &self.args,
environment_override: self.environment.as_deref(),
};
let exit_code = exec::execute_exec(request, executor).await?;
if exit_code == 0 {
Ok(format!("Command exited with code {exit_code}"))
} else {
Err(cuenv_core::Error::configuration(format!(
"Command failed with exit code {exit_code}"
)))
}
}
}
#[allow(clippy::struct_excessive_bools)]
pub struct TaskHandler {
pub path: String,
pub package: String,
pub name: Option<String>,
pub labels: Vec<String>,
pub environment: Option<String>,
pub format: String,
pub materialize_outputs: Option<String>,
pub show_cache_path: bool,
pub backend: Option<String>,
pub tui: bool,
pub interactive: bool,
pub help: bool,
pub skip_dependencies: bool,
pub dry_run: cuenv_core::DryRun,
pub task_args: Vec<String>,
}
#[async_trait]
impl CommandHandler for TaskHandler {
fn command_name(&self) -> &'static str {
"task"
}
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
if !self.labels.is_empty() && self.name.is_some() {
return Err(cuenv_core::Error::configuration(
"Cannot specify both a task name and --label",
));
}
if !self.labels.is_empty() && !self.task_args.is_empty() {
return Err(cuenv_core::Error::configuration(
"Task arguments are not supported when selecting tasks by label",
));
}
let mut request = match (&self.name, &self.labels, self.interactive) {
(Some(name), _, _) => {
task::TaskExecutionRequest::named(&self.path, &self.package, name, executor)
.with_args(self.task_args.clone())
}
(None, labels, _) if !labels.is_empty() => task::TaskExecutionRequest::labels(
&self.path,
&self.package,
labels.clone(),
executor,
),
(None, _, true) => {
task::TaskExecutionRequest::interactive(&self.path, &self.package, executor)
}
(None, _, _) => task::TaskExecutionRequest::list(&self.path, &self.package, executor),
};
if let Some(env) = &self.environment {
request = request.with_environment(env);
}
request = request.with_format(&self.format);
if let Some(path) = &self.materialize_outputs {
request = request.with_materialize_outputs(path);
}
if self.show_cache_path {
request = request.with_show_cache_path();
}
if let Some(backend) = &self.backend {
request = request.with_backend(backend);
}
if self.tui {
request = request.with_tui();
}
if self.help {
request = request.with_help();
}
if self.skip_dependencies {
request = request.with_skip_dependencies();
}
if self.dry_run.is_dry_run() {
request = request.with_dry_run();
}
task::execute(request).await
}
}
pub struct CiHandler {
pub args: ci::CiArgs,
}
#[async_trait]
impl CommandHandler for CiHandler {
fn command_name(&self) -> &'static str {
"ci"
}
fn should_print_output(&self) -> bool {
false }
async fn execute(&self, _executor: &CommandExecutor) -> Result<String> {
ci::execute_ci(self.args.clone()).await?;
Ok("CI execution completed".to_string())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum SyncScope {
#[default]
Path,
Workspace,
}
pub struct SyncHandler {
pub subcommand: Option<String>,
pub path: String,
pub package: String,
pub mode: sync::SyncMode,
pub scope: SyncScope,
pub show_diff: bool,
pub ci_provider: Option<String>,
pub update_tools: Option<Vec<String>>,
}
struct SelectedSyncProvidersRequest<'a> {
registry: &'a sync::SyncRegistry,
provider_names: &'a [&'a str],
path: &'a std::path::Path,
package: &'a str,
options: &'a sync::SyncOptions,
scope: &'a SyncScope,
executor: &'a CommandExecutor,
}
async fn run_selected_sync_providers(request: SelectedSyncProvidersRequest<'_>) -> Result<String> {
let mut outputs = Vec::new();
let mut had_error = false;
let sync_all = request.scope == &SyncScope::Workspace;
for name in request.provider_names {
let result = request
.registry
.sync_provider(
name,
request.path,
request.package,
request.options,
sync_all,
request.executor,
)
.await;
match result {
Ok(r) => {
if !r.output.is_empty() {
outputs.push(format!("[{name}]\n{}", r.output));
}
had_error |= r.had_error;
}
Err(e) => {
outputs.push(format!("[{name}] Error: {e}"));
had_error = true;
}
}
}
let combined = outputs.join("\n\n");
if had_error {
Err(cuenv_core::Error::configuration(combined))
} else if combined.is_empty() {
Ok("No sync operations performed.".to_string())
} else {
Ok(combined)
}
}
#[async_trait]
impl CommandHandler for SyncHandler {
fn command_name(&self) -> &'static str {
"sync"
}
async fn execute(&self, executor: &CommandExecutor) -> Result<String> {
use sync::{SyncOptions, default_registry};
let registry = default_registry();
let options = SyncOptions {
mode: self.mode.clone(),
show_diff: self.show_diff,
ci_provider: self.ci_provider.clone(),
update_tools: self.update_tools.clone(),
};
let path = std::path::Path::new(&self.path);
let sync_all = self.scope == SyncScope::Workspace;
let project_error = |path: &std::path::Path| {
cuenv_core::Error::configuration(format!(
"No cuenv project found at path: {}. Run 'cuenv info' to inspect project layout or use 'cuenv sync -A' to sync all projects.",
path.display()
))
};
if self.subcommand.is_none() && !sync_all {
let target_path = path.canonicalize().map_err(|e| cuenv_core::Error::Io {
source: e,
path: Some(path.to_path_buf().into_boxed_path()),
operation: "canonicalize path".to_string(),
})?;
let (is_project, is_root) = {
let module = executor.get_module(&target_path)?;
let is_root = module.root == target_path;
let is_project = module.projects().any(|instance| {
module
.root
.join(&instance.path)
.canonicalize()
.ok()
.is_some_and(|path| path == target_path)
});
(is_project, is_root)
};
if !is_project {
if !is_root {
return Err(project_error(path));
}
return run_selected_sync_providers(SelectedSyncProvidersRequest {
registry: ®istry,
provider_names: &["rules"],
path,
package: &self.package,
options: &options,
scope: &self.scope,
executor,
})
.await;
}
}
if let Some(name) = &self.subcommand {
let mut use_workspace = sync_all;
if !use_workspace && *name == "ci" && self.path == "." {
tracing::info!("sync ci: switching to workspace mode at module root");
use_workspace = true;
}
let result = registry
.sync_provider(name, path, &self.package, &options, use_workspace, executor)
.await?;
return Ok(result.output);
}
run_selected_sync_providers(SelectedSyncProvidersRequest {
registry: ®istry,
provider_names: &["lock", "codegen", "ci", "rules", "git-hooks"],
path,
package: &self.package,
options: &options,
scope: &self.scope,
executor,
})
.await
}
}
pub struct ShellInitHandler {
pub shell: ShellType,
}
impl ShellInitHandler {
pub fn execute_sync(&self, executor: &CommandExecutor) {
let name = "shell init";
executor.send_event(Event::CommandStart {
command: name.to_string(),
});
let output = hooks::execute_shell_init(self.shell);
executor.send_event(Event::CommandComplete {
command: name.to_string(),
success: true,
output,
});
}
}