Skip to main content

kimun_notes/cli/
mod.rs

1// tui/src/cli/mod.rs
2pub mod commands;
3pub mod helpers;
4pub mod json_output;
5pub mod metadata_extractor;
6pub mod output;
7
8use clap::Subcommand;
9use color_eyre::eyre::{Result, eyre};
10use commands::JournalArgs;
11use commands::note_ops::NoteSubcommand;
12use commands::workspace::WorkspaceSubcommand;
13use helpers::{
14    create_and_init_vault, load_and_resolve_workspace, load_settings, resolve_inbox_path,
15    resolve_quick_note_path,
16};
17use kimun_core::{NoteVault, VaultConfig};
18use output::OutputFormat;
19
20#[derive(Subcommand)]
21pub enum CliCommand {
22    /// Search notes by query
23    Search {
24        query: String,
25        #[arg(long, value_enum, default_value = "text")]
26        format: OutputFormat,
27    },
28    /// List all notes
29    Notes {
30        #[arg(long, help = "Filter notes by path prefix")]
31        path: Option<String>,
32        #[arg(long, value_enum, default_value = "text")]
33        format: OutputFormat,
34    },
35    /// Manage workspaces
36    Workspace {
37        #[command(subcommand)]
38        subcommand: WorkspaceSubcommand,
39    },
40    /// Note operations (create, append, show)
41    Note {
42        #[command(subcommand)]
43        subcommand: NoteSubcommand,
44    },
45    /// Append to or show journal entries
46    Journal(JournalArgs),
47    /// Report what this terminal can deliver, and every key binding's fate
48    Doctor,
49    /// Start the MCP server (stdio transport)
50    Mcp,
51    /// List all hashtag labels in the vault with note counts
52    Labels {
53        #[arg(long, value_enum, default_value = "text")]
54        format: OutputFormat,
55    },
56    /// Check for a newer release and, where possible, self-update
57    Update {
58        /// Only check and report; do not download or install
59        #[arg(long)]
60        check: bool,
61    },
62}
63
64pub async fn run_cli(command: CliCommand, config_path: Option<std::path::PathBuf>) -> Result<()> {
65    match command {
66        CliCommand::Workspace { subcommand } => {
67            let mut settings = load_settings(config_path)?;
68            commands::workspace::run(subcommand, &mut settings).await
69        }
70        CliCommand::Note { subcommand } => {
71            let (settings, workspace_path, workspace_name) =
72                load_and_resolve_workspace(config_path)?;
73            let quick_note_path = resolve_quick_note_path(&settings);
74            let inbox_path = resolve_inbox_path(&settings);
75            let cache_path = settings.index_for(&workspace_name);
76            let mut vault = NoteVault::new(
77                VaultConfig::new(workspace_path)
78                    .with_index(cache_path)
79                    .with_backup(true),
80            )
81            .await?;
82            vault.set_inbox_path(kimun_core::nfs::VaultPath::new(&inbox_path));
83            // Every one-shot command closes its vault before returning: the
84            // command owns that vault for its whole life, and dropping a pool
85            // only schedules the close, so anything the same process does next
86            // (a `workspace rename`/`remove`, a test driving several commands
87            // in a row) would find the cache file still open — fatal on
88            // Windows, which will not move or delete an open file.
89            let result = if vault.index_ready() {
90                commands::note_ops::run(subcommand, &vault, &quick_note_path, &workspace_name).await
91            } else {
92                Err(eyre!(
93                    "Workspace index is not ready.\nRun `kimun workspace reindex` to initialise it."
94                ))
95            };
96            vault.close().await;
97            result
98        }
99        CliCommand::Search { query, format } => {
100            let (vault, workspace_name) = create_and_init_vault(config_path).await?;
101            let result =
102                commands::search::run(&vault, &query, format, &workspace_name, false).await;
103            vault.close().await;
104            result
105        }
106        CliCommand::Notes { path, format } => {
107            let (vault, workspace_name) = create_and_init_vault(config_path).await?;
108            let result =
109                commands::notes::run(&vault, path.as_deref(), format, &workspace_name, false).await;
110            vault.close().await;
111            result
112        }
113        CliCommand::Journal(args) => {
114            let (vault, workspace_name) = create_and_init_vault(config_path).await?;
115            let result = commands::journal::run(args, &vault, &workspace_name).await;
116            vault.close().await;
117            result
118        }
119        // Vault-independent: reads the config and asks the terminal, nothing
120        // more. Opening a workspace here would make a diagnostic depend on the
121        // thing being diagnosed.
122        CliCommand::Doctor => commands::doctor::run(config_path),
123        CliCommand::Mcp => commands::mcp::run(config_path).await,
124        CliCommand::Labels { format } => {
125            let (vault, workspace_name) = create_and_init_vault(config_path).await?;
126            let result = commands::labels::run(&vault, format, &workspace_name).await;
127            vault.close().await;
128            result
129        }
130        // Update is vault-independent: it talks to GitHub and the app config
131        // dir, not a workspace.
132        CliCommand::Update { check } => commands::update::run(check).await,
133    }
134}