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}