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