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}