add:init path

This commit is contained in:
kokopi-dev
2026-04-04 22:49:59 +09:00
parent 69fa44b36a
commit 99a80f2f58
2 changed files with 132 additions and 6 deletions

View File

@@ -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 })
} }
} }

View 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"))
}