add:add command + add existing

This commit is contained in:
kokopi-dev
2026-04-04 22:45:23 +09:00
parent 973356cda5
commit 69fa44b36a
19 changed files with 1088 additions and 0 deletions

View File

@@ -0,0 +1,55 @@
use std::{fs, path::PathBuf};
use crate::{
config::{
defaults::{config_dir, config_file, default_bin_dir, scripts_dir},
types::Config,
},
error::AppError,
};
pub struct ConfigService {
pub config_dir: PathBuf,
pub scripts_dir: PathBuf,
pub config_path: PathBuf,
}
impl ConfigService {
pub fn new() -> Result<Self, AppError> {
Ok(Self {
config_dir: config_dir()?,
scripts_dir: scripts_dir()?,
config_path: config_file()?,
})
}
/// Creates all required directories and a default `config.toml` if they do
/// not already exist. Safe to call on every startup.
pub fn ensure_initialized(&self) -> Result<(), AppError> {
fs::create_dir_all(&self.config_dir)?;
fs::create_dir_all(&self.scripts_dir)?;
if !self.config_path.exists() {
let default_config = Config {
bin_dir: default_bin_dir()?,
default_shell: "bash".to_string(),
};
let contents = toml::to_string_pretty(&default_config)?;
fs::write(&self.config_path, contents)?;
}
Ok(())
}
pub fn load(&self) -> Result<Config, AppError> {
let contents = fs::read_to_string(&self.config_path)?;
let config = toml::from_str(&contents)?;
Ok(config)
}
pub fn save(&self, config: &Config) -> Result<(), AppError> {
let contents = toml::to_string_pretty(config)?;
fs::write(&self.config_path, contents)?;
Ok(())
}
}

View File

@@ -0,0 +1,55 @@
use std::{
env,
path::{Path, PathBuf},
};
/// Result of checking whether a name collides with an existing executable on `$PATH`.
#[derive(Debug)]
pub enum CollisionResult {
/// No conflict found — safe to use this name.
Clear,
/// A file with this name already exists at the given path.
/// The caller decides whether to warn and proceed or re-prompt.
Collision { path: PathBuf },
}
pub struct PathService {
/// Our own bin_dir — shims we've written here are excluded from collision results
/// so we don't report our own scripts as conflicts.
bin_dir: PathBuf,
}
impl PathService {
pub fn new(bin_dir: PathBuf) -> Self {
Self { bin_dir }
}
/// Walks every directory in `$PATH` looking for an executable named `name`.
/// Skips our own `bin_dir` so previously registered shims don't self-collide.
pub fn check_collision(&self, name: &str) -> CollisionResult {
let path_var = env::var("PATH").unwrap_or_default();
for dir in env::split_paths(&path_var) {
// Skip our own bin_dir — our shims live here
if dir == self.bin_dir {
continue;
}
let candidate = dir.join(name);
if is_executable(&candidate) {
return CollisionResult::Collision { path: candidate };
}
}
CollisionResult::Clear
}
}
/// Returns true if the path exists, is a file, and has at least one executable bit set.
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
path.metadata()
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}

View File

@@ -0,0 +1,53 @@
use std::{fs, path::PathBuf};
use crate::{
config::{defaults::registry_file, types::{Registry, ScriptEntry}},
error::AppError,
};
pub struct RegistryService {
pub registry_path: PathBuf,
}
impl RegistryService {
pub fn new() -> Result<Self, AppError> {
Ok(Self {
registry_path: registry_file()?,
})
}
/// Creates an empty `registry.toml` if it does not already exist.
pub fn ensure_initialized(&self) -> Result<(), AppError> {
if !self.registry_path.exists() {
let empty = Registry::default();
let contents = toml::to_string_pretty(&empty)?;
fs::write(&self.registry_path, contents)?;
}
Ok(())
}
pub fn load(&self) -> Result<Registry, AppError> {
let contents = fs::read_to_string(&self.registry_path)?;
let registry = toml::from_str(&contents)?;
Ok(registry)
}
pub fn save(&self, registry: &Registry) -> Result<(), AppError> {
let contents = toml::to_string_pretty(registry)?;
fs::write(&self.registry_path, contents)?;
Ok(())
}
/// Appends a new entry and persists the registry.
pub fn add(&self, entry: ScriptEntry) -> Result<(), AppError> {
let mut registry = self.load()?;
registry.scripts.push(entry);
self.save(&registry)
}
/// Returns true if a script with the given name is already registered.
pub fn name_exists(&self, name: &str) -> Result<bool, AppError> {
let registry = self.load()?;
Ok(registry.scripts.iter().any(|s| s.name == name))
}
}