use std::collections::HashMap;
use tracing::{debug, info};
use crate::commands::{
handler_description, handler_symbol, status, status_style, DisplayConflict, DisplayFile,
DisplayNote, DisplayPack, PackStatusResult,
};
use crate::conflicts;
use crate::datastore::format_command_for_display;
use crate::handlers;
use crate::operations::HandlerIntent;
use crate::packs::orchestration::{self, ExecutionContext, PackResult};
use crate::packs::Pack;
use crate::probe;
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.display_name.clone(), intents));
}
Err(e) => {
info!(pack = %pack.display_name, error = %e, "intent collection failed");
intent_errors.push(PackResult {
pack_name: pack.display_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;
let config_handlers = if ctx.dry_run {
Vec::new()
} else {
handlers::configuration_handler_names(ctx.fs.as_ref())
};
let pack_by_display: HashMap<&str, &Pack> =
packs.iter().map(|p| (p.display_name.as_str(), p)).collect();
for (pack_name, intents) in pack_intents {
info!(pack = %pack_name, intents = intents.len(), "executing pack");
if !ctx.dry_run {
let pack = pack_by_display
.get(pack_name.as_str())
.copied()
.expect("pack_intents was built from packs; lookup must succeed");
if let Err(e) = wipe_configuration_state(pack, &config_handlers, ctx) {
info!(pack = %pack_name, error = %e, "reconcile failed");
pack_results.push(PackResult {
pack_name,
success: false,
operations: Vec::new(),
error: Some(format!("reconcile error: {e}")),
});
continue;
}
}
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");
let root_config = ctx.config_manager.root_config()?;
shell::write_init_script(
ctx.fs.as_ref(),
ctx.paths.as_ref(),
root_config.profiling.enabled,
)?;
info!("writing deployment map");
probe::write_deployment_map(ctx.fs.as_ref(), ctx.paths.as_ref())?;
if let Err(e) = probe::write_last_up_marker(ctx.fs.as_ref(), ctx.paths.as_ref()) {
debug!(error = %e, "failed to write last-up marker");
}
let removed = probe::rotate_profiles(
ctx.fs.as_ref(),
ctx.paths.as_ref(),
root_config.profiling.keep_last_runs,
)?;
if removed > 0 {
debug!(removed, "pruned old shell-init profiles");
}
let report = shell::validate_shell_sources(
ctx.fs.as_ref(),
ctx.paths.as_ref(),
ctx.syntax_checker.as_ref(),
)?;
if !report.failures.is_empty() {
info!(
count = report.failures.len(),
"shell syntax check found failures"
);
eprintln!(
"dodot: {} shell file{} failed pre-flight syntax check (see `dodot status`)",
report.failures.len(),
if report.failures.len() == 1 { "" } else { "s" }
);
}
for interp in &report.missing_interpreters {
eprintln!(
"dodot: `{interp}` not on PATH, skipped syntax check for matching shell files"
);
}
}
let has_failures = pack_results
.iter()
.any(|pr| !pr.success || pr.operations.iter().any(|op| !op.success));
let (display_packs, notes) = if ctx.dry_run {
render_intents(&pack_results, ctx.paths.home_dir())
} else {
let pack_names: Vec<String> = packs.iter().map(|p| p.display_name.clone()).collect();
let status_result = status::status(Some(&pack_names), ctx)?;
let mut notes = status_result.notes;
let display_packs = overlay_errors(
status_result.packs,
&pack_results,
ctx.paths.home_dir(),
&mut notes,
);
(display_packs, notes)
};
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(),
notes,
conflicts: Vec::new(),
ignored_packs: Vec::new(),
view_mode: ctx.view_mode.as_str().into(),
group_mode: ctx.group_mode.as_str().into(),
})
}
pub fn up_or_status_for_conflict(
pack_filter: Option<&[String]>,
ctx: &ExecutionContext,
) -> Result<PackStatusResult> {
match up(pack_filter, ctx) {
Ok(r) => Ok(r),
Err(crate::DodotError::CrossPackConflict { conflicts: raw }) => {
let home = ctx.paths.home_dir();
let display_conflicts: Vec<DisplayConflict> = raw
.iter()
.map(|c| DisplayConflict::from_conflict(c, home))
.collect();
let mut base = status::status(pack_filter, ctx)?;
base.message = Some("Cross-pack conflicts prevent deployment.".into());
base.dry_run = ctx.dry_run;
base.conflicts = display_conflicts;
Ok(base)
}
Err(e) => Err(e),
}
}
fn wipe_configuration_state(
pack: &Pack,
config_handlers: &[String],
ctx: &ExecutionContext,
) -> Result<()> {
for handler in config_handlers {
ctx.datastore.remove_state(&pack.name, handler)?;
}
Ok(())
}
fn render_intents(
pack_results: &[PackResult],
home: &std::path::Path,
) -> (Vec<DisplayPack>, Vec<DisplayNote>) {
let mut notes: Vec<DisplayNote> = Vec::new();
let packs = 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, status_label, note_ref) = if op.success {
(status_style(true).to_string(), op.message.clone(), None)
} else {
notes.push(DisplayNote {
body: op.message.clone(),
hint: None,
});
(
"error".to_string(),
"error".to_string(),
Some(notes.len() as u32),
)
};
DisplayFile {
name: name.clone(),
symbol: handler_symbol(&handler).into(),
description: handler_description(&handler, &name, user_target.as_deref()),
status,
status_label,
handler,
note_ref,
}
})
.collect();
if let Some(err) = &pr.error {
notes.push(DisplayNote {
body: err.clone(),
hint: None,
});
files.push(DisplayFile {
name: String::new(),
symbol: "×".into(),
description: String::new(),
status: "error".into(),
status_label: "error".into(),
handler: String::new(),
note_ref: Some(notes.len() as u32),
});
}
DisplayPack::new(pr.pack_name.clone(), files)
})
.collect();
(packs, notes)
}
pub(crate) fn overlay_errors(
mut packs: Vec<DisplayPack>,
pack_results: &[PackResult],
home: &std::path::Path,
notes: &mut Vec<DisplayNote>,
) -> Vec<DisplayPack> {
for pr in pack_results {
let display_pack = match packs.iter_mut().find(|p| p.name == pr.pack_name) {
Some(p) => p,
None => continue,
};
for op_result in &pr.operations {
if op_result.success {
continue;
}
let (handler, name, user_target) = extract_op_info(&op_result.operation, home);
let body = op_result.message.clone();
let pos = display_pack
.files
.iter()
.position(|f| f.handler == handler && f.name == name)
.or_else(|| {
user_target.as_ref().and_then(|ut| {
display_pack
.files
.iter()
.position(|f| f.handler == handler && &f.description == ut)
})
})
.or_else(|| display_pack.files.iter().position(|f| f.name == name));
match pos {
Some(idx) => {
let file = &mut display_pack.files[idx];
if let Some(existing) = file.note_ref {
notes[(existing - 1) as usize] = DisplayNote { body, hint: None };
} else {
notes.push(DisplayNote { body, hint: None });
file.note_ref = Some(notes.len() as u32);
}
file.status = "error".into();
file.status_label = "error".into();
}
None => {
notes.push(DisplayNote { body, hint: None });
display_pack.files.push(DisplayFile {
name: name.clone(),
symbol: handler_symbol(&handler).into(),
description: handler_description(&handler, &name, user_target.as_deref()),
status: "error".into(),
status_label: "error".into(),
handler,
note_ref: Some(notes.len() as u32),
});
}
}
}
if let Some(err) = &pr.error {
let fallback_idx = if display_pack.files.is_empty() {
None
} else {
Some(0)
};
let target_idx = display_pack
.files
.iter()
.position(|f| f.status != "error")
.or(fallback_idx);
let body = err.clone();
match target_idx {
Some(idx) => {
let file = &mut display_pack.files[idx];
if let Some(existing) = file.note_ref {
notes[(existing - 1) as usize] = DisplayNote { body, hint: None };
} else {
notes.push(DisplayNote { body, hint: None });
file.note_ref = Some(notes.len() as u32);
}
file.status = "error".into();
file.status_label = "error".into();
}
None => {
notes.push(DisplayNote { body, hint: None });
display_pack.files.push(DisplayFile {
name: String::new(),
symbol: "×".into(),
description: String::new(),
status: "error".into(),
status_label: "error".into(),
handler: String::new(),
note_ref: Some(notes.len() as u32),
});
}
}
}
}
for pack in &mut packs {
pack.recompute_summary();
}
packs
}
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),
}
}