Skip to main content

grex_cli/cli/verbs/
teardown.rs

1//! `grex teardown` — drive the M5-2b pack-type teardown lifecycle.
2//!
3//! Thin CLI glue mirroring [`crate::cli::verbs::sync`]. Walks the pack
4//! tree, then invokes [`grex_core::sync::teardown`] which dispatches
5//! `PackTypePlugin::teardown` on every pack in reverse post-order.
6//!
7//! Exit codes are shared with `sync` (1 validation, 2 exec, 3 tree).
8//! See `crates/grex/src/cli/verbs/sync.rs` for the rationale — the
9//! teardown verb reuses the same halted-context renderer so operators
10//! see a consistent post-mortem regardless of which lifecycle halted.
11
12use crate::cli::args::{GlobalFlags, TeardownArgs};
13use anyhow::Result;
14use grex_core::sync::{self, HaltedContext, SyncError, SyncOptions, SyncReport, SyncStep};
15use tokio_util::sync::CancellationToken;
16
17/// Entry point for the `teardown` verb.
18///
19/// # Errors
20///
21/// Surfaces `anyhow::Result` so `main` can render the orchestrator's
22/// output; exit codes are set via `std::process::exit` on halt paths
23/// (same pattern as `sync` since `anyhow::Error` does not carry them).
24pub fn run(args: TeardownArgs, global: &GlobalFlags, cancel: &CancellationToken) -> Result<()> {
25    let Some(pack_root) = args.pack_root.clone() else {
26        // Missing required positional → usage error. Mirrors `sync`'s
27        // fall-through (see that verb for the rationale).
28        if global.json {
29            super::sync::emit_json_error(
30                "usage",
31                "`<pack_root>` is required (directory with `.grex/pack.yaml` or the YAML file)",
32                "teardown",
33            );
34        } else {
35            eprintln!(
36                "grex teardown: <pack_root> required (directory with `.grex/pack.yaml` or the YAML file)"
37            );
38        }
39        std::process::exit(2);
40    };
41    let opts = SyncOptions::new()
42        .with_dry_run(global.dry_run)
43        .with_validate(!args.no_validate)
44        .with_workspace(args.workspace.clone());
45    match run_impl(&pack_root, &opts, args.quiet, global.json, cancel) {
46        RunOutcome::Ok => Ok(()),
47        RunOutcome::Validation => std::process::exit(1),
48        RunOutcome::Exec => std::process::exit(2),
49        RunOutcome::Tree => std::process::exit(3),
50    }
51}
52
53enum RunOutcome {
54    Ok,
55    Validation,
56    Exec,
57    Tree,
58}
59
60fn run_impl(
61    pack_root: &std::path::Path,
62    opts: &SyncOptions,
63    quiet: bool,
64    json: bool,
65    cancel: &CancellationToken,
66) -> RunOutcome {
67    match sync::teardown(pack_root, opts, cancel) {
68        Ok(report) => {
69            if json {
70                super::sync::emit_json_report(&report, opts.dry_run, "teardown");
71            } else {
72                render_report(&report, quiet);
73            }
74            if report.halted.is_some() {
75                return RunOutcome::Exec;
76            }
77            RunOutcome::Ok
78        }
79        Err(err) => map_sync_outcome(super::sync::classify_sync_err(err, json, "teardown")),
80    }
81}
82
83/// Narrow `sync`'s [`super::sync::RunOutcome`] (which carries a
84/// `UsageError` variant for `--only` glob errors) down to teardown's
85/// smaller enum. Teardown doesn't accept `--only`, so `UsageError`
86/// would be unreachable; we collapse it into `Tree` defensively.
87fn map_sync_outcome(o: super::sync::RunOutcome) -> RunOutcome {
88    match o {
89        super::sync::RunOutcome::Ok => RunOutcome::Ok,
90        super::sync::RunOutcome::Validation => RunOutcome::Validation,
91        super::sync::RunOutcome::Exec => RunOutcome::Exec,
92        super::sync::RunOutcome::Tree | super::sync::RunOutcome::UsageError => RunOutcome::Tree,
93    }
94}
95
96fn print_halted_context(ctx: &HaltedContext) {
97    eprintln!(
98        "teardown halted at pack `{}` action #{} ({}):",
99        ctx.pack, ctx.action_idx, ctx.action_name
100    );
101    eprintln!("  error: {}", ctx.error);
102    if let Some(hint) = &ctx.recovery_hint {
103        eprintln!("  hint:  {hint}");
104    }
105}
106
107fn render_report(report: &SyncReport, quiet: bool) {
108    if !quiet {
109        for s in &report.steps {
110            print_step(s);
111        }
112    }
113    for w in &report.event_log_warnings {
114        eprintln!("warning: {w}");
115    }
116    if let Some(err) = &report.halted {
117        match err {
118            SyncError::Halted(ctx) => print_halted_context(ctx),
119            other => eprintln!("halted: {other}"),
120        }
121    }
122}
123
124fn print_step(s: &SyncStep) {
125    use grex_core::ExecResult;
126    let tag = match &s.exec_step.result {
127        ExecResult::PerformedChange => "ok",
128        ExecResult::WouldPerformChange => "would",
129        ExecResult::AlreadySatisfied => "skipped",
130        ExecResult::NoOp => "noop",
131        _ => "other",
132    };
133    println!(
134        "[{tag}] pack={pack} action={kind} idx={idx}",
135        pack = s.pack,
136        kind = s.exec_step.action_name,
137        idx = s.action_idx,
138    );
139}