Files
scripts-organizer/src/cli/list.rs
2026-04-05 00:36:10 +09:00

128 lines
4.4 KiB
Rust

use std::{os::unix::process::CommandExt, process::Command};
use cliclack::{confirm, log, note, outro, select};
use crate::{
config::types::ScriptEntry,
error::AppError,
services::{registry_service::is_healthy, ServiceStore},
};
#[derive(Clone, Eq, PartialEq)]
enum Health {
Ok,
Broken,
}
/// Select value — either a real script name or a group header sentinel.
/// Headers are re-prompted if accidentally selected.
#[derive(Clone, Eq, PartialEq)]
enum Item {
Script(String),
Header(String),
}
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(());
}
// ── Group by primary tag, sort within each group by name ──────────────────
let mut scripts_with_health: Vec<(&ScriptEntry, Health)> = registry
.scripts
.iter()
.map(|e| {
let health = if is_healthy(e) { Health::Ok } else { Health::Broken };
(e, health)
})
.collect();
scripts_with_health.sort_by(|(a, _), (b, _)| {
let tag_a = a.tags.first().map(String::as_str).unwrap_or("untagged");
let tag_b = b.tags.first().map(String::as_str).unwrap_or("untagged");
tag_a.cmp(tag_b).then(a.name.cmp(&b.name))
});
// Collect into (group_label, entries) pairs
let mut groups: Vec<(String, Vec<(&ScriptEntry, Health)>)> = Vec::new();
for (entry, health) in scripts_with_health {
let group_key = if entry.tags.is_empty() {
"untagged".to_string()
} else {
// Header shows all tags: [git, config]
format!("[{}]", entry.tags.join(", "))
};
match groups.last_mut() {
Some((key, entries)) if key == &group_key => entries.push((entry, health)),
_ => groups.push((group_key, vec![(entry, health)])),
}
}
// ── Build select — headers + indented script names ─────────────────────────
let selected_name = loop {
let mut prompt = select("Which script?");
for (group_label, entries) in &groups {
prompt = prompt.item(Item::Header(group_label.clone()), group_label, "");
for (entry, health) in entries {
let name = match health {
Health::Ok => format!(" * {}", entry.name),
Health::Broken => format!(" * ⚠ {}", entry.name),
};
prompt = prompt.item(
Item::Script(entry.name.clone()),
name,
&entry.description,
);
}
}
match prompt.interact()? {
Item::Script(name) => break name,
Item::Header(_) => continue,
}
};
// ── Look up the selected entry ─────────────────────────────────────────────
let (entry, health) = registry
.scripts
.iter()
.map(|e| {
let health = if is_healthy(e) { Health::Ok } else { Health::Broken };
(e, health)
})
.find(|(e, _)| e.name == selected_name)
.expect("selected name must exist in registry");
// ── 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: exec shim --help, replacing this process ─────────────────────
outro(format!("Running {} --help...", entry.name))?;
let err = Command::new(&entry.shim_path).arg("--help").exec();
Err(AppError::Io(err))
}