use std::fs;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddCommandOutcome {
Added,
AlreadyPresent,
}
pub fn add_command(
path: &Path,
name: &str,
description: &str,
) -> Result<AddCommandOutcome, String> {
let content = fs::read_to_string(path)
.map_err(|e| format!("cannot read {}: {e}", path.display()))?;
let (new_content, outcome) = insert_command(&content, name, description)?;
if outcome == AddCommandOutcome::Added {
fs::write(path, new_content)
.map_err(|e| format!("cannot write {}: {e}", path.display()))?;
}
Ok(outcome)
}
fn insert_command(
content: &str,
name: &str,
description: &str,
) -> Result<(String, AddCommandOutcome), String> {
if name.is_empty() {
return Err("command name cannot be empty".into());
}
let lines: Vec<&str> = content.lines().collect();
let header_idx = lines
.iter()
.position(|l| l.trim_end() == "commands:" && !l.starts_with([' ', '\t']));
let Some(header_idx) = header_idx else {
let mut out = content.to_string();
if !out.is_empty() && !out.ends_with('\n') {
out.push('\n');
}
if !out.is_empty() && !out.ends_with("\n\n") {
out.push('\n');
}
out.push_str("commands:\n");
out.push_str(&render_entry(" ", name, description));
return Ok((out, AddCommandOutcome::Added));
};
let block_end = lines[header_idx + 1..]
.iter()
.position(|l| !l.is_empty() && !l.starts_with([' ', '\t']))
.map(|i| header_idx + 1 + i)
.unwrap_or(lines.len());
let child_indent = lines[header_idx + 1..block_end]
.iter()
.find(|l| !l.trim().is_empty())
.map(|l| {
let n = l.chars().take_while(|c| *c == ' ').count();
" ".repeat(n)
})
.unwrap_or_else(|| " ".to_string());
let key_token = format!("{name}:");
for line in &lines[header_idx + 1..block_end] {
if !line.starts_with(&child_indent) {
continue;
}
let after_indent = &line[child_indent.len()..];
if after_indent.starts_with(' ') {
continue;
}
let trimmed = after_indent.trim_start();
if trimmed == key_token
|| trimmed.starts_with(&format!("{key_token} "))
|| trimmed.starts_with(&format!("{name} :"))
{
return Ok((content.to_string(), AddCommandOutcome::AlreadyPresent));
}
}
let mut insert_at = header_idx + 1;
for (offset, line) in lines[header_idx + 1..block_end].iter().enumerate() {
if !line.trim().is_empty() {
insert_at = header_idx + 1 + offset + 1;
}
}
let entry = render_entry(&child_indent, name, description);
let mut out = lines[..insert_at].join("\n");
if !out.is_empty() {
out.push('\n');
}
let prev_blank = insert_at == header_idx + 1
|| lines.get(insert_at - 1).is_some_and(|l| l.trim().is_empty());
if !prev_blank {
out.push('\n');
}
out.push_str(&entry);
if insert_at < lines.len() {
out.push_str(&lines[insert_at..].join("\n"));
if content.ends_with('\n') {
out.push('\n');
}
}
Ok((out, AddCommandOutcome::Added))
}
fn render_entry(child_indent: &str, name: &str, description: &str) -> String {
let mut out = format!("{child_indent}{name}:\n");
if !description.is_empty() {
out.push_str(&format!("{child_indent}{child_indent}description: {description}\n"));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn appends_to_existing_commands_block() {
let input = "\
description: my project
commands:
build:
run: cargo build
docker: dev
";
let (out, outcome) = insert_command(input, "flodl-hf", "HF integration").unwrap();
assert_eq!(outcome, AddCommandOutcome::Added);
assert!(out.contains("build:"), "preserves existing: {out}");
assert!(out.contains("flodl-hf:"), "appends: {out}");
assert!(out.contains("description: HF integration"));
let build = out.find("build:").unwrap();
let new = out.find("flodl-hf:").unwrap();
assert!(new > build);
}
#[test]
fn already_present_is_noop() {
let input = "\
commands:
flodl-hf:
description: existing entry
build:
run: cargo build
";
let (out, outcome) = insert_command(input, "flodl-hf", "new desc").unwrap();
assert_eq!(outcome, AddCommandOutcome::AlreadyPresent);
assert_eq!(out, input);
}
#[test]
fn missing_commands_block_appends_at_eof() {
let input = "description: my project\n";
let (out, outcome) = insert_command(input, "flodl-hf", "HF").unwrap();
assert_eq!(outcome, AddCommandOutcome::Added);
assert!(out.contains("commands:"));
assert!(out.contains(" flodl-hf:"));
assert!(out.contains(" description: HF"));
}
#[test]
fn empty_commands_block_inserts_first_child() {
let input = "commands:\n";
let (out, outcome) = insert_command(input, "flodl-hf", "HF").unwrap();
assert_eq!(outcome, AddCommandOutcome::Added);
assert!(out.contains(" flodl-hf:"));
assert!(out.contains(" description: HF"));
}
#[test]
fn detects_existing_indent_and_matches_it() {
let input = "\
commands:
build:
run: cargo build
";
let (out, _) = insert_command(input, "flodl-hf", "HF").unwrap();
assert!(out.contains(" flodl-hf:"));
assert!(out.contains(" description: HF"));
}
#[test]
fn empty_description_omits_subfield() {
let input = "commands:\n build:\n run: cargo build\n";
let (out, _) = insert_command(input, "flodl-hf", "").unwrap();
assert!(out.contains(" flodl-hf:"));
assert!(!out.contains("description: \n"), "no empty description: {out}");
}
#[test]
fn neighbouring_command_name_does_not_false_positive() {
let input = "commands:\n flodl-hf:\n description: existing\n";
let (out, outcome) = insert_command(input, "flodl", "new").unwrap();
assert_eq!(outcome, AddCommandOutcome::Added);
assert!(out.contains("flodl-hf:"));
assert!(out.contains("flodl:"));
}
#[test]
fn preserves_trailing_content_after_block() {
let input = "\
commands:
build:
run: cargo build
other_top_level: foo
";
let (out, _) = insert_command(input, "flodl-hf", "HF").unwrap();
assert!(out.contains("other_top_level: foo"), "trailing key preserved: {out}");
let new = out.find("flodl-hf:").unwrap();
let other = out.find("other_top_level:").unwrap();
assert!(new < other);
}
#[test]
fn empty_name_errors() {
let err = insert_command("commands:\n", "", "x").unwrap_err();
assert!(err.contains("name cannot be empty"));
}
}