add:init path
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
pub mod config_service;
|
pub mod config_service;
|
||||||
pub mod path_service;
|
pub mod path_service;
|
||||||
pub mod registry_service;
|
pub mod registry_service;
|
||||||
|
pub mod shell_service;
|
||||||
|
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use config_service::ConfigService;
|
use config_service::ConfigService;
|
||||||
use path_service::PathService;
|
use path_service::PathService;
|
||||||
use registry_service::RegistryService;
|
use registry_service::RegistryService;
|
||||||
|
use shell_service::ShellService;
|
||||||
|
|
||||||
/// Owns all application services and is constructed once at startup.
|
/// Owns all application services and is constructed once at startup.
|
||||||
/// Pass as `&ServiceStore` into every CLI handler.
|
/// Pass as `&ServiceStore` into every CLI handler.
|
||||||
@@ -13,12 +15,12 @@ pub struct ServiceStore {
|
|||||||
pub config: ConfigService,
|
pub config: ConfigService,
|
||||||
pub registry: RegistryService,
|
pub registry: RegistryService,
|
||||||
pub path: PathService,
|
pub path: PathService,
|
||||||
|
pub shell: ShellService,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceStore {
|
impl ServiceStore {
|
||||||
/// Boots all services: resolves paths, creates missing config files/dirs.
|
/// Boots all services: resolves paths, creates missing config files/dirs,
|
||||||
/// `PathService` is constructed after config is loaded so it can exclude
|
/// and ensures bin_dir is on $PATH.
|
||||||
/// our own bin_dir from collision results.
|
|
||||||
pub fn init() -> Result<Self, AppError> {
|
pub fn init() -> Result<Self, AppError> {
|
||||||
let config = ConfigService::new()?;
|
let config = ConfigService::new()?;
|
||||||
let registry = RegistryService::new()?;
|
let registry = RegistryService::new()?;
|
||||||
@@ -26,10 +28,13 @@ impl ServiceStore {
|
|||||||
config.ensure_initialized()?;
|
config.ensure_initialized()?;
|
||||||
registry.ensure_initialized()?;
|
registry.ensure_initialized()?;
|
||||||
|
|
||||||
// Load config to get bin_dir for PathService
|
|
||||||
let loaded_config = config.load()?;
|
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