intelli_shell/service/
mod.rs1use std::{
2 collections::HashSet,
3 env,
4 path::{Path, PathBuf},
5 sync::{Arc, Mutex},
6};
7
8use directories::BaseDirs;
9use tokio::fs::File;
10use tracing::instrument;
11use walkdir::WalkDir;
12
13use crate::{
14 config::{AiConfig, SearchTuning},
15 errors::Result,
16 model::{CATEGORY_WORKSPACE, SOURCE_WORKSPACE},
17 service::import::parse_import_items,
18 storage::SqliteStorage,
19 utils::get_working_dir,
20};
21
22mod ai;
23mod command;
24mod completion;
25mod export;
26mod import;
27mod variable;
28mod version;
29
30#[cfg(feature = "tldr")]
31mod tldr;
32
33pub use ai::AiFixProgress;
34pub use completion::{FORBIDDEN_COMPLETION_ROOT_CMD_CHARS, FORBIDDEN_COMPLETION_VARIABLE_CHARS};
35#[cfg(feature = "tldr")]
36pub use tldr::{RepoStatus, TldrFetchProgress};
37
38#[derive(Clone)]
40pub struct IntelliShellService {
41 check_updates: bool,
42 storage: SqliteStorage,
43 tuning: SearchTuning,
44 ai: AiConfig,
45 #[cfg(feature = "tldr")]
46 tldr_repo_path: PathBuf,
47 version_check_state: Arc<Mutex<version::VersionCheckState>>,
48}
49
50impl IntelliShellService {
51 pub fn new(
53 storage: SqliteStorage,
54 tuning: SearchTuning,
55 ai: AiConfig,
56 data_dir: impl AsRef<Path>,
57 check_updates: bool,
58 ) -> Self {
59 Self {
60 check_updates,
61 storage,
62 tuning,
63 ai,
64 #[cfg(feature = "tldr")]
65 tldr_repo_path: data_dir.as_ref().join("tldr"),
66 version_check_state: Arc::new(Mutex::new(version::VersionCheckState::NotStarted)),
67 }
68 }
69
70 #[cfg(debug_assertions)]
71 pub async fn query(&self, sql: String) -> crate::errors::Result<String> {
72 self.storage.query(sql).await
73 }
74
75 #[instrument(skip_all)]
87 pub async fn load_workspace_items(&self) -> Result<bool> {
88 if !env::var("INTELLI_SKIP_WORKSPACE")
89 .map(|v| v != "1" && v.to_lowercase() != "true")
90 .unwrap_or(true)
91 {
92 tracing::info!("Skipping workspace load due to INTELLI_SKIP_WORKSPACE");
93 return Ok(false);
94 }
95
96 let workspace_files = find_workspace_files();
98 if workspace_files.is_empty() {
99 tracing::debug!("No workspace files were found");
100 return Ok(false);
101 }
102
103 self.storage.setup_workspace_storage().await?;
105
106 for (workspace_file, tag_name) in workspace_files {
108 let file = File::open(&workspace_file).await?;
110 let tag = format!("#{}", tag_name.as_deref().unwrap_or("workspace"));
111 let items_stream = parse_import_items(file, vec![tag], CATEGORY_WORKSPACE, SOURCE_WORKSPACE);
112
113 match self.storage.import_items(items_stream, false, true).await {
115 Ok(stats) => {
116 tracing::info!(
117 "Loaded {} commands and {} completions from workspace file {}",
118 stats.commands_imported,
119 stats.completions_imported,
120 workspace_file.display()
121 );
122 }
123 Err(err) => {
124 tracing::error!("Failed to load workspace file {}", workspace_file.display());
125 return Err(err);
126 }
127 }
128 }
129
130 Ok(true)
131 }
132}
133
134fn find_workspace_files() -> Vec<(PathBuf, Option<String>)> {
147 let mut result = Vec::new();
148 let mut seen_paths = HashSet::new();
149
150 let working_dir = PathBuf::from(get_working_dir());
152 let mut current = Some(working_dir.as_path());
153 tracing::debug!(
154 "Searching for workspace .intellishell file or folder from working dir: {}",
155 working_dir.display()
156 );
157 while let Some(parent) = current {
158 let candidate = parent.join(".intellishell");
159 if candidate.exists() {
160 collect_intellishell_files_from_location(&candidate, &mut seen_paths, &mut result);
161 break;
162 }
163
164 if parent.join(".git").is_dir() {
165 break;
167 }
168
169 current = parent.parent();
170 }
171
172 if let Some(base_dirs) = BaseDirs::new() {
174 let home_dir = base_dirs.home_dir();
175 tracing::debug!(
176 "Searching for .intellishell file or folder in home dir: {}",
177 home_dir.display()
178 );
179 let home_candidate = home_dir.join(".intellishell");
180 if home_candidate.exists() {
181 collect_intellishell_files_from_location(&home_candidate, &mut seen_paths, &mut result);
182 }
183 }
184
185 #[cfg(target_os = "windows")]
187 let system_candidate = PathBuf::from(r"C:\ProgramData\.intellishell");
188 #[cfg(not(target_os = "windows"))]
189 let system_candidate = PathBuf::from("/etc/.intellishell");
190
191 tracing::debug!(
192 "Searching for .intellishell file or folder system-wide: {}",
193 system_candidate.display()
194 );
195 if system_candidate.exists() {
196 collect_intellishell_files_from_location(&system_candidate, &mut seen_paths, &mut result);
197 }
198
199 result
200}
201
202fn collect_intellishell_files_from_location(
210 path: &Path,
211 seen_paths: &mut HashSet<PathBuf>,
212 result: &mut Vec<(PathBuf, Option<String>)>,
213) {
214 if path.is_file() {
215 if seen_paths.insert(path.to_path_buf()) {
217 let folder_name = path
218 .parent()
219 .and_then(|p| p.file_name())
220 .and_then(|n| n.to_str())
221 .map(String::from);
222 result.push((path.to_path_buf(), folder_name));
223 } else {
224 tracing::trace!("Skipping duplicate workspace file: {}", path.display());
225 }
226 } else if path.is_dir() {
227 for entry in WalkDir::new(path).min_depth(1).into_iter().filter_map(|e| e.ok()) {
230 let entry_path = entry.path();
231 let file_name = entry.file_name().to_string_lossy();
232 if entry_path.is_file() && !file_name.starts_with('.') {
234 if seen_paths.insert(entry_path.to_path_buf()) {
235 let tag = entry_path.file_stem().and_then(|n| n.to_str()).map(String::from);
236 result.push((entry_path.to_path_buf(), tag));
237 } else {
238 tracing::trace!("Skipping duplicate workspace file: {}", entry_path.display());
239 }
240 }
241 }
242 }
243}