add:add command + add existing
This commit is contained in:
211
src/cli/add.rs
Normal file
211
src/cli/add.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use std::{
|
||||
os::unix::fs,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use cliclack::{confirm, input, log, note, select};
|
||||
|
||||
use crate::{
|
||||
config::types::ScriptEntry,
|
||||
error::AppError,
|
||||
services::{path_service::CollisionResult, ServiceStore},
|
||||
shim::template::write_shim,
|
||||
};
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
enum AddMode {
|
||||
Existing,
|
||||
Create,
|
||||
}
|
||||
|
||||
pub fn run(store: &ServiceStore) -> Result<(), AppError> {
|
||||
let mode = select("What would you like to do?")
|
||||
.item(AddMode::Existing, "Add existing script", "register a script that already exists")
|
||||
.item(AddMode::Create, "Create new script", "scaffold a new script from a template")
|
||||
.interact()?;
|
||||
|
||||
match mode {
|
||||
AddMode::Existing => add_existing(store),
|
||||
AddMode::Create => {
|
||||
log::info("Create new script — coming soon.")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_existing(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<String> = 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)
|
||||
}
|
||||
6
src/cli/list.rs
Normal file
6
src/cli/list.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use crate::{error::AppError, services::ServiceStore};
|
||||
|
||||
pub fn run(_store: &ServiceStore) -> Result<(), AppError> {
|
||||
// TODO: load registry, display all scripts in a cliclack table/select
|
||||
Ok(())
|
||||
}
|
||||
6
src/cli/remove.rs
Normal file
6
src/cli/remove.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use crate::{error::AppError, services::ServiceStore};
|
||||
|
||||
pub fn run(_store: &ServiceStore) -> Result<(), AppError> {
|
||||
// TODO: load registry, prompt for selection, delete shim, update registry
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user