Skip to main content

degenbot_cli_core/
command.rs

1//! The constructor-parsed command model (ADR-051 D1).
2//!
3//! One `Command` enum, one execution entry. The argv facade maps clap matches
4//! into a `Command` value; it never re-encodes semantics.
5
6use crate::aave::{self, AaveCommand};
7use crate::cancel::CancelHandle;
8use crate::config::{self, ConfigCommand};
9use crate::context::CliContext;
10use crate::database;
11use crate::error::CliError;
12use crate::exchange::{self, ExchangeCommand};
13use crate::fleet::{self, FleetCommand};
14use crate::path::{self, PathCommand};
15use crate::pool::{self, PoolCommand};
16use crate::prompt::{PromptPlan, Prompter};
17use crate::report::CommandReport;
18use crate::strategy::{self, StrategyCommand};
19
20/// A console command.
21#[derive(Debug, Clone, PartialEq)]
22pub enum Command {
23    /// The `database` command group.
24    Database(DatabaseCommand),
25    /// The `exchange` command group.
26    Exchange(ExchangeCommand),
27    /// The `pool` command group.
28    Pool(PoolCommand),
29    /// The `aave` command group.
30    Aave(AaveCommand),
31    /// The `fleet` command group (ADR-051 D6).
32    Fleet(FleetCommand),
33    /// The `path` command group (ADR-051 D6).
34    Path(PathCommand),
35    /// The `strategy` command group (ADR-055 facets).
36    Strategy(StrategyCommand),
37    /// The `config` command group (ADR-062 D6).
38    Config(ConfigCommand),
39}
40
41impl Command {
42    /// The command's declared confirmation policy (ADR-051 D4).
43    #[must_use]
44    pub fn prompt_plan(&self, ctx: &CliContext<'_>) -> PromptPlan {
45        match self {
46            Self::Database(command) => command.prompt_plan(ctx),
47            Self::Exchange(command) => command.prompt_plan(ctx),
48            Self::Pool(command) => command.prompt_plan(ctx),
49            Self::Aave(command) => command.prompt_plan(ctx),
50            Self::Fleet(command) => command.prompt_plan(ctx),
51            Self::Path(command) => command.prompt_plan(ctx),
52            Self::Strategy(command) => command.prompt_plan(ctx),
53            Self::Config(command) => command.prompt_plan(ctx),
54        }
55    }
56
57    /// Execute the command, asking `prompter` per the command's [`PromptPlan`].
58    ///
59    /// The updater arms thread a fresh [`CancelHandle`]; use
60    /// [`execute_with_cancel`](Self::execute_with_cancel) to share the facade's
61    /// SIGINT-driven handle.
62    ///
63    /// # Errors
64    ///
65    /// [`CliError`] for a declined prompt, a database failure, a schema refusal,
66    /// an unresolved driver-domain value, or a core arm failure.
67    pub fn execute(
68        &self,
69        ctx: &CliContext<'_>,
70        prompter: &dyn Prompter,
71    ) -> Result<CommandReport, CliError> {
72        self.execute_with_cancel(ctx, prompter, &CancelHandle::new())
73    }
74
75    /// Execute the command with the caller's [`CancelHandle`] (ADR-051 D7: the
76    /// facade installs the SIGINT policy and hands the handle in).
77    ///
78    /// # Errors
79    ///
80    /// As [`execute`](Self::execute).
81    pub fn execute_with_cancel(
82        &self,
83        ctx: &CliContext<'_>,
84        prompter: &dyn Prompter,
85        cancel: &CancelHandle,
86    ) -> Result<CommandReport, CliError> {
87        match self {
88            Self::Database(command) => {
89                database::execute(command, ctx, prompter).map(CommandReport::Database)
90            }
91            Self::Exchange(command) => {
92                exchange::execute(command, ctx, prompter).map(CommandReport::Exchange)
93            }
94            Self::Pool(command) => {
95                pool::execute(command, ctx, prompter, cancel).map(CommandReport::Pool)
96            }
97            Self::Aave(command) => {
98                aave::execute(command, ctx, prompter, cancel).map(CommandReport::Aave)
99            }
100            Self::Fleet(command) => fleet::execute(command, ctx).map(CommandReport::Fleet),
101            Self::Path(command) => path::execute(command, ctx).map(CommandReport::Path),
102            Self::Strategy(command) => {
103                strategy::execute(command, ctx, prompter).map(CommandReport::Strategy)
104            }
105            Self::Config(command) => {
106                config::execute(command.clone(), ctx, prompter).map(CommandReport::Config)
107            }
108        }
109    }
110}
111
112/// The `database` command group (the template group; ports
113/// `src/degenbot/cli/database.py` arm for arm).
114#[derive(Debug, Clone, PartialEq, Eq)]
115pub enum DatabaseCommand {
116    /// Back up the database to its `.db.bak` sibling. Confirms replacement only
117    /// when the target already exists.
118    Backup,
119    /// Remove and recreate the database (confirms unless `--force`).
120    Reset {
121        /// Skip the confirmation prompt.
122        force: bool,
123    },
124    /// RETIRED: render the pointed-retirement error and exit 1. The database
125    /// upgrades itself at open (ADR-052).
126    Upgrade {
127        /// Accepted for argv parity; the retirement error fires before any prompt.
128        force: bool,
129    },
130    /// Compact the database (`VACUUM`); never prompts.
131    Compact,
132    /// Flip an Alembic-stamped DB into Rust schema ownership (ADR-010).
133    Cutover {
134        /// Report the schema state and what cutover would do; write nothing.
135        dry_run: bool,
136        /// Skip the confirmation prompt.
137        force: bool,
138    },
139    /// Rebuild an Alembic-stamped DB into Rust ownership out-of-place (ADR-011).
140    Heal {
141        /// Report the schema state and what heal would do; write nothing.
142        dry_run: bool,
143        /// Skip the confirmation prompt.
144        force: bool,
145    },
146    /// Read-only schema-state inspection; never triggers a write.
147    Inspect,
148}
149
150impl DatabaseCommand {
151    /// The arm's declared confirmation policy — ported verbatim from the click
152    /// handlers (backup: replace-target-exists; reset/cutover/heal: unless-force;
153    /// compact/inspect: none).
154    ///
155    /// `upgrade` reports [`PromptPlan::UnlessForce`] for argv parity with the
156    /// retired handler, but its arm returns [`CliError::DatabaseUpgradeRetired`]
157    /// before any prompt is reached.
158    #[must_use]
159    pub fn prompt_plan(&self, ctx: &CliContext<'_>) -> PromptPlan {
160        match self {
161            Self::Backup => match ctx.database_path() {
162                Ok(path) => {
163                    PromptPlan::OnCondition(database::database_backup_path(&path.value).exists())
164                }
165                // A context whose config layers do not load must not SKIP the
166                // confirmation: ask, and let the arm fail loudly with the
167                // loader's own message.
168                Err(_) => PromptPlan::UnlessForce,
169            },
170            Self::Reset { .. }
171            | Self::Upgrade { .. }
172            | Self::Cutover { .. }
173            | Self::Heal { .. } => PromptPlan::UnlessForce,
174            Self::Compact | Self::Inspect => PromptPlan::None,
175        }
176    }
177}