use std::os::unix::fs; use std::path::PathBuf; use cliclack::{confirm, input, log, note}; use crate::{ config::types::ScriptEntry, error::AppError, services::{path_service::CollisionResult, ServiceStore}, shim::template::write_shim, }; pub fn run(store: &ServiceStore) -> Result<(), AppError> { let config = store.config.load()?; // ── 1. Path to the original script ──────────────────────────────────────── let source_path: String = input("Path to the script") .placeholder("~/dotfiles/git/git-config.py") .validate(|raw: &String| { let expanded = expand_tilde(raw.trim()); if !expanded.exists() { Err("File not found.") } else if !expanded.is_file() { Err("Path is a directory, not a file.") } else { Ok(()) } }) .interact()?; let source_path = expand_tilde(source_path.trim()); // ── 2. Name (shim name, no extension) ───────────────────────────────────── // Default to the file stem of the source, without extension let default_name = source_path .file_stem() .and_then(|s| s.to_str()) .unwrap_or("") .to_string(); let name: String = input("Script name (this becomes the command you call)") .placeholder(&default_name) .default_input(&default_name) .validate(|n: &String| { let n = n.trim(); if n.is_empty() { return Err("Name cannot be empty."); } if n.contains(' ') { return Err("Name cannot contain spaces."); } if n.contains('/') { return Err("Name cannot contain slashes."); } Ok(()) }) .interact()?; let name = name.trim().to_string(); // Guard: already registered under this name if store.registry.name_exists(&name)? { log::error(format!("A script named '{name}' is already registered."))?; return Err(AppError::Cancelled); } // Guard: collision with existing $PATH executable match store.path.check_collision(&name) { CollisionResult::Collision { path } => { note( "Name conflict", format!( "'{name}' already exists at {}\nYour shim will shadow it on $PATH.", path.display() ), )?; let proceed = confirm("Continue anyway?").interact()?; if !proceed { return Err(AppError::Cancelled); } } CollisionResult::Clear => {} } // ── 3. Description ───────────────────────────────────────────────────────── let description: String = input("Description") .placeholder("One-line summary shown in --help and the script list") .validate(|d: &String| { if d.trim().is_empty() { Err("Description cannot be empty.") } else { Ok(()) } }) .interact()?; // ── 4. Usage line ────────────────────────────────────────────────────────── let default_usage = format!("{name} [options]"); let usage: String = input("Usage line") .placeholder(&default_usage) .default_input(&default_usage) .interact()?; // ── 5. Tags (optional) ───────────────────────────────────────────────────── let tags_raw: String = input("Tags (optional, comma-separated)") .placeholder("git, config, tools") .required(false) .interact()?; let tags: Vec = tags_raw .split(',') .map(|t| t.trim().to_string()) .filter(|t| !t.is_empty()) .collect(); // ── 6. Confirm ───────────────────────────────────────────────────────────── let symlink_path = store.config.scripts_dir.join(&name); let shim_path = config.bin_dir.join(&name); note( "Summary", format!( "Name: {name}\n\ Source: {source}\n\ Symlink: {symlink}\n\ Shim: {shim}\n\ Description: {description}\n\ Usage: {usage}\n\ Tags: {tags}", source = source_path.display(), symlink = symlink_path.display(), shim = shim_path.display(), tags = if tags.is_empty() { "none".to_string() } else { tags.join(", ") }, ), )?; let confirmed = confirm("Register this script?").interact()?; if !confirmed { return Err(AppError::Cancelled); } // ── 7. Write symlink, shim, registry entry ───────────────────────────────── // Ensure bin_dir exists std::fs::create_dir_all(&config.bin_dir)?; // Create symlink: scripts_dir/name → source_path if symlink_path.exists() || symlink_path.is_symlink() { std::fs::remove_file(&symlink_path)?; } fs::symlink(&source_path, &symlink_path)?; // Write the bash shim write_shim( &shim_path, &symlink_path, &name, &description, &usage, &config.default_shell, )?; // Persist to registry store.registry.add(ScriptEntry { name: name.clone(), source_path, symlink_path, shim_path, description, usage, tags, })?; log::success(format!("'{name}' registered. You can now call it directly."))?; Ok(()) } /// Expands a leading `~/` to the user's home directory. /// Returns the path unchanged if it doesn't start with `~/`. fn expand_tilde(raw: &str) -> PathBuf { if let Some(rest) = raw.strip_prefix("~/") { if let Ok(home) = std::env::var("HOME") { return PathBuf::from(home).join(rest); } } PathBuf::from(raw) }