hni 0.0.2

ni-compatible package manager command router with node shim
Documentation
use std::env;

use crate::{
    core::{
        error::{HniError, HniResult},
        resolve::{self, ResolveContext},
        storage::{Storage, load_storage, save_storage},
        types::ResolvedExecution,
    },
    features::interactive::{
        completion::{
            completion_candidates, completion_script_bash, completion_script_fish,
            completion_script_zsh,
        },
        nr_scripts::{choose_script_interactive, read_scripts},
    },
};

pub fn handle(mut args: Vec<String>, ctx: &ResolveContext) -> HniResult<Option<ResolvedExecution>> {
    if let Some(first) = args.first() {
        if let Some(script) = completion_script_for(first) {
            println!("{script}");
            return Ok(None);
        }

        if first == "--completion" {
            handle_completion_query(&args[1..], &ctx.cwd)?;
            return Ok(None);
        }
    }

    let needs_last = args
        .first()
        .is_some_and(|a| matches!(a.as_str(), "-" | "--repeat-last"));

    if args.is_empty() {
        args.push(choose_script_interactive(&ctx.cwd)?);
    }

    let mut storage = match load_storage() {
        Ok(storage) => storage,
        Err(err) => {
            eprintln!("[hni] warning: failed to load stored command history: {err}");
            Storage::default()
        }
    };

    if needs_last {
        let last = storage
            .last_run_command
            .ok_or_else(|| HniError::interactive("no last command found"))?;
        args[0] = last;
    }

    storage.last_run_command = args.first().cloned();
    if let Err(err) = save_storage(&storage) {
        eprintln!("[hni] warning: failed to persist last command: {err}");
    }

    let resolved = resolve::resolve_nr(args, ctx)?;
    Ok(Some(resolved))
}

fn handle_completion_query(args: &[String], cwd: &std::path::Path) -> HniResult<()> {
    let scripts = read_scripts(cwd)?;
    let script_names = scripts.into_iter().map(|s| s.name).collect::<Vec<_>>();

    let comp_word = env::var("COMP_CWORD")
        .ok()
        .and_then(|v| v.parse::<usize>().ok())
        .unwrap_or(0);

    let prefix = if comp_word > 1 {
        args.last().cloned().unwrap_or_default()
    } else {
        args.get(1).cloned().unwrap_or_default()
    };

    for candidate in completion_candidates(&prefix, script_names) {
        println!("{candidate}");
    }

    Ok(())
}

fn completion_script_for(flag: &str) -> Option<String> {
    match flag {
        "--completion-bash" => Some(completion_script_bash("nr")),
        "--completion-zsh" => Some(completion_script_zsh("nr")),
        "--completion-fish" => Some(completion_script_fish("nr")),
        _ => None,
    }
}