#![allow(clippy::expect_used)]
use crossterm::ExecutableCommand;
use cuenv::cli::{
self, CliError, EXIT_OK, OkEnvelope, OutputFormat, exit_code_for, parse, render_error,
};
use cuenv::commands::{self, Command, CommandExecutor};
use cuenv::tracing::{self, Level, TracingConfig, TracingFormat};
use cuenv::{coordinator, tui};
use cuenv_events::renderers::{CliRenderer, JsonRenderer};
use cuenv_hooks::{ExecutionStatus, Hook, HookExecutionConfig, StateManager, execute_hooks};
use std::path::PathBuf;
use std::sync::Arc;
use tracing::instrument;
const EXIT_SIGINT: i32 = 130;
const LLMS_CONTENT: &str = include_str!(concat!(env!("OUT_DIR"), "/llms-full.txt"));
fn main() {
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install default rustls crypto provider");
#[allow(clippy::print_stderr)]
std::panic::set_hook(Box::new(|panic_info| {
eprintln!("Application panicked: {panic_info}");
eprintln!("Internal error occurred. Run with RUST_LOG=debug for more information.");
}));
if let Ok(token) = std::env::var("OP_SERVICE_ACCOUNT_TOKEN") {
cuenv_events::register_secret(token);
}
if cli::try_complete() {
std::process::exit(EXIT_OK);
}
let args: Vec<String> = std::env::args().collect();
#[cfg(unix)]
if args.len() > 1 && args[1] == "__supervise" {
let rest: Vec<String> = args.iter().skip(2).cloned().collect();
let code = commands::supervise::run(&rest);
std::process::exit(code);
}
if args.len() > 1 && (args[1] == "__hook-supervisor" || args[1] == "__coordinator") {
#[cfg(unix)]
if args[1] == "__hook-supervisor" {
#[expect(
unsafe_code,
reason = "Required for POSIX process detachment via setsid()"
)]
unsafe {
libc::setsid();
}
}
let exit_code = run_with_tokio();
std::process::exit(exit_code);
}
let cli = cli::parse();
if requires_async_runtime(&cli) {
let exit_code = run_with_tokio();
std::process::exit(exit_code);
} else {
let exit_code = run_sync(cli);
std::process::exit(exit_code);
}
}
fn run_with_tokio() -> i32 {
let rt = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
#[allow(clippy::print_stderr)]
{
eprintln!("Fatal error: Failed to create tokio runtime: {e}");
}
return 1;
}
};
rt.block_on(run())
}
const fn requires_async_runtime(cli: &cli::Cli) -> bool {
if cli.llms {
return false;
}
match &cli.command {
None => false, Some(cmd) => match cmd {
cli::Commands::Version { .. }
| cli::Commands::Info { .. }
| cli::Commands::Build { .. }
| cli::Commands::Completions { .. }
| cli::Commands::Changeset { .. }
| cli::Commands::Secrets { .. }
| cli::Commands::Export { .. }
| cli::Commands::Fmt { .. } => false,
cli::Commands::Shell { subcommand } => match subcommand {
cli::ShellCommands::Init { .. } => false,
},
cli::Commands::Release { subcommand } => match subcommand {
cli::ReleaseCommands::Version { .. }
| cli::ReleaseCommands::Publish { .. }
| cli::ReleaseCommands::Prepare { .. } => false,
cli::ReleaseCommands::Binaries { .. } => true,
},
cli::Commands::Env { subcommand } => match subcommand {
cli::EnvCommands::Status { wait: false, .. }
| cli::EnvCommands::Print { .. }
| cli::EnvCommands::List { .. } => false,
_ => true,
},
cli::Commands::Task { .. }
| cli::Commands::Exec { .. }
| cli::Commands::Ci { .. }
| cli::Commands::Tui
| cli::Commands::Web { .. }
| cli::Commands::Allow { .. }
| cli::Commands::Deny { .. }
| cli::Commands::Sync { .. }
| cli::Commands::Runtime { .. }
| cli::Commands::Up { .. }
| cli::Commands::Down { .. }
| cli::Commands::Logs { .. }
| cli::Commands::Ps { .. }
| cli::Commands::Restart { .. } => true,
cli::Commands::Tools { subcommand } => match subcommand {
cli::ToolsCommands::Download | cli::ToolsCommands::Activate => true,
cli::ToolsCommands::List => false,
},
},
}
}
fn run_sync(cli: cli::Cli) -> i32 {
let _ = ctrlc::set_handler(|| {
if let Ok(rt) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
{
rt.block_on(async {
let registry = cuenv_core::tasks::global_registry();
registry
.terminate_all(std::time::Duration::from_secs(3))
.await;
});
}
cleanup_terminal();
std::process::exit(EXIT_SIGINT);
});
let log_level = match cli.level {
tracing::LogLevel::Trace => Level::TRACE,
tracing::LogLevel::Debug => Level::DEBUG,
tracing::LogLevel::Info => Level::INFO,
tracing::LogLevel::Warn => Level::WARN,
tracing::LogLevel::Error => Level::ERROR,
};
let tracing_config = tracing::TracingConfig {
format: if cli.json {
tracing::TracingFormat::Json
} else {
tracing::TracingFormat::Pretty
},
level: log_level,
..Default::default()
};
let _ = tracing::init_tracing(tracing_config);
if cli.llms {
#[allow(clippy::print_stdout)] {
print!("{LLMS_CONTENT}");
}
return EXIT_OK;
}
let json_format = OutputFormat::from_json_flag(cli.json);
let Some(cli_command) = cli.command else {
render_error(
&CliError::config_with_help(
"No subcommand provided",
"Run 'cuenv --help' for usage information",
),
json_format,
);
return exit_code_for(&CliError::config("No subcommand provided"));
};
if let cli::Commands::Completions { shell } = &cli_command {
cli::generate_completions(*shell);
return EXIT_OK;
}
let command: Command = cli_command.into_command(cli.environment.clone());
match execute_sync_command(command, json_format) {
Ok(()) => EXIT_OK,
Err(err) => {
render_error(&err, json_format);
exit_code_for(&err)
}
}
}
#[allow(clippy::too_many_lines)] fn execute_sync_command(command: Command, json_format: cli::OutputFormat) -> Result<(), CliError> {
match command {
Command::Version { format: _ } => {
let version_info = commands::version::get_version_info();
#[allow(clippy::print_stdout)] {
println!("{version_info}");
}
Ok(())
}
Command::Info {
path,
package,
meta,
} => {
let options = commands::info::InfoOptions {
path: path.as_deref(),
package: &package,
json_output: json_format.is_json(),
with_meta: meta,
};
match commands::info::execute_info(options) {
Ok(output) => {
cuenv_events::print_redacted(&output);
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Info command failed: {e}"),
"Check that you are in a CUE module with valid env.cue files",
)),
}
}
Command::ShellInit { shell } => execute_shell_init_command_safe(shell, json_format),
Command::EnvStatus {
path,
package,
wait: false,
format,
..
} => match commands::hooks::execute_env_status_sync(&path, &package, format) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({
"status": output
}));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!("JSON serialization failed: {e}")));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Env status failed: {e}"),
"Check that your env.cue file exists",
)),
},
Command::EnvPrint {
path,
package,
format,
environment,
} => {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| CliError::other(format!("Runtime error: {e}")))?;
let executor = create_executor(&package);
rt.block_on(async {
match commands::env::execute_env_print(
&path,
&format,
environment.as_deref(),
&executor,
)
.await
{
Ok(result) => {
cuenv_events::println_redacted(&result);
Ok(())
}
Err(e) => {
let cli_err: CliError = e.into();
Err(cli_err.with_help("Check your CUE files and package configuration"))
}
}
})
}
Command::EnvList {
path,
package,
format,
} => {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| CliError::other(format!("Runtime error: {e}")))?;
let executor = create_executor(&package);
rt.block_on(async {
match commands::env::execute_env_list(&path, &format, &executor).await {
Ok(result) => {
cuenv_events::println_redacted(&result);
Ok(())
}
Err(e) => {
let cli_err: CliError = e.into();
Err(cli_err.with_help("Check your CUE files and package configuration"))
}
}
})
}
Command::ChangesetAdd {
path,
summary,
description,
packages,
} => match commands::release::execute_changeset_add(
&path,
&packages,
summary.as_deref(),
description.as_deref(),
) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({ "message": output }));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!("JSON serialization failed: {e}")));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Changeset add failed: {e}"),
"Check package names and bump types (major, minor, patch)",
)),
},
Command::ChangesetStatus { path, json } => {
let merged_format = OutputFormat::from_json_flag(json || json_format.is_json());
match commands::release::execute_changeset_status_with_format(&path, merged_format) {
Ok(output) => {
cuenv_events::println_redacted(&output);
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Changeset status failed: {e}"),
"Check that the path is valid",
)),
}
}
Command::ChangesetFromCommits { path, since } => {
match commands::release::execute_changeset_from_commits(&path, since.as_deref()) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({ "message": output }));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!(
"JSON serialization failed: {e}"
)));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Changeset from-commits failed: {e}"),
"Check that the path is a valid git repository",
)),
}
}
Command::ReleasePrepare {
path,
since,
dry_run,
branch,
no_pr,
} => {
let opts = commands::release::ReleasePrepareOptions {
path,
since,
dry_run,
branch,
no_pr,
};
match commands::release::execute_release_prepare(&opts) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({ "result": output }));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!(
"JSON serialization failed: {e}"
)));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Release prepare failed: {e}"),
"Check git history and workspace configuration",
)),
}
}
Command::ReleaseVersion { path, dry_run } => {
match commands::release::execute_release_version(&path, dry_run) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({ "result": output }));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!(
"JSON serialization failed: {e}"
)));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Release version failed: {e}"),
"Create changesets first with 'cuenv changeset add'",
)),
}
}
Command::ReleasePublish { path, dry_run } => {
let format = if json_format.is_json() {
commands::release::OutputFormat::Json
} else {
commands::release::OutputFormat::Human
};
match commands::release::execute_release_publish(&path, dry_run, format) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({ "result": output }));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!(
"JSON serialization failed: {e}"
)));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Release publish failed: {e}"),
"Check that packages are ready for publishing",
)),
}
}
Command::Completions { shell } => {
cli::generate_completions(shell);
Ok(())
}
Command::SecretsSetup { provider, wasm_url } => {
commands::secrets::execute_secrets_setup(provider, wasm_url.as_deref())
}
Command::ToolsList => commands::tools::execute_tools_list(),
Command::Fmt {
path,
package,
fix,
only,
} => match commands::fmt::execute_fmt(&path, &package, fix, only.as_deref()) {
Ok(output) => {
cuenv_events::println_redacted(&output);
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Format failed: {e}"),
"Check your formatters configuration in env.cue",
)),
},
Command::Export {
shell,
path,
package,
} => {
match commands::export::execute_export_sync(shell.as_deref(), &path, &package) {
Ok(Some(output)) => {
cuenv_events::print_redacted(&output);
Ok(())
}
Ok(None) => {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| CliError::other(format!("Runtime error: {e}")))?;
rt.block_on(async {
match commands::export::execute_export(
shell.as_deref(),
&path,
&package,
None,
)
.await
{
Ok(result) => {
cuenv_events::print_redacted(&result);
Ok(())
}
Err(e) => {
let cli_err: CliError = e.into();
Err(cli_err.with_help("Check your CUE configuration"))
}
}
})
}
Err(e) => {
let cli_err: CliError = e.into();
Err(cli_err.with_help("Check your CUE configuration"))
}
}
}
_ => Err(CliError::other(
"Internal error: async command reached sync path",
)),
}
}
#[instrument(name = "cuenv_run")]
async fn run() -> i32 {
tokio::select! {
biased;
_ = tokio::signal::ctrl_c() => {
let registry = cuenv_core::tasks::global_registry();
registry.terminate_all(std::time::Duration::from_secs(5)).await;
cleanup_terminal();
EXIT_SIGINT
}
result = real_main() => {
match result {
Ok(()) => EXIT_OK,
Err(err) => {
let args: Vec<String> = std::env::args().collect();
let json_mode = args.iter().any(|arg| arg == "--json");
render_error(&err, OutputFormat::from_json_flag(json_mode));
exit_code_for(&err)
}
}
}
}
}
fn cleanup_terminal() {
use std::io::Write;
let mut stdout = std::io::stdout();
let _ = crossterm::terminal::disable_raw_mode();
let _ = stdout.execute(crossterm::event::PopKeyboardEnhancementFlags);
let _ = stdout.execute(crossterm::cursor::Show);
let _ = stdout.execute(crossterm::terminal::LeaveAlternateScreen);
let _ = stdout.flush();
drain_stdin();
}
fn drain_stdin() {
use std::time::Duration;
std::thread::sleep(Duration::from_millis(50));
while crossterm::event::poll(Duration::from_millis(10)).unwrap_or(false) {
let _ = crossterm::event::read();
}
}
#[instrument(name = "cuenv_real_main")]
async fn real_main() -> Result<(), CliError> {
if cli::try_complete() {
return Ok(());
}
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 && args[1] == "__hook-supervisor" {
return run_hook_supervisor(args).await;
}
if args.len() > 1 && args[1] == "__coordinator" {
return run_coordinator().await;
}
let init_result = match initialize_cli_and_tracing().await {
Ok(result) => result,
Err(e) => {
return Err(CliError::config_with_help(
format!("Failed to parse CLI arguments: {e}"),
"Check your command line arguments and try again",
));
}
};
if init_result.cli.llms {
#[allow(clippy::print_stdout)] {
print!("{LLMS_CONTENT}");
}
return Ok(());
}
let Some(cli_command) = init_result.cli.command else {
return Err(CliError::config_with_help(
"No subcommand provided",
"Run 'cuenv --help' for usage information",
));
};
if let cli::Commands::Completions { shell } = &cli_command {
cli::generate_completions(*shell);
return Ok(());
}
let executor = create_executor(cli_command.package());
let command: Command = cli_command.into_command(init_result.cli.environment.clone());
let json_format = OutputFormat::from_json_flag(init_result.cli.json);
let result = execute_command_safe(command, json_format, &executor).await;
cuenv_events::emit_shutdown!();
if let Some(handle) = init_result.renderer_handle {
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), handle).await;
}
tracing::shutdown_global_events();
result
}
struct InitResult {
cli: cli::Cli,
renderer_handle: Option<tokio::task::JoinHandle<()>>,
}
fn create_executor(package: &str) -> Arc<CommandExecutor> {
let (event_sender, _event_receiver) = tokio::sync::mpsc::unbounded_channel();
Arc::new(CommandExecutor::new(event_sender, package.to_string()))
}
#[instrument(name = "cuenv_initialize_cli_and_tracing")]
async fn initialize_cli_and_tracing() -> Result<InitResult, CliError> {
let cli = parse();
let trace_format = if cli.json {
TracingFormat::Json
} else {
TracingFormat::Pretty
};
let log_level = match cli.level {
tracing::LogLevel::Trace => Level::TRACE,
tracing::LogLevel::Debug => Level::DEBUG,
tracing::LogLevel::Info => Level::INFO,
tracing::LogLevel::Warn => Level::WARN,
tracing::LogLevel::Error => Level::ERROR,
};
let tracing_config = TracingConfig {
format: trace_format,
level: log_level,
..Default::default()
};
let receiver = match tracing::init_tracing_with_events(tracing_config) {
Ok(rx) => rx,
Err(e) => {
return Err(CliError::config(format!(
"Failed to initialize tracing: {e}"
)));
}
};
let tui_mode = matches!(&cli.command, Some(cli::Commands::Task { tui: true, .. }));
let renderer_handle = if cli.json {
let renderer = JsonRenderer::new();
Some(tokio::spawn(async move {
renderer.run(receiver).await;
}))
} else if tui_mode {
drop(receiver);
None
} else {
let renderer = CliRenderer::new();
Some(tokio::spawn(async move {
renderer.run(receiver).await;
}))
};
Ok(InitResult {
cli,
renderer_handle,
})
}
#[allow(clippy::too_many_lines)]
#[instrument(name = "cuenv_execute_command_safe", skip(executor))]
async fn execute_command_safe(
command: Command,
json_format: cli::OutputFormat,
executor: &CommandExecutor,
) -> Result<(), CliError> {
match &command {
Command::Tui => {
return execute_tui_command()
.await
.map_err(|e| CliError::other(e.to_string()));
}
Command::Web { port, host } => {
return execute_web_command(*port, host.clone())
.await
.map_err(|e| CliError::other(e.to_string()));
}
Command::Completions { shell } => {
cli::generate_completions(*shell);
return Ok(());
}
Command::SecretsSetup { provider, wasm_url } => {
return commands::secrets::execute_secrets_setup(*provider, wasm_url.as_deref());
}
Command::RuntimeOciActivate => {
return run_oci_activate().await;
}
Command::ToolsDownload => {
return commands::tools::execute_tools_download().await;
}
Command::ToolsActivate => {
return commands::tools::execute_tools_activate();
}
Command::ToolsList => {
return commands::tools::execute_tools_list();
}
Command::Build {
path,
package,
names,
labels,
} => {
let options = commands::build::BuildOptions {
path: path.clone(),
package: package.clone(),
names: names.clone(),
labels: labels.clone(),
};
return commands::build::execute_build(&options, executor)
.map_err(|e| CliError::eval(format!("Build command failed: {e}")));
}
Command::Up {
path,
package,
services,
labels,
environment,
} => {
let options = commands::up::UpOptions {
path: path.clone(),
package: package.clone(),
services: services.clone(),
labels: labels.clone(),
environment: environment.clone(),
};
return commands::up::execute_up(&options, executor)
.await
.map_err(|e| CliError::eval(format!("Up command failed: {e}")));
}
Command::Down {
path,
package,
services,
} => {
let options = commands::down::DownOptions {
path: path.clone(),
package: package.clone(),
services: services.clone(),
};
return commands::down::execute_down(&options)
.map(|_| ())
.map_err(|e| CliError::eval(format!("Down command failed: {e}")));
}
Command::Logs {
path,
package,
services,
follow,
lines,
} => {
let options = commands::logs::LogsOptions {
path: path.clone(),
package: package.clone(),
services: services.clone(),
follow: *follow,
lines: *lines,
};
return commands::logs::execute_logs(&options)
.map(|_| ())
.map_err(|e| CliError::eval(format!("Logs command failed: {e}")));
}
Command::Ps {
path,
package,
output_format,
} => {
let options = commands::ps::PsOptions {
path: path.clone(),
package: package.clone(),
output_format: output_format.clone(),
};
return commands::ps::execute_ps(&options)
.map(|_| ())
.map_err(|e| CliError::eval(format!("Ps command failed: {e}")));
}
Command::Restart {
path,
package,
services,
} => {
let options = commands::restart::RestartOptions {
path: path.clone(),
package: package.clone(),
services: services.clone(),
};
return commands::restart::execute_restart(&options)
.map(|_| ())
.map_err(|e| CliError::eval(format!("Restart command failed: {e}")));
}
Command::Info {
path,
package,
meta,
} => {
let options = commands::info::InfoOptions {
path: path.as_deref(),
package,
json_output: json_format.is_json(),
with_meta: *meta,
};
return match commands::info::execute_info(options) {
Ok(output) => {
cuenv_events::print_redacted(&output);
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Info command failed: {e}"),
"Check that you are in a CUE module with valid env.cue files",
)),
};
}
Command::ChangesetAdd {
path,
summary,
description,
packages,
} => {
return execute_changeset_add_safe(
path.clone(),
summary.clone(),
description.clone(),
packages.clone(),
json_format,
)
.await;
}
Command::ChangesetStatus { path, json } => {
let merged_format = OutputFormat::from_json_flag(*json || json_format.is_json());
return execute_changeset_status_safe(path.clone(), merged_format).await;
}
Command::ChangesetFromCommits { path, since } => {
return execute_changeset_from_commits_safe(path.clone(), since.clone(), json_format)
.await;
}
Command::ReleasePrepare {
path,
since,
dry_run,
branch,
no_pr,
} => {
return execute_release_prepare_safe(
path.clone(),
since.clone(),
*dry_run,
branch.clone(),
*no_pr,
json_format,
)
.await;
}
Command::ReleaseVersion { path, dry_run } => {
return execute_release_version_safe(path.clone(), *dry_run, json_format).await;
}
Command::ReleasePublish { path, dry_run } => {
return execute_release_publish_safe(path.clone(), *dry_run, json_format).await;
}
Command::ReleaseBinaries {
path,
dry_run,
backends,
build_only,
package_only,
publish_only,
targets,
version,
} => {
use commands::release::{ReleaseBinariesOptions, ReleaseBinariesPhase};
let phase = if *build_only {
ReleaseBinariesPhase::Build
} else if *package_only {
ReleaseBinariesPhase::Package
} else if *publish_only {
ReleaseBinariesPhase::Publish
} else {
ReleaseBinariesPhase::Full
};
let opts = ReleaseBinariesOptions::new(path.clone())
.with_dry_run(*dry_run)
.with_backends(backends.clone())
.with_phase(phase)
.with_targets(targets.clone())
.with_version(version.clone());
return execute_release_binaries_safe(opts, json_format).await;
}
_ => {}
}
executor.execute(command).await.map_err(|e| {
let cli_err: CliError = e.into();
cli_err.with_help("Run with --help for usage information")
})
}
#[instrument(name = "cuenv_execute_tui")]
async fn execute_tui_command() -> Result<(), CliError> {
use coordinator::client::CoordinatorClient;
use coordinator::protocol::UiType;
let Ok(mut client) = CoordinatorClient::connect_as_consumer(UiType::Tui).await else {
return Err(CliError::other(
"No cuenv coordinator is running.\n\n\
The TUI connects to an event coordinator to display events from other cuenv commands.\n\
To use the TUI:\n\
1. Run a cuenv command (e.g., 'cuenv t') in another terminal\n\
2. Then run 'cuenv tui' to watch the events\n\n\
Note: The coordinator is started automatically when running task commands."
.to_string(),
));
};
cuenv_events::emit_command_started!("tui");
match tui::run_event_viewer(&mut client).await {
Ok(()) => {
cuenv_events::emit_command_completed!("tui", true, 0_u64);
Ok(())
}
Err(e) => {
cuenv_events::emit_command_completed!("tui", false, 0_u64);
Err(CliError::other(format!("TUI error: {e}")))
}
}
}
#[instrument(name = "cuenv_execute_web")]
async fn execute_web_command(port: u16, host: String) -> Result<(), CliError> {
cuenv_events::emit_command_started!("web");
cuenv_events::emit_stdout!(format!(
"Web server would start on http://{}:{}\nThis feature is not yet implemented.",
host, port
));
cuenv_events::emit_command_completed!("web", true, 0_u64);
Ok(())
}
#[instrument(name = "cuenv_execute_shell_init_safe")]
fn execute_shell_init_command_safe(
shell: cli::ShellType,
json_format: cli::OutputFormat,
) -> Result<(), CliError> {
let output = commands::hooks::execute_shell_init(shell);
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({
"script": output
}));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => return Err(CliError::other(format!("JSON serialization failed: {e}"))),
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
async fn run_coordinator() -> Result<(), CliError> {
use coordinator::server::EventCoordinator;
let coordinator = EventCoordinator::new();
coordinator
.run()
.await
.map_err(|e| CliError::other(format!("Coordinator failed: {e}")))
}
async fn run_oci_activate() -> Result<(), CliError> {
use cuenv_core::lockfile::{ArtifactKind, Lockfile};
use cuenv_tools_oci::{OciCache, OciClient, current_platform};
use std::collections::HashSet;
let lockfile_path = find_lockfile().ok_or_else(|| {
CliError::config_with_help(
"No cuenv.lock found",
"Run 'cuenv sync lock' to create the lockfile",
)
})?;
let lockfile = Lockfile::load(&lockfile_path)
.map_err(|e| CliError::other(format!("Failed to load lockfile: {e}")))?
.ok_or_else(|| {
CliError::config_with_help(
"Lockfile is empty",
"Run 'cuenv sync lock' to populate the lockfile",
)
})?;
let platform = current_platform();
let platform_str = platform.to_string();
let client = OciClient::new();
let cache = OciCache::default();
cache
.ensure_dirs()
.map_err(|e| CliError::other(format!("Failed to create cache directories: {e}")))?;
let mut bin_dirs: HashSet<PathBuf> = HashSet::new();
for artifact in &lockfile.artifacts {
let Some(platform_data) = artifact.platforms.get(&platform_str) else {
continue;
};
match &artifact.kind {
ArtifactKind::Image { image } => {
let digest = &platform_data.digest;
let binary_name = extract_binary_name_from_image(image);
if let Some(cached_path) = cache.get_binary(digest, &binary_name) {
if let Some(parent) = cached_path.parent() {
bin_dirs.insert(parent.to_path_buf());
}
continue;
}
let resolved = client.resolve_digest(image, &platform).await.map_err(|e| {
CliError::other(format!("Failed to resolve '{}': {}", image, e))
})?;
let layer_paths = client.pull_layers(&resolved, &cache).await.map_err(|e| {
CliError::other(format!("Failed to pull layers for '{}': {}", image, e))
})?;
if layer_paths.is_empty() {
#[allow(clippy::print_stderr)] {
eprintln!("Warning: OCI image '{}' has no layers to extract", image);
}
continue;
}
let dest = cache.binary_path(digest, &binary_name);
if let Some(parent) = dest.parent() {
bin_dirs.insert(parent.to_path_buf());
}
}
}
}
if !bin_dirs.is_empty() {
let path_additions: Vec<String> =
bin_dirs.iter().map(|p| p.display().to_string()).collect();
#[allow(clippy::print_stdout)] {
println!("export PATH=\"{}:$PATH\"", path_additions.join(":"));
}
}
Ok(())
}
fn find_lockfile() -> Option<PathBuf> {
use cuenv_core::lockfile::LOCKFILE_NAME;
let mut current = std::env::current_dir().ok()?;
loop {
let lockfile_path = current.join(LOCKFILE_NAME);
if lockfile_path.exists() {
return Some(lockfile_path);
}
let cue_mod_lockfile = current.join("cue.mod").join(LOCKFILE_NAME);
if cue_mod_lockfile.exists() {
return Some(cue_mod_lockfile);
}
if !current.pop() {
return None;
}
}
}
fn extract_binary_name_from_image(image: &str) -> String {
let without_tag = image.split(':').next().unwrap_or(image);
let without_digest = without_tag.split('@').next().unwrap_or(without_tag);
without_digest
.rsplit('/')
.next()
.unwrap_or("binary")
.to_string()
}
#[allow(clippy::too_many_lines)]
async fn run_hook_supervisor(args: Vec<String>) -> Result<(), CliError> {
let mut directory_path = PathBuf::new();
let mut instance_hash = String::new();
let mut hooks_file = PathBuf::new();
let mut config_file = PathBuf::new();
let mut i = 2; while i < args.len() {
match args[i].as_str() {
"--directory" => {
directory_path = PathBuf::from(&args[i + 1]);
i += 2;
}
"--instance-hash" => {
instance_hash.clone_from(&args[i + 1]);
i += 2;
}
"--config-hash" => {
i += 2;
}
"--hooks-file" => {
hooks_file = PathBuf::from(&args[i + 1]);
i += 2;
}
"--config-file" => {
config_file = PathBuf::from(&args[i + 1]);
i += 2;
}
_ => i += 1,
}
}
let _ = tracing_subscriber::fmt()
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
.with_writer(std::io::stderr)
.try_init();
if let Err(e) = std::env::set_current_dir(&directory_path) {
cuenv_events::emit_supervisor_log!(
"supervisor",
format!(
"Failed to change directory to {}: {}",
directory_path.display(),
e
)
);
return Err(CliError::other(format!("Failed to change directory: {e}")));
}
cuenv_events::emit_supervisor_log!("supervisor", format!("Starting with args: {args:?}"));
cuenv_events::emit_supervisor_log!(
"supervisor",
format!("Directory: {}", directory_path.display())
);
cuenv_events::emit_supervisor_log!("supervisor", format!("Instance hash: {instance_hash}"));
cuenv_events::emit_supervisor_log!(
"supervisor",
format!("Hooks file: {}", hooks_file.display())
);
cuenv_events::emit_supervisor_log!(
"supervisor",
format!("Config file: {}", config_file.display())
);
let hooks_json = std::fs::read_to_string(&hooks_file)
.map_err(|e| CliError::other(format!("Failed to read hooks file: {e}")))?;
let config_json = std::fs::read_to_string(&config_file)
.map_err(|e| CliError::other(format!("Failed to read config file: {e}")))?;
let hooks: Vec<Hook> = serde_json::from_str(&hooks_json)
.map_err(|e| CliError::other(format!("Failed to deserialize hooks: {e}")))?;
let config: HookExecutionConfig = serde_json::from_str(&config_json)
.map_err(|e| CliError::other(format!("Failed to deserialize config: {e}")))?;
std::fs::remove_file(&hooks_file).ok();
std::fs::remove_file(&config_file).ok();
let state_dir = match config.state_dir.clone() {
Some(dir) => dir,
None => StateManager::default_state_dir()
.map_err(|e| CliError::other(format!("failed to get default state dir: {e}")))?,
};
cuenv_events::emit_supervisor_log!(
"supervisor",
format!("Using state dir: {}", state_dir.display())
);
let state_manager = StateManager::new(state_dir);
let state_file = state_manager.get_state_file_path(&instance_hash);
cuenv_events::emit_supervisor_log!(
"supervisor",
format!("Looking for state file: {}", state_file.display())
);
let pid_file = state_file.with_extension("pid");
std::fs::write(&pid_file, format!("{}", std::process::id()))
.map_err(|e| CliError::other(format!("Failed to write PID file: {e}")))?;
let mut state = state_manager
.load_state(&instance_hash)
.await
.map_err(|e| CliError::other(format!("Failed to load state: {e}")))?
.ok_or_else(|| CliError::other("State not found for supervisor"))?;
cuenv_events::emit_supervisor_log!(
"supervisor",
format!(
"Executing {} hooks for directory: {}",
hooks.len(),
directory_path.display()
)
);
let result = execute_hooks(hooks, &directory_path, &config, &state_manager, &mut state).await;
if let Err(e) = result {
cuenv_events::emit_supervisor_log!("supervisor", format!("Hook execution failed: {e}"));
state.status = ExecutionStatus::Failed;
state.error_message = Some(e.to_string());
state.finished_at = Some(chrono::Utc::now());
state_manager
.save_state(&state)
.await
.map_err(|e| CliError::other(format!("Failed to save error state: {e}")))?;
return Err(CliError::other(format!("Hook execution failed: {e}")));
}
cuenv_events::emit_supervisor_log!(
"supervisor",
format!(
"Saving final state with {} environment variables",
state.environment_vars.len()
)
);
state_manager
.save_state(&state)
.await
.map_err(|e| CliError::other(format!("Failed to save final state: {e}")))?;
std::fs::remove_file(&pid_file).ok();
cuenv_events::emit_supervisor_log!("supervisor", "Completed successfully");
Ok(())
}
#[instrument(name = "cuenv_execute_changeset_add_safe")]
async fn execute_changeset_add_safe(
path: String,
summary: Option<String>,
description: Option<String>,
packages: Vec<(String, String)>,
json_format: cli::OutputFormat,
) -> Result<(), CliError> {
match commands::release::execute_changeset_add(
&path,
&packages,
summary.as_deref(),
description.as_deref(),
) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({
"message": output
}));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!("JSON serialization failed: {e}")));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Changeset add failed: {e}"),
"Check package names and bump types (major, minor, patch)",
)),
}
}
#[instrument(name = "cuenv_execute_changeset_status_safe")]
async fn execute_changeset_status_safe(
path: String,
json_format: cli::OutputFormat,
) -> Result<(), CliError> {
match commands::release::execute_changeset_status_with_format(&path, json_format) {
Ok(output) => {
cuenv_events::println_redacted(&output);
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Changeset status failed: {e}"),
"Check that the path is valid",
)),
}
}
#[instrument(name = "cuenv_execute_changeset_from_commits_safe")]
async fn execute_changeset_from_commits_safe(
path: String,
since: Option<String>,
json_format: cli::OutputFormat,
) -> Result<(), CliError> {
match commands::release::execute_changeset_from_commits(&path, since.as_deref()) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({
"message": output
}));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!("JSON serialization failed: {e}")));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Changeset from-commits failed: {e}"),
"Check that the path is a valid git repository",
)),
}
}
#[instrument(name = "cuenv_execute_release_prepare_safe")]
async fn execute_release_prepare_safe(
path: String,
since: Option<String>,
dry_run: cuenv_core::DryRun,
branch: String,
no_pr: bool,
json_format: cli::OutputFormat,
) -> Result<(), CliError> {
let opts = commands::release::ReleasePrepareOptions {
path,
since,
dry_run,
branch,
no_pr,
};
match commands::release::execute_release_prepare(&opts) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({
"result": output
}));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!("JSON serialization failed: {e}")));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Release prepare failed: {e}"),
"Check git history and workspace configuration",
)),
}
}
#[instrument(name = "cuenv_execute_release_version_safe")]
async fn execute_release_version_safe(
path: String,
dry_run: cuenv_core::DryRun,
json_format: cli::OutputFormat,
) -> Result<(), CliError> {
match commands::release::execute_release_version(&path, dry_run) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({
"result": output
}));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!("JSON serialization failed: {e}")));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Release version failed: {e}"),
"Create changesets first with 'cuenv changeset add'",
)),
}
}
#[instrument(name = "cuenv_execute_release_publish_safe")]
async fn execute_release_publish_safe(
path: String,
dry_run: cuenv_core::DryRun,
json_format: cli::OutputFormat,
) -> Result<(), CliError> {
let format = if json_format.is_json() {
commands::release::OutputFormat::Json
} else {
commands::release::OutputFormat::Human
};
match commands::release::execute_release_publish(&path, dry_run, format) {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({
"result": output
}));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!("JSON serialization failed: {e}")));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Release publish failed: {e}"),
"Check that packages are ready for publishing",
)),
}
}
async fn execute_release_binaries_safe(
opts: commands::release::ReleaseBinariesOptions,
json_format: cli::OutputFormat,
) -> Result<(), CliError> {
match commands::release::execute_release_binaries(opts).await {
Ok(output) => {
if json_format.is_json() {
let envelope = OkEnvelope::new(serde_json::json!({
"result": output
}));
match serde_json::to_string(&envelope) {
Ok(json) => cuenv_events::println_redacted(&json),
Err(e) => {
return Err(CliError::other(format!("JSON serialization failed: {e}")));
}
}
} else {
cuenv_events::println_redacted(&output);
}
Ok(())
}
Err(e) => Err(CliError::eval_with_help(
format!("Release binaries failed: {e}"),
"Check that binaries are built and artifacts directory exists",
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_panic_hook() {
let _ = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let _ = std::panic::take_hook();
}
#[test]
fn test_cli_args_json_flag() {
let cli_args = ["cuenv".to_string(), "--json".to_string()];
let json_flag = cli_args.iter().any(|arg| arg == "--json");
assert!(json_flag);
}
#[test]
fn test_cli_args_level_flag() {
let cli_args = [
"cuenv".to_string(),
"--level".to_string(),
"debug".to_string(),
];
let level_flag = cli_args.windows(2).find_map(|args| {
if args[0] == "--level" || args[0] == "-l" {
Some(args[1].as_str())
} else {
None
}
});
assert_eq!(level_flag, Some("debug"));
}
#[test]
fn test_trace_format_selection() {
let json_flag = true;
let trace_format = if json_flag {
TracingFormat::Json
} else {
TracingFormat::Pretty
};
assert!(matches!(trace_format, TracingFormat::Json));
let json_flag = false;
let trace_format = if json_flag {
TracingFormat::Json
} else {
TracingFormat::Pretty
};
assert!(matches!(trace_format, TracingFormat::Pretty));
}
#[test]
fn test_log_level_parsing() {
let test_cases = vec![
(Some("trace"), Level::TRACE),
(Some("debug"), Level::DEBUG),
(Some("info"), Level::INFO),
(Some("warn"), Level::WARN),
(Some("error"), Level::ERROR),
(None, Level::WARN), (Some("invalid"), Level::WARN), ];
for (input, expected) in test_cases {
let log_level = match input {
Some("trace") => Level::TRACE,
Some("debug") => Level::DEBUG,
Some("info") => Level::INFO,
Some("error") => Level::ERROR,
_ => Level::WARN,
};
assert_eq!(log_level, expected);
}
}
#[test]
fn test_tracing_config_default() {
let tracing_config = TracingConfig {
format: TracingFormat::Dev,
level: Level::WARN,
..Default::default()
};
assert!(matches!(tracing_config.format, TracingFormat::Dev));
assert_eq!(tracing_config.level, Level::WARN);
}
#[tokio::test]
async fn test_command_conversion() {
use cli::{Commands, OutputFormat};
let cli_command = Commands::Version {
output_format: OutputFormat::Text,
};
let command: Command = cli_command.into_command(None);
match command {
Command::Version { format } => assert_eq!(format, "text"),
_ => panic!("Expected Command::Version"),
}
}
}