add:list functionality
This commit is contained in:
@@ -1,6 +1,99 @@
|
|||||||
|
use std::{os::unix::process::CommandExt, process::Command};
|
||||||
|
|
||||||
|
use cliclack::{confirm, log, note, outro, select};
|
||||||
|
|
||||||
use crate::{error::AppError, services::ServiceStore};
|
use crate::{error::AppError, services::ServiceStore};
|
||||||
|
|
||||||
pub fn run(_store: &ServiceStore) -> Result<(), AppError> {
|
/// Health of a registered script's files on disk.
|
||||||
// TODO: load registry, display all scripts in a cliclack table/select
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
Ok(())
|
enum Health {
|
||||||
|
Ok,
|
||||||
|
Broken,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(store: &ServiceStore) -> Result<(), AppError> {
|
||||||
|
let registry = store.registry.load()?;
|
||||||
|
|
||||||
|
if registry.scripts.is_empty() {
|
||||||
|
note(
|
||||||
|
"No scripts registered",
|
||||||
|
"Use 'Add' to register your first script.",
|
||||||
|
)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Annotate each entry with its health before building the select items
|
||||||
|
let annotated: Vec<(&crate::config::types::ScriptEntry, Health)> = registry
|
||||||
|
.scripts
|
||||||
|
.iter()
|
||||||
|
.map(|entry| {
|
||||||
|
let health = if is_healthy(entry) {
|
||||||
|
Health::Ok
|
||||||
|
} else {
|
||||||
|
Health::Broken
|
||||||
|
};
|
||||||
|
(entry, health)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// ── Script selection ───────────────────────────────────────────────────────
|
||||||
|
// Use the script name as the select value — simple, Clone+Eq for free.
|
||||||
|
let selected_name: String = {
|
||||||
|
let mut prompt = select("Which script?");
|
||||||
|
for (entry, health) in &annotated {
|
||||||
|
let label = match health {
|
||||||
|
Health::Ok => entry.name.clone(),
|
||||||
|
Health::Broken => format!("⚠ {}", entry.name),
|
||||||
|
};
|
||||||
|
prompt = prompt.item(entry.name.clone(), label, &entry.description);
|
||||||
|
}
|
||||||
|
prompt.interact()?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Look up the full entry and its health by name
|
||||||
|
let (entry, health) = annotated
|
||||||
|
.into_iter()
|
||||||
|
.find(|(e, _)| e.name == selected_name)
|
||||||
|
.expect("selected name must exist in annotated list");
|
||||||
|
|
||||||
|
// ── Broken entry handling ──────────────────────────────────────────────────
|
||||||
|
if health == Health::Broken {
|
||||||
|
log::warning(format!(
|
||||||
|
"'{}' is broken — shim or symlink is missing.\n Source: {}",
|
||||||
|
entry.name,
|
||||||
|
entry.source_path.display(),
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let remove = confirm("Remove this broken entry from the registry?").interact()?;
|
||||||
|
if remove {
|
||||||
|
store.registry.remove(&entry.name)?;
|
||||||
|
log::success(format!("'{}' removed from registry.", entry.name))?;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Healthy entry: exec shim --help, replacing this process ───────────────
|
||||||
|
outro(format!("Running {} --help...", entry.name))?;
|
||||||
|
|
||||||
|
let err = Command::new(&entry.shim_path).arg("--help").exec();
|
||||||
|
|
||||||
|
// exec() only returns on failure
|
||||||
|
Err(AppError::Io(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if both the shim and the symlink target are present on disk.
|
||||||
|
fn is_healthy(entry: &crate::config::types::ScriptEntry) -> bool {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
let shim_ok = entry
|
||||||
|
.shim_path
|
||||||
|
.metadata()
|
||||||
|
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
// Follow the symlink — if the target is gone, symlink_metadata succeeds but
|
||||||
|
// metadata() (which follows links) will fail
|
||||||
|
let symlink_ok = entry.symlink_path.metadata().map(|m| m.is_file()).unwrap_or(false);
|
||||||
|
|
||||||
|
shim_ok && symlink_ok
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
use std::{fs, path::PathBuf};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{defaults::registry_file, types::{Registry, ScriptEntry}},
|
config::{
|
||||||
|
defaults::registry_file,
|
||||||
|
types::{Registry, ScriptEntry},
|
||||||
|
},
|
||||||
error::AppError,
|
error::AppError,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,6 +48,26 @@ impl RegistryService {
|
|||||||
self.save(®istry)
|
self.save(®istry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Removes the entry with the given name and persists the registry.
|
||||||
|
/// Also removes the shim and symlink files from disk if they exist.
|
||||||
|
/// Does nothing if the name is not found.
|
||||||
|
pub fn remove(&self, name: &str) -> Result<(), AppError> {
|
||||||
|
let mut registry = self.load()?;
|
||||||
|
|
||||||
|
if let Some(pos) = registry.scripts.iter().position(|s| s.name == name) {
|
||||||
|
let entry = ®istry.scripts[pos];
|
||||||
|
|
||||||
|
// Best-effort removal of generated files — don't fail if already gone
|
||||||
|
let _ = fs::remove_file(&entry.shim_path);
|
||||||
|
let _ = fs::remove_file(&entry.symlink_path);
|
||||||
|
|
||||||
|
registry.scripts.remove(pos);
|
||||||
|
self.save(®istry)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns true if a script with the given name is already registered.
|
/// Returns true if a script with the given name is already registered.
|
||||||
pub fn name_exists(&self, name: &str) -> Result<bool, AppError> {
|
pub fn name_exists(&self, name: &str) -> Result<bool, AppError> {
|
||||||
let registry = self.load()?;
|
let registry = self.load()?;
|
||||||
|
|||||||
Reference in New Issue
Block a user