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    crate::cli::deprecation::warn_workspace_alias_used();
26    // v1.3.1 B2 — defer to the shared cwd-default helper so `grex
27    // teardown` run from inside a pack root no longer requires the
28    // operator to repeat the path. Mirrors `sync`'s resolution order.
29    let Some(pack_root) = super::resolve_pack_root_or_cwd(args.pack_root.as_deref()) else {
30        // Missing required positional → usage error. Mirrors `sync`'s
31        // fall-through (see that verb for the rationale).
32        if global.json {
33            super::sync::emit_json_error(
34                "usage",
35                "`<pack_root>` is required (directory with `.grex/pack.yaml` or the YAML file)",
36                "teardown",
37            );
38        } else {
39            eprintln!(
40                "grex teardown: <pack_root> required (directory with `.grex/pack.yaml` or the YAML file)"
41            );
42        }
43        std::process::exit(2);
44    };
45    let opts = SyncOptions::new()
46        .with_dry_run(global.dry_run)
47        .with_validate(!args.no_validate)
48        .with_workspace(args.pack.clone());
49    match run_impl(&pack_root, &opts, args.quiet, global.json, cancel) {
50        RunOutcome::Ok => Ok(()),
51        RunOutcome::Validation => std::process::exit(1),
52        RunOutcome::Exec => std::process::exit(2),
53        RunOutcome::Tree => std::process::exit(3),
54    }
55}
56
57enum RunOutcome {
58    Ok,
59    Validation,
60    Exec,
61    Tree,
62}
63
64fn run_impl(
65    pack_root: &std::path::Path,
66    opts: &SyncOptions,
67    quiet: bool,
68    json: bool,
69    cancel: &CancellationToken,
70) -> RunOutcome {
71    match sync::teardown(pack_root, opts, cancel) {
72        Ok(report) => {
73            if json {
74                super::sync::emit_json_report(&report, opts.dry_run, "teardown");
75            } else {
76                render_report(&report, quiet);
77            }
78            if report.halted.is_some() {
79                return RunOutcome::Exec;
80            }
81            RunOutcome::Ok
82        }
83        Err(err) => map_sync_outcome(super::sync::classify_sync_err(err, json, "teardown")),
84    }
85}
86
87/// Narrow `sync`'s [`super::sync::RunOutcome`] (which carries a
88/// `UsageError` variant for `--only` glob errors) down to teardown's
89/// smaller enum. Teardown doesn't accept `--only`, so `UsageError`
90/// would be unreachable; we collapse it into `Tree` defensively.
91fn map_sync_outcome(o: super::sync::RunOutcome) -> RunOutcome {
92    match o {
93        super::sync::RunOutcome::Ok => RunOutcome::Ok,
94        super::sync::RunOutcome::Validation => RunOutcome::Validation,
95        super::sync::RunOutcome::Exec => RunOutcome::Exec,
96        super::sync::RunOutcome::Tree | super::sync::RunOutcome::UsageError => RunOutcome::Tree,
97    }
98}
99
100fn print_halted_context(ctx: &HaltedContext) {
101    eprintln!(
102        "teardown halted at pack `{}` action #{} ({}):",
103        ctx.pack, ctx.action_idx, ctx.action_name
104    );
105    eprintln!("  error: {}", ctx.error);
106    if let Some(hint) = &ctx.recovery_hint {
107        eprintln!("  hint:  {hint}");
108    }
109}
110
111fn render_report(report: &SyncReport, quiet: bool) {
112    if !quiet {
113        for s in &report.steps {
114            print_step(s);
115        }
116    }
117    for w in &report.event_log_warnings {
118        eprintln!("warning: {w}");
119    }
120    if let Some(err) = &report.halted {
121        match err {
122            SyncError::Halted(ctx) => print_halted_context(ctx),
123            other => eprintln!("halted: {other}"),
124        }
125    }
126}
127
128fn print_step(s: &SyncStep) {
129    use grex_core::ExecResult;
130    let tag = match &s.exec_step.result {
131        ExecResult::PerformedChange => "ok",
132        ExecResult::WouldPerformChange => "would",
133        ExecResult::AlreadySatisfied => "skipped",
134        ExecResult::NoOp => "noop",
135        _ => "other",
136    };
137    println!(
138        "[{tag}] pack={pack} action={kind} idx={idx}",
139        pack = s.pack,
140        kind = s.exec_step.action_name,
141        idx = s.action_idx,
142    );
143}