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