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