add:init path
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
pub mod config_service;
|
||||
pub mod path_service;
|
||||
pub mod registry_service;
|
||||
pub mod shell_service;
|
||||
|
||||
use crate::error::AppError;
|
||||
use config_service::ConfigService;
|
||||
use path_service::PathService;
|
||||
use registry_service::RegistryService;
|
||||
use shell_service::ShellService;
|
||||
|
||||
/// Owns all application services and is constructed once at startup.
|
||||
/// Pass as `&ServiceStore` into every CLI handler.
|
||||
@@ -13,12 +15,12 @@ pub struct ServiceStore {
|
||||
pub config: ConfigService,
|
||||
pub registry: RegistryService,
|
||||
pub path: PathService,
|
||||
pub shell: ShellService,
|
||||
}
|
||||
|
||||
impl ServiceStore {
|
||||
/// Boots all services: resolves paths, creates missing config files/dirs.
|
||||
/// `PathService` is constructed after config is loaded so it can exclude
|
||||
/// our own bin_dir from collision results.
|
||||
/// Boots all services: resolves paths, creates missing config files/dirs,
|
||||
/// and ensures bin_dir is on $PATH.
|
||||
pub fn init() -> Result<Self, AppError> {
|
||||
let config = ConfigService::new()?;
|
||||
let registry = RegistryService::new()?;
|
||||
@@ -26,10 +28,13 @@ impl ServiceStore {
|
||||
config.ensure_initialized()?;
|
||||
registry.ensure_initialized()?;
|
||||
|
||||
// Load config to get bin_dir for PathService
|
||||
let loaded_config = config.load()?;
|
||||
let path = PathService::new(loaded_config.bin_dir);
|
||||
let path = PathService::new(loaded_config.bin_dir.clone());
|
||||
let shell = ShellService::new(loaded_config.bin_dir.clone());
|
||||
|
||||
Ok(Self { config, registry, path })
|
||||
// Check and patch $PATH on every startup — idempotent, safe to repeat
|
||||
shell.ensure_bin_dir_on_path()?;
|
||||
|
||||
Ok(Self { config, registry, path, shell })
|
||||
}
|
||||
}
|
||||
|
||||
121
src/services/shell_service.rs
Normal file
121
src/services/shell_service.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use std::io::Write;
|
||||
use std::{
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use cliclack::log;
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
/// The line we append to the shell rc file, guarded by a marker comment so we
|
||||
/// never append it twice and can reliably detect our own prior writes.
|
||||
const PATH_MARKER: &str = "# scripts-organizer: bin_dir";
|
||||
const PATH_EXPORT: &str = "export PATH=\"$HOME/.local/bin:$PATH\"";
|
||||
|
||||
/// Result of checking whether bin_dir is already on $PATH.
|
||||
#[derive(Debug)]
|
||||
pub enum PathStatus {
|
||||
/// bin_dir is already present in the current $PATH — nothing to do.
|
||||
AlreadySet,
|
||||
/// bin_dir was not found; we appended the export line to the rc file.
|
||||
Added { rc_path: PathBuf },
|
||||
/// bin_dir was not found and we could not determine a supported rc file.
|
||||
NoRcFound,
|
||||
}
|
||||
|
||||
pub struct ShellService {
|
||||
bin_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl ShellService {
|
||||
pub fn new(bin_dir: PathBuf) -> Self {
|
||||
Self { bin_dir }
|
||||
}
|
||||
|
||||
/// Checks whether `bin_dir` is on the current `$PATH`.
|
||||
/// If it is not, attempts to append an export line to the user's shell rc.
|
||||
/// Prints an informational message via cliclack for either outcome.
|
||||
pub fn ensure_bin_dir_on_path(&self) -> Result<PathStatus, AppError> {
|
||||
if self.is_on_path() {
|
||||
return Ok(PathStatus::AlreadySet);
|
||||
}
|
||||
|
||||
match detect_rc_file() {
|
||||
Some(rc_path) => {
|
||||
self.append_to_rc(&rc_path)?;
|
||||
log::info(format!(
|
||||
"Added {} to PATH in {}\n Restart your shell or run: source {}",
|
||||
self.bin_dir.display(),
|
||||
rc_path.display(),
|
||||
rc_path.display(),
|
||||
))?;
|
||||
Ok(PathStatus::Added { rc_path })
|
||||
}
|
||||
None => {
|
||||
log::warning(format!(
|
||||
"{} is not on your PATH and no supported rc file was found.\n Add this line to your shell config manually:\n\n {}",
|
||||
self.bin_dir.display(),
|
||||
PATH_EXPORT,
|
||||
))?;
|
||||
Ok(PathStatus::NoRcFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `bin_dir` appears in any entry of the current `$PATH`.
|
||||
fn is_on_path(&self) -> bool {
|
||||
let path_var = env::var("PATH").unwrap_or_default();
|
||||
env::split_paths(&path_var).any(|p| p == self.bin_dir)
|
||||
}
|
||||
|
||||
/// Appends the PATH export to `rc_path`, but only if our marker isn't already
|
||||
/// present — making this safe to call repeatedly.
|
||||
fn append_to_rc(&self, rc_path: &Path) -> Result<(), AppError> {
|
||||
let existing = if rc_path.exists() {
|
||||
fs::read_to_string(rc_path)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// Idempotency guard — never write twice
|
||||
if existing.contains(PATH_MARKER) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let addition = format!(
|
||||
"\n{PATH_MARKER}\n{PATH_EXPORT}\n",
|
||||
);
|
||||
|
||||
// Append rather than overwrite — never touch anything already in the file
|
||||
fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(rc_path)?
|
||||
.write_all(addition.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Detects which rc file to write to based on what exists in $HOME.
|
||||
/// Preference order: ~/.bashrc → ~/.bash_profile → ~/.profile
|
||||
/// Returns `None` if none of these are found and none can be created.
|
||||
fn detect_rc_file() -> Option<PathBuf> {
|
||||
let home = env::var("HOME").ok()?;
|
||||
let home = PathBuf::from(home);
|
||||
|
||||
// Candidates in preference order for bash/Linux
|
||||
let candidates = [".bashrc", ".bash_profile", ".profile"];
|
||||
|
||||
// Return the first one that already exists
|
||||
for name in &candidates {
|
||||
let path = home.join(name);
|
||||
if path.exists() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing exists yet — fall back to creating ~/.bashrc
|
||||
Some(home.join(".bashrc"))
|
||||
}
|
||||
Reference in New Issue
Block a user