use super::tree;
use crate::args::Args;
use crate::failure::Failure;
use crate::output::outln;
use crate::style::Stream;
pub(super) fn run(cwd: &std::path::Path, a: &Args) -> Result<i32, Failure> {
let roots = super::resolve_roots(cwd)?;
if a.has("undo") {
return undo(&roots, a);
}
if let Some(refusal) = super::refuse_home_or_global_store(&roots) {
return Err(refusal);
}
apply(&roots, a)
}
fn apply(roots: &super::Roots, a: &Args) -> Result<i32, Failure> {
let below = tree::trees_below(&roots.here);
let join_spec = a.opt("join");
if !below.is_empty() {
return Err(match join_spec {
Some(spec) => super::claude_code::tree_below_join_refusal(&roots.here, &below, spec),
None => tree::tree_below_refusal(&below),
});
}
if let Some(spec) = join_spec {
return match tree::plan_join(roots, spec, a.opt("lane-name"))? {
Some((join_roots, plan)) => apply_writes(&join_roots, a, plan),
None => Ok(0),
};
}
let plan = tree::plan(roots, a)?;
apply_writes(roots, a, plan)
}
fn full_plan(here: &std::path::Path, plan: &tree::TreePlan) -> String {
let mut s = super::claude_code::heading(Stream::Out, "vivac init", here);
if let Some(name) = &plan.name_collision {
s.push_str(&super::claude_code::name_collision_paragraph(name));
}
let mut items = tree::opening_items(plan);
items.extend(tree::closing_items(plan));
s.push_str(&super::claude_code::render_items(Stream::Out, &items));
s
}
fn warning_paragraphs(plan: &tree::TreePlan) -> Vec<String> {
let mut v = Vec::new();
if !plan.unknown_product_warning.is_empty() {
v.push(plan.unknown_product_warning.clone());
}
if plan.log_tracked {
v.push(super::claude_code::tracked_git_warning());
}
if let Some(w) = &plan.above_warning {
v.push(w.clone());
}
v
}
fn apply_writes(roots: &super::Roots, a: &Args, plan: tree::TreePlan) -> Result<i32, Failure> {
let here = &roots.here;
let nothing_to_write = !plan.vivac_missing
&& !plan.gitignore_missing
&& plan.lane.unchanged
&& !plan.lane.needs_lock
&& plan.lane.stale_worktrees.is_empty();
let mut blocks = vec![full_plan(here, &plan).trim_end_matches('\n').to_string()];
blocks.extend(warning_paragraphs(&plan));
let plan_text = format!("{}\n\n", blocks.join("\n\n"));
if a.has("dry-run") {
outln!(
"{}",
super::claude_code::close_with(&plan_text, super::claude_code::DRY_RUN_LINE)
);
return Ok(0);
}
if nothing_to_write {
tree::note_registry(roots);
outln!(
"{}",
super::claude_code::close_with(
&plan_text,
"Nothing to write: this project is already set up."
)
);
return Ok(0);
}
if !a.has("yes") && !super::stdin_is_terminal() {
return Err(Failure::Model(super::init_no_terminal_text(a)));
}
super::claude_code::print_plan(&plan_text);
let proceed = a.has("yes") || super::ask("\nWrite it? [y/N] ");
if !proceed {
outln!("\nNothing written.");
return Ok(0);
}
let writes = tree::file_writes(roots, &plan);
super::commit(&writes)?;
tree::commit(roots, &plan, &writes)?;
tree::note_registry(roots);
tree::note_name(&plan);
print!("\n{}", written_text(&plan));
Ok(0)
}
fn written_text(plan: &tree::TreePlan) -> String {
let mut paragraphs = vec![crate::style::good(Stream::Out, "Written.")];
if !plan.vivac_missing {
let lane_declared = !plan.lane.unchanged || !plan.lane.stale_worktrees.is_empty();
paragraphs.push(super::claude_code::tree_paragraph(
"init",
plan.gitignore_missing,
lane_declared,
plan.lane.needs_lock,
));
}
paragraphs.push(next_agent_block());
format!("{}\n", paragraphs.join("\n\n"))
}
fn next_agent_block() -> String {
format!(
"{} set up the agent you use\n\n {}\n {}",
crate::style::bold(Stream::Out, "Next:"),
crate::style::bold(Stream::Out, "vivac setup claude-code"),
crate::style::bold(Stream::Out, "vivac setup codex")
)
}
fn undo(roots: &super::Roots, a: &Args) -> Result<i32, Failure> {
let here = roots.here.as_path();
let undo_lane = tree::undo_lane(roots)?;
if undo_lane.raw.is_none() {
if crate::store::already_planted(here) {
return undo_bare_tree(roots, a);
}
outln!("Nothing to undo: this folder carries no lane vivac init wrote.");
return Ok(0);
}
let mut items = tree::vivac_dir_items(&undo_lane);
items.extend(tree::undo_lane_items(&undo_lane));
let mut s = super::claude_code::heading(Stream::Out, "vivac init --undo", here);
s.push_str(&super::claude_code::render_items(Stream::Out, &items));
if a.has("dry-run") {
outln!(
"{}",
super::claude_code::close_with(&s, super::claude_code::DRY_RUN_LINE)
);
return Ok(0);
}
if !undo_lane.removable {
outln!(
"{}",
super::claude_code::close_with(
&s,
"Nothing written: this lane has written to the tree."
)
);
return Ok(0);
}
if !a.has("yes") && !super::stdin_is_terminal() {
return Err(Failure::Model(super::init_no_terminal_text(a)));
}
super::claude_code::print_plan(&s);
let proceed = a.has("yes") || super::ask("\nUndo it? [y/N] ");
if !proceed {
outln!("\nNothing written.");
return Ok(0);
}
let mut writes = vec![super::PlannedWrite::delete(
undo_lane.path.clone(),
undo_lane.raw.clone().unwrap_or_default(),
)];
if undo_lane.vivac_dir_removable {
let gitignore = undo_lane.vivac_dir.join(crate::store::GITIGNORE);
if let Ok(original) = std::fs::read(&gitignore) {
writes.push(super::PlannedWrite::delete(gitignore, original));
}
}
super::commit(&writes)?;
if undo_lane.vivac_dir_removable {
super::claude_code::remove_if_empty(Some(&undo_lane.vivac_dir));
}
outln!("\nUndone. The tree in .vivac/ is untouched.");
Ok(0)
}
fn undo_bare_tree(roots: &super::Roots, a: &Args) -> Result<i32, Failure> {
let here = roots.here.as_path();
let vivac_dir = here.join(crate::store::DIR);
let store = crate::store::Store::open(roots.tree.clone())?;
let (events, _) = store.read_all()?;
let writes = crate::session::capture_count(&events);
if writes > 0 {
outln!(
"Nothing to undo: the tree in .vivac/ already holds work ({writes} writes), and \
init --undo never removes a tree that does."
);
return Ok(0);
}
let items = vec![
super::claude_code::PlanItem::new(
"remove",
tree::VIVAC_LABEL,
"the tree init planted here: it holds no work yet",
),
super::claude_code::PlanItem::new("forget", "~/.vivac/projects", "this project"),
];
let mut s = super::claude_code::heading(Stream::Out, "vivac init --undo", here);
s.push_str(&super::claude_code::render_items(Stream::Out, &items));
if a.has("dry-run") {
outln!(
"{}",
super::claude_code::close_with(&s, super::claude_code::DRY_RUN_LINE)
);
return Ok(0);
}
if !a.has("yes") && !super::stdin_is_terminal() {
return Err(Failure::Model(super::init_no_terminal_text(a)));
}
super::claude_code::print_plan(&s);
let proceed = a.has("yes") || super::ask("\nUndo it? [y/N] ");
if !proceed {
outln!("\nNothing written.");
return Ok(0);
}
let project_id = crate::store::first_event_id(&roots.tree);
std::fs::remove_dir_all(&vivac_dir).map_err(|e| {
Failure::Io(std::io::Error::other(format!(
"{} could not be removed ({e})",
vivac_dir.display()
)))
})?;
if let (Some(store_dir), Some(project_id)) = (crate::store::store_dir(), project_id) {
crate::registry::forget(&store_dir, &project_id);
}
outln!("\nUndone. There is no tree here any more.");
Ok(0)
}