use tracing::{debug, info};
use crate::commands::{
handler_description, handler_symbol, status_style, DisplayFile, DisplayPack, PackStatusResult,
};
use crate::conflicts;
use crate::datastore::format_command_for_display;
use crate::operations::HandlerIntent;
use crate::packs::orchestration::{self, ExecutionContext, PackResult};
use crate::shell;
use crate::Result;
pub fn up(pack_filter: Option<&[String]>, ctx: &ExecutionContext) -> Result<PackStatusResult> {
info!(
dry_run = ctx.dry_run,
force = ctx.force,
no_provision = ctx.no_provision,
"starting up command"
);
let packs = orchestration::prepare_packs(pack_filter, ctx)?;
let mut pack_intents: Vec<(String, Vec<HandlerIntent>)> = Vec::with_capacity(packs.len());
let mut intent_errors: Vec<PackResult> = Vec::new();
for pack in &packs {
match orchestration::collect_pack_intents(pack, ctx) {
Ok(intents) => {
pack_intents.push((pack.name.clone(), intents));
}
Err(e) => {
info!(pack = %pack.name, error = %e, "intent collection failed");
intent_errors.push(PackResult {
pack_name: pack.name.clone(),
success: false,
operations: Vec::new(),
error: Some(format!("intent collection error: {e}")),
});
}
}
}
info!("checking for cross-pack conflicts");
let conflicts = conflicts::detect_cross_pack_conflicts(&pack_intents, ctx.fs.as_ref());
if !conflicts.is_empty() {
info!(count = conflicts.len(), "cross-pack conflicts detected");
return Err(crate::DodotError::CrossPackConflict { conflicts });
}
debug!("no cross-pack conflicts");
let mut pack_results: Vec<PackResult> = intent_errors;
for (pack_name, intents) in pack_intents {
info!(pack = %pack_name, intents = intents.len(), "executing pack");
match orchestration::execute_intents(intents, ctx) {
Ok(operations) => {
let success = operations.iter().all(|r| r.success);
let succeeded = operations.iter().filter(|o| o.success).count();
let failed = operations.iter().filter(|o| !o.success).count();
debug!(pack = %pack_name, succeeded, failed, "pack execution complete");
pack_results.push(PackResult {
pack_name,
success,
operations,
error: None,
});
}
Err(e) => {
info!(pack = %pack_name, error = %e, "pack execution failed");
pack_results.push(PackResult {
pack_name,
success: false,
operations: Vec::new(),
error: Some(format!("execution error: {e}")),
});
}
}
}
if !ctx.dry_run {
info!("regenerating shell init script");
shell::write_init_script(ctx.fs.as_ref(), ctx.paths.as_ref())?;
}
let home = ctx.paths.home_dir();
let display_packs: Vec<DisplayPack> = pack_results
.iter()
.map(|pr| {
let mut files: Vec<DisplayFile> = pr
.operations
.iter()
.map(|op| {
let (handler, name, user_target) = extract_op_info(&op.operation, home);
let status = if op.success {
status_style(true).into()
} else {
"error".into()
};
DisplayFile {
name: name.clone(),
symbol: handler_symbol(&handler).into(),
description: handler_description(&handler, &name, user_target.as_deref()),
status,
status_label: op.message.clone(),
handler,
}
})
.collect();
if let Some(err) = &pr.error {
files.push(DisplayFile {
name: String::new(),
symbol: "×".into(),
description: String::new(),
status: "error".into(),
status_label: err.clone(),
handler: String::new(),
});
}
DisplayPack {
name: pr.pack_name.clone(),
files,
}
})
.collect();
let has_failures = pack_results
.iter()
.any(|pr| !pr.success || pr.operations.iter().any(|op| !op.success));
let message = if has_failures {
"Packs deployed with errors.".into()
} else {
"Packs deployed.".into()
};
Ok(PackStatusResult {
message: Some(message),
dry_run: ctx.dry_run,
packs: display_packs,
warnings: Vec::new(),
conflicts: Vec::new(),
ignored_packs: Vec::new(),
})
}
fn extract_op_info(
op: &crate::operations::Operation,
home: &std::path::Path,
) -> (String, String, Option<String>) {
match op {
crate::operations::Operation::CreateDataLink {
handler, source, ..
} => (
handler.clone(),
source
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
None,
),
crate::operations::Operation::CreateUserLink {
handler,
datastore_path,
user_path,
..
} => {
let name = datastore_path
.file_name()
.unwrap_or_else(|| user_path.file_name().unwrap_or_default())
.to_string_lossy()
.into_owned();
let target = if let Ok(rel) = user_path.strip_prefix(home) {
format!("~/{}", rel.display())
} else {
user_path.display().to_string()
};
(handler.clone(), name, Some(target))
}
crate::operations::Operation::RunCommand {
handler,
executable,
arguments,
..
} => (
handler.clone(),
format_command_for_display(executable, arguments),
None,
),
crate::operations::Operation::CheckSentinel {
handler, sentinel, ..
} => (handler.clone(), sentinel.clone(), None),
}
}