intelli_shell/service/
mod.rs

1use std::{
2    env,
3    path::{Path, PathBuf},
4    sync::{Arc, Mutex},
5};
6
7use tokio::fs::File;
8use tracing::instrument;
9
10use crate::{
11    config::{AiConfig, SearchTuning},
12    errors::Result,
13    model::{CATEGORY_WORKSPACE, SOURCE_WORKSPACE},
14    service::import::parse_import_items,
15    storage::SqliteStorage,
16    utils::get_working_dir,
17};
18
19mod ai;
20mod command;
21mod completion;
22mod export;
23mod import;
24mod variable;
25mod version;
26
27#[cfg(feature = "tldr")]
28mod tldr;
29
30pub use ai::AiFixProgress;
31pub use completion::{FORBIDDEN_COMPLETION_ROOT_CMD_CHARS, FORBIDDEN_COMPLETION_VARIABLE_CHARS};
32#[cfg(feature = "tldr")]
33pub use tldr::{RepoStatus, TldrFetchProgress};
34
35/// Service for managing user commands in IntelliShell
36#[derive(Clone)]
37pub struct IntelliShellService {
38    check_updates: bool,
39    storage: SqliteStorage,
40    tuning: SearchTuning,
41    ai: AiConfig,
42    #[cfg(feature = "tldr")]
43    tldr_repo_path: PathBuf,
44    version_check_state: Arc<Mutex<version::VersionCheckState>>,
45}
46
47impl IntelliShellService {
48    /// Creates a new instance of `IntelliShellService`
49    pub fn new(
50        storage: SqliteStorage,
51        tuning: SearchTuning,
52        ai: AiConfig,
53        data_dir: impl AsRef<Path>,
54        check_updates: bool,
55    ) -> Self {
56        Self {
57            check_updates,
58            storage,
59            tuning,
60            ai,
61            #[cfg(feature = "tldr")]
62            tldr_repo_path: data_dir.as_ref().join("tldr"),
63            version_check_state: Arc::new(Mutex::new(version::VersionCheckState::NotStarted)),
64        }
65    }
66
67    #[cfg(debug_assertions)]
68    pub async fn query(&self, sql: String) -> crate::errors::Result<String> {
69        self.storage.query(sql).await
70    }
71
72    /// Loads workspace commands and completions from the `.intellishell` file in the current working directory setting
73    /// up the temporary tables in the database if they don't exist.
74    ///
75    /// Returns whether a workspace file was processed or not
76    #[instrument(skip_all)]
77    pub async fn load_workspace_items(&self) -> Result<bool> {
78        if env::var("INTELLI_SKIP_WORKSPACE")
79            .map(|v| v != "1" && v.to_lowercase() != "true")
80            .unwrap_or(true)
81            && let Some((workspace_file, folder_name)) = find_workspace_file()
82        {
83            tracing::debug!("Found workspace file at {}", workspace_file.display());
84
85            // Set up the temporary tables in the database
86            self.storage.setup_workspace_storage().await?;
87
88            // Parse the items from the file
89            let file = File::open(&workspace_file).await?;
90            let tag = format!("#{}", folder_name.as_deref().unwrap_or("workspace"));
91            let items_stream = parse_import_items(file, vec![tag], CATEGORY_WORKSPACE, SOURCE_WORKSPACE);
92
93            // Import items into the temp tables
94            let stats = self.storage.import_items(items_stream, false, true).await?;
95
96            tracing::info!(
97                "Loaded {} commands and {} completions from workspace {}",
98                stats.commands_imported,
99                stats.completions_imported,
100                workspace_file.display()
101            );
102
103            Ok(true)
104        } else {
105            Ok(false)
106        }
107    }
108}
109
110/// Searches upwards from the current working dir for a `.intellishell` file.
111///
112/// The search stops if a `.git` directory or the filesystem root is found.
113/// Returns a tuple of (file_path, folder_name) if found.
114fn find_workspace_file() -> Option<(PathBuf, Option<String>)> {
115    let working_dir = PathBuf::from(get_working_dir());
116    let mut current = Some(working_dir.as_path());
117    while let Some(parent) = current {
118        let candidate = parent.join(".intellishell");
119        if candidate.is_file() {
120            let folder_name = parent.file_name().and_then(|n| n.to_str()).map(String::from);
121            return Some((candidate, folder_name));
122        }
123
124        if parent.join(".git").is_dir() {
125            // Workspace boundary found
126            return None;
127        }
128
129        current = parent.parent();
130    }
131    None
132}