Skip to main content

grex_cli/cli/verbs/
rm.rs

1//! `grex rm` — tear down a pack and delete its directory.
2//!
3//! Loads the manifest at `<path>/.grex/pack.yaml` to enforce the
4//! meta-with-children guard, then drives the regular teardown
5//! lifecycle (`grex_core::sync::teardown`) to allow per-pack-type
6//! teardown actions to fire, then removes the directory.
7
8use crate::cli::args::{GlobalFlags, RmArgs};
9use anyhow::Result;
10use grex_core::sync::{self, SyncOptions};
11use grex_core::tree::{FsPackLoader, PackLoader};
12use grex_core::PackType;
13use std::path::PathBuf;
14use tokio_util::sync::CancellationToken;
15
16pub fn run(args: RmArgs, global: &GlobalFlags, cancel: &CancellationToken) -> Result<()> {
17    let pack_root = PathBuf::from(&args.path);
18    if !pack_root.exists() {
19        emit_error(global.json, "not_found", &format!("{} does not exist", pack_root.display()));
20        std::process::exit(2);
21    }
22    let manifest = match FsPackLoader::new().load(&pack_root) {
23        Ok(m) => m,
24        Err(err) => {
25            emit_error(global.json, "load_manifest", &err.to_string());
26            std::process::exit(3);
27        }
28    };
29    if matches!(manifest.r#type, PackType::Meta) && !manifest.children.is_empty() && !args.force {
30        let msg = format!(
31            "refusing to remove meta-pack with {} children; pass --force to override",
32            manifest.children.len()
33        );
34        emit_error(global.json, "has_children", &msg);
35        std::process::exit(1);
36    }
37    if !args.force {
38        run_teardown(&pack_root, global, cancel);
39    }
40    if global.dry_run {
41        emit_ok(global.json, &pack_root, true);
42        return Ok(());
43    }
44    if let Err(err) = std::fs::remove_dir_all(&pack_root) {
45        emit_error(global.json, "rmtree", &format!("remove {}: {err}", pack_root.display()));
46        std::process::exit(2);
47    }
48    emit_ok(global.json, &pack_root, false);
49    Ok(())
50}
51
52/// Drive the per-pack-type teardown lifecycle. `--force` callers skip
53/// this — they take responsibility for cleaning up state the teardown
54/// plugin would have unwound. Mirrors the teardown verb's error
55/// routing so exit codes stay consistent across `rm` and `teardown`.
56fn run_teardown(pack_root: &std::path::Path, global: &GlobalFlags, cancel: &CancellationToken) {
57    let opts = SyncOptions::new().with_dry_run(global.dry_run).with_validate(true);
58    if let Err(err) = sync::teardown(pack_root, &opts, cancel) {
59        match super::sync::classify_sync_err(err, global.json, "rm") {
60            super::sync::RunOutcome::Validation => std::process::exit(1),
61            super::sync::RunOutcome::Exec => std::process::exit(2),
62            super::sync::RunOutcome::Tree | super::sync::RunOutcome::UsageError => {
63                std::process::exit(3)
64            }
65            super::sync::RunOutcome::Ok => {}
66        }
67    }
68}
69
70fn emit_ok(json: bool, path: &std::path::Path, dry_run: bool) {
71    if json {
72        let doc = serde_json::json!({
73            "verb": "rm",
74            "status": "ok",
75            "path": path.display().to_string(),
76            "dry_run": dry_run,
77        });
78        println!("{}", serde_json::to_string(&doc).unwrap_or_default());
79    } else {
80        let prefix = if dry_run { "DRY-RUN: would remove" } else { "removed" };
81        println!("{prefix} {}", path.display());
82    }
83}
84
85fn emit_error(json: bool, kind: &str, msg: &str) {
86    if json {
87        let doc = serde_json::json!({
88            "verb": "rm",
89            "error": { "kind": kind, "message": msg },
90        });
91        println!("{}", serde_json::to_string(&doc).unwrap_or_default());
92    } else {
93        eprintln!("grex rm: {msg}");
94    }
95}