1use crate::cli::{Cli, Commands};
2use crate::context::CliContext;
3use crate::handlers;
4use clap::{CommandFactory, FromArgMatches};
5use kanban_core::AppConfig;
6use kanban_persistence::{StoreFactory, StoreRegistry};
7use kanban_service::StoreManager;
8#[cfg(feature = "tui")]
9use kanban_tui::App;
10
11fn open_debug_log_file() -> Option<std::fs::File> {
12 std::env::var("KANBAN_DEBUG_LOG").ok().and_then(|log_path| {
13 std::fs::OpenOptions::new()
14 .create(true)
15 .append(true)
16 .open(&log_path)
17 .ok()
18 })
19}
20
21fn init_tracing_cli() {
22 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
23
24 if let Some(log_file) = open_debug_log_file() {
25 tracing_subscriber::registry()
26 .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")))
27 .with(
28 tracing_subscriber::fmt::layer()
29 .with_writer(log_file)
30 .with_ansi(false)
31 .with_target(true)
32 .with_thread_ids(true)
33 .with_file(true)
34 .with_line_number(true),
35 )
36 .try_init()
37 .ok();
38 return;
39 }
40 tracing_subscriber::registry()
41 .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")))
42 .with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
43 .try_init()
44 .ok();
45}
46
47#[cfg(feature = "tui")]
48fn init_tracing_tui(
49 error_log: std::sync::Arc<std::sync::Mutex<kanban_tui::error_log::ErrorLogState>>,
50) {
51 use kanban_tui::error_log::InMemoryLogLayer;
52 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
53
54 let in_memory = InMemoryLogLayer::new(error_log);
55 if let Some(log_file) = open_debug_log_file() {
56 tracing_subscriber::registry()
57 .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("debug")))
58 .with(
59 tracing_subscriber::fmt::layer()
60 .with_writer(log_file)
61 .with_ansi(false)
62 .with_target(true)
63 .with_thread_ids(true)
64 .with_file(true)
65 .with_line_number(true),
66 )
67 .with(in_memory)
68 .try_init()
69 .ok();
70 return;
71 }
72 tracing_subscriber::registry()
73 .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")))
74 .with(in_memory)
75 .try_init()
76 .ok();
77}
78
79fn parse_cli<I, T>(store_manager: &StoreManager, args: I) -> anyhow::Result<(Cli, clap::Command)>
80where
81 I: IntoIterator<Item = T>,
82 T: Into<std::ffi::OsString> + Clone,
83{
84 let backend_names: Vec<String> = store_manager
85 .backend_names()
86 .into_iter()
87 .map(str::to_owned)
88 .collect();
89 let mut cmd = Cli::command().mut_subcommand("migrate", |sub| {
90 sub.mut_arg("backend", |arg| {
91 arg.value_parser(clap::builder::PossibleValuesParser::new(
92 backend_names.clone(),
93 ))
94 })
95 });
96 let matches = cmd.try_get_matches_from_mut(args)?;
97 let cli = Cli::from_arg_matches(&matches)?;
98 Ok((cli, cmd))
99}
100
101async fn dispatch_subcommand(ctx: &mut CliContext, cmd: Commands) -> anyhow::Result<()> {
102 match cmd {
103 Commands::Board(board_cmd) => {
104 handlers::board::handle(ctx, board_cmd.action).await?;
105 }
106 Commands::Column(column_cmd) => {
107 handlers::column::handle(ctx, column_cmd.action).await?;
108 }
109 Commands::Card(card_cmd) => {
110 handlers::card::handle(ctx, card_cmd.action).await?;
111 }
112 Commands::Sprint(sprint_cmd) => {
113 handlers::sprint::handle(ctx, sprint_cmd.action).await?;
114 }
115 Commands::Export(args) => {
116 handlers::export::handle_export(ctx, args).await?;
117 }
118 Commands::Import(args) => {
119 handlers::export::handle_import(ctx, args).await?;
120 }
121 Commands::Completions { .. } | Commands::Migrate(_) => unreachable!(),
122 }
123 Ok(())
124}
125
126pub struct CliApp {
132 registry: StoreRegistry,
133 config: Option<AppConfig>,
134}
135
136impl Default for CliApp {
137 fn default() -> Self {
141 Self {
142 registry: StoreRegistry::new(),
143 config: None,
144 }
145 }
146}
147
148impl CliApp {
149 pub fn with_defaults() -> Self {
154 #[cfg(any(feature = "json", feature = "sqlite"))]
155 let registry = kanban_service::default_registry();
156 #[cfg(not(any(feature = "json", feature = "sqlite")))]
157 let registry = kanban_persistence::StoreRegistry::new();
158 Self {
159 registry,
160 config: None,
161 }
162 }
163
164 pub fn register_backend(mut self, factory: Box<dyn StoreFactory>) -> Self {
198 self.registry.register(factory);
199 self
200 }
201
202 pub fn with_config(mut self, config: AppConfig) -> Self {
204 self.config = Some(config);
205 self
206 }
207
208 pub fn registry(&self) -> &StoreRegistry {
210 &self.registry
211 }
212
213 pub async fn run(self) -> anyhow::Result<()> {
216 self.run_with_args(std::env::args_os()).await
217 }
218
219 pub async fn run_with_args<I, T>(self, args: I) -> anyhow::Result<()>
223 where
224 I: IntoIterator<Item = T>,
225 T: Into<std::ffi::OsString> + Clone,
226 {
227 let store_manager = StoreManager::new(self.registry);
228 let (Cli { command, file }, mut cmd) = parse_cli(&store_manager, args)?;
229
230 if let Some(Commands::Completions { shell }) = command {
231 clap_complete::generate(shell, &mut cmd, "kanban", &mut std::io::stdout());
232 return Ok(());
233 }
234
235 if !store_manager.has_backends() {
236 anyhow::bail!(
237 "No storage backends registered. \
238 Use CliApp::with_defaults() or call register_backend() before run()."
239 );
240 }
241
242 let config = self.config.unwrap_or_else(kanban_service::config::load);
243 let validated_file: Option<String> = match file {
244 Some(ref p) => Some(
245 kanban_service::validate_path(std::path::Path::new(p))?
246 .to_string_lossy()
247 .to_string(),
248 ),
249 None => None,
250 };
251 let effective_file = validated_file
252 .clone()
253 .unwrap_or_else(|| kanban_service::config::resolve_storage_location(&config));
254
255 match command {
256 None => {
257 #[cfg(feature = "tui")]
258 {
259 let error_log = std::sync::Arc::new(std::sync::Mutex::new(
260 kanban_tui::error_log::ErrorLogState::default(),
261 ));
262 init_tracing_tui(std::sync::Arc::clone(&error_log));
263
264 let (mut app, save_rx) =
265 App::new_with_store(store_manager, validated_file).await?;
266 app.set_error_log(error_log);
267 app.run(save_rx).await?;
268 }
269 #[cfg(not(feature = "tui"))]
270 anyhow::bail!(
271 "TUI not available in this build. Run `kanban --help` for available subcommands."
272 );
273 }
274 Some(Commands::Completions { .. }) => unreachable!(),
275 Some(Commands::Migrate(args)) => {
276 init_tracing_cli();
277 handlers::migrate::handle(&store_manager, args).await?;
278 }
279 Some(cmd) => {
280 init_tracing_cli();
281 let mut ctx = CliContext::load(&store_manager, &effective_file, config).await?;
282 dispatch_subcommand(&mut ctx, cmd).await?;
283 }
284 }
285
286 Ok(())
287 }
288}