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