use anyhow::{bail, Context, Result};
use colored::Colorize;
use std::fs;
use std::path::Path;
use flk::flake::parsers::{
commands::{add_shell_hook_command, parse_shell_hook_section, remove_shell_hook_command},
utils::resolve_profile,
};
pub fn run_add(
name: &str,
command: &str,
file: Option<String>,
target_profile: Option<String>,
) -> Result<()> {
let profile = resolve_profile(target_profile)?;
let flake_path = Path::new(".flk/profiles/").join(format!("{}.nix", profile));
if !is_valid_command_name(name) {
bail!(
"Invalid command name '{}'. Use only letters, numbers, hyphens, and underscores.",
name
);
}
println!("{} Adding command: {}", "→".blue().bold(), name.green());
let command_content = if let Some(filepath) = file {
println!(" Sourcing from: {}", filepath.cyan());
fs::read_to_string(&filepath)
.with_context(|| format!("Failed to read file: {}", filepath))?
} else {
command.to_string()
};
if command_content.trim().is_empty() {
bail!("Command cannot be empty");
}
let flake_content = fs::read_to_string(&flake_path).with_context(|| {
format!(
"Failed to read profile file at '{}'. Have you run 'flk init'?",
flake_path.display()
)
})?;
let section = parse_shell_hook_section(&flake_content).with_context(|| {
format!(
"Failed to parse shellHook section in profile file '{}'",
flake_path.display()
)
})?;
if section.command_exists(name) {
bail!(
"Command '{}' already exists. Remove it with: {}",
name.cyan(),
format!("flk remove-command {}", name).yellow()
);
}
let updated_content = add_shell_hook_command(&flake_content, name, &command_content)
.context("Failed to add command to shellHook")?;
fs::write(flake_path, updated_content).context("Failed to write flake.nix")?;
println!(
"{} Command '{}' added successfully!",
"✓".green().bold(),
name
);
println!("\n{}", "Next steps:".bold());
println!(" 1. Run {} to enter the dev shell", "nix develop".cyan());
println!(" 2. Use your command: {}", name.cyan());
Ok(())
}
pub fn run_remove(name: &str, target_profile: Option<String>) -> Result<()> {
let profile = resolve_profile(target_profile)?;
let flake_path = Path::new(".flk/profiles/").join(format!("{}.nix", profile));
if !flake_path.exists() {
bail!(
"Profile file '{}' not found. Have you run 'flk init'?",
flake_path.display()
);
}
println!("{} Removing command: {}", "→".blue().bold(), name.yellow());
let flake_content = fs::read_to_string(&flake_path)
.with_context(|| format!("Failed to read profile file at '{}'", flake_path.display()))?;
let section = parse_shell_hook_section(&flake_content).with_context(|| {
format!(
"Failed to parse shellHook section in profile file '{}'",
flake_path.display()
)
})?;
if !section.command_exists(name) {
bail!("Command '{}' not found in profile", name.cyan());
}
let updated_content = remove_shell_hook_command(&flake_content, name)
.context("Failed to remove command from shellHook")?;
fs::write(&flake_path, updated_content).context("Failed to write flake.nix")?;
println!(
"{} Command '{}' removed successfully!",
"✓".green().bold(),
name
);
Ok(())
}
pub fn list(target_profile: Option<String>) -> Result<()> {
let profile = resolve_profile(target_profile)?;
let flake_path = Path::new(".flk/profiles/").join(format!("{}.nix", profile));
let flake_content = fs::read_to_string(&flake_path).with_context(|| {
format!(
"Failed to read profile file at '{}'. Have you run 'flk init'?",
flake_path.display()
)
})?;
let section = parse_shell_hook_section(&flake_content).with_context(|| {
format!(
"Failed to parse shellHook section in profile file '{}'",
flake_path.display()
)
})?;
if section.entries.is_empty() {
println!(
"{} No commands found in the current profile.",
"✗".red().bold()
);
return Ok(());
}
for entry in section.entries {
println!("{} {}", "•".green(), entry.name.bold());
}
Ok(())
}
fn is_valid_command_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
&& !name.starts_with('-')
}