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