use std::{
collections::HashSet,
env,
path::{Path, PathBuf},
sync::{Arc, LazyLock, Mutex},
time::Duration,
};
use directories::BaseDirs;
use semver::Version;
use tokio::fs::File;
use tracing::instrument;
use walkdir::WalkDir;
pub(super) const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
pub(super) const FETCH_INTERVAL: chrono::Duration = chrono::Duration::hours(16);
pub(crate) static CURRENT_VERSION: LazyLock<Version> =
LazyLock::new(|| Version::parse(env!("CARGO_PKG_VERSION")).expect("valid version"));
use crate::{
config::{AiConfig, SearchTuning},
errors::Result,
model::{CATEGORY_WORKSPACE, SOURCE_WORKSPACE},
service::import::parse_import_items,
storage::SqliteStorage,
utils::get_working_dir,
};
mod ai;
mod command;
mod completion;
mod export;
mod import;
mod release;
mod variable;
mod version;
#[cfg(feature = "tldr")]
mod tldr;
pub use ai::AiFixProgress;
pub use completion::{FORBIDDEN_COMPLETION_ROOT_CMD_CHARS, FORBIDDEN_COMPLETION_VARIABLE_CHARS};
#[cfg(feature = "tldr")]
pub use tldr::{RepoStatus, TldrFetchProgress};
#[derive(Clone)]
pub struct IntelliShellService {
check_updates: bool,
storage: SqliteStorage,
tuning: SearchTuning,
ai: AiConfig,
#[cfg(feature = "tldr")]
tldr_repo_path: PathBuf,
version_check_state: Arc<Mutex<version::VersionCheckState>>,
}
impl IntelliShellService {
pub fn new(
storage: SqliteStorage,
tuning: SearchTuning,
ai: AiConfig,
data_dir: impl AsRef<Path>,
check_updates: bool,
) -> Self {
Self {
check_updates,
storage,
tuning,
ai,
#[cfg(feature = "tldr")]
tldr_repo_path: data_dir.as_ref().join("tldr"),
version_check_state: Arc::new(Mutex::new(version::VersionCheckState::NotStarted)),
}
}
#[cfg(debug_assertions)]
pub async fn query(&self, sql: String) -> crate::errors::Result<String> {
self.storage.query(sql).await
}
#[instrument(skip_all)]
pub async fn load_workspace_items(&self) -> Result<bool> {
if !env::var("INTELLI_SKIP_WORKSPACE")
.map(|v| v != "1" && v.to_lowercase() != "true")
.unwrap_or(true)
{
tracing::info!("Skipping workspace load due to INTELLI_SKIP_WORKSPACE");
return Ok(false);
}
let workspace_files = find_workspace_files();
if workspace_files.is_empty() {
tracing::debug!("No workspace files were found");
return Ok(false);
}
self.storage.setup_workspace_storage().await?;
for (workspace_file, tag_name) in workspace_files {
let file = File::open(&workspace_file).await?;
let tag = format!("#{}", tag_name.as_deref().unwrap_or("workspace"));
let items_stream = parse_import_items(file, vec![tag], CATEGORY_WORKSPACE, SOURCE_WORKSPACE);
match self.storage.import_items(items_stream, false, true).await {
Ok(stats) => {
tracing::info!(
"Loaded {} commands and {} completions from workspace file {}",
stats.commands_imported,
stats.completions_imported,
workspace_file.display()
);
}
Err(err) => {
tracing::error!("Failed to load workspace file {}", workspace_file.display());
return Err(err);
}
}
}
Ok(true)
}
}
fn find_workspace_files() -> Vec<(PathBuf, Option<String>)> {
let mut result = Vec::new();
let mut seen_paths = HashSet::new();
let working_dir = PathBuf::from(get_working_dir());
let mut current = Some(working_dir.as_path());
tracing::debug!(
"Searching for workspace .intellishell file or folder from working dir: {}",
working_dir.display()
);
while let Some(parent) = current {
let candidate = parent.join(".intellishell");
if candidate.exists() {
collect_intellishell_files_from_location(&candidate, &mut seen_paths, &mut result);
break;
}
if parent.join(".git").is_dir() {
break;
}
current = parent.parent();
}
if let Some(base_dirs) = BaseDirs::new() {
let home_dir = base_dirs.home_dir();
tracing::debug!(
"Searching for .intellishell file or folder in home dir: {}",
home_dir.display()
);
let home_candidate = home_dir.join(".intellishell");
if home_candidate.exists() {
collect_intellishell_files_from_location(&home_candidate, &mut seen_paths, &mut result);
}
}
#[cfg(target_os = "windows")]
let system_candidate = PathBuf::from(r"C:\ProgramData\.intellishell");
#[cfg(not(target_os = "windows"))]
let system_candidate = PathBuf::from("/etc/.intellishell");
tracing::debug!(
"Searching for .intellishell file or folder system-wide: {}",
system_candidate.display()
);
if system_candidate.exists() {
collect_intellishell_files_from_location(&system_candidate, &mut seen_paths, &mut result);
}
result
}
fn collect_intellishell_files_from_location(
path: &Path,
seen_paths: &mut HashSet<PathBuf>,
result: &mut Vec<(PathBuf, Option<String>)>,
) {
if path.is_file() {
if seen_paths.insert(path.to_path_buf()) {
let folder_name = path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.map(String::from);
result.push((path.to_path_buf(), folder_name));
} else {
tracing::trace!("Skipping duplicate workspace file: {}", path.display());
}
} else if path.is_dir() {
for entry in WalkDir::new(path).min_depth(1).into_iter().filter_map(|e| e.ok()) {
let entry_path = entry.path();
let file_name = entry.file_name().to_string_lossy();
if entry_path.is_file() && !file_name.starts_with('.') {
if seen_paths.insert(entry_path.to_path_buf()) {
let tag = entry_path.file_stem().and_then(|n| n.to_str()).map(String::from);
result.push((entry_path.to_path_buf(), tag));
} else {
tracing::trace!("Skipping duplicate workspace file: {}", entry_path.display());
}
}
}
}
}