amql-cli 0.0.0-alpha.0

AQL command-line interface and REPL
//! Interactive REPL for AQL queries.
//!
//! Supports multiline input, history, and REPL-specific commands.

use amql_engine::{
    find_project_root, load_manifest, meta, run_all_extractors, suggest_repairs, unified_query,
    validate, AnnotationStore, CodeCache, ExtractorRegistry, Manifest, ProjectRoot,
    ResolverRegistry, Scope,
};
use rustyline::error::ReadlineError;
use rustyline::DefaultEditor;
use std::path::PathBuf;
use std::process::ExitCode;

const PROMPT: &str = "aql> ";
const HISTORY_FILE: &str = ".aql_history";

pub fn run_repl() -> ExitCode {
    let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
    let project_root = match find_project_root(&cwd) {
        Some(root) => root,
        None => {
            eprintln!(
                "Error: no {} found in any parent directory",
                meta::schema_file()
            );
            return ExitCode::FAILURE;
        }
    };

    let manifest = match load_manifest(&project_root) {
        Ok(m) => m,
        Err(e) => {
            eprintln!("Error loading manifest: {e}");
            return ExitCode::FAILURE;
        }
    };

    let resolvers = ResolverRegistry::with_defaults();

    let mut cache = CodeCache::new(&project_root);
    let mut store = AnnotationStore::new(&project_root);
    store.load_all_from_locator();
    load_extractors(&manifest, &project_root, &mut store);

    let mut editor = match DefaultEditor::new() {
        Ok(e) => e,
        Err(e) => {
            eprintln!("Failed to initialize editor: {e}");
            return ExitCode::FAILURE;
        }
    };

    let history_path = dirs_path().join(HISTORY_FILE);
    let _ = editor.load_history(&history_path);

    println!("AQL REPL — type a selector to query, or :help for commands");

    loop {
        match editor.readline(PROMPT) {
            Ok(line) => {
                let trimmed = line.trim();
                if trimmed.is_empty() {
                    continue;
                }

                let _ = editor.add_history_entry(trimmed);

                if trimmed.starts_with(':') {
                    match handle_command(trimmed, &store, &manifest, &cache) {
                        CommandResult::Continue => {}
                        CommandResult::Quit => break,
                    }
                } else {
                    // Treat input as a query selector
                    match unified_query(
                        trimmed,
                        &Scope::from(""),
                        &mut cache,
                        &mut store,
                        &resolvers,
                        None,
                    ) {
                        Ok(results) => {
                            println!("{}", serde_json::to_string_pretty(&results).unwrap());
                        }
                        Err(e) => {
                            eprintln!("Error: {e}");
                        }
                    }
                }
            }
            Err(ReadlineError::Interrupted | ReadlineError::Eof) => break,
            Err(e) => {
                eprintln!("Error: {e}");
                break;
            }
        }
    }

    let _ = editor.save_history(&history_path);
    ExitCode::SUCCESS
}

enum CommandResult {
    Continue,
    Quit,
}

fn handle_command(
    input: &str,
    store: &AnnotationStore,
    manifest: &Manifest,
    cache: &CodeCache,
) -> CommandResult {
    match input {
        ":quit" | ":q" => CommandResult::Quit,
        ":schema" => {
            println!("{}", serde_json::to_string_pretty(manifest).unwrap());
            CommandResult::Continue
        }
        ":validate" => {
            let results = validate(store, manifest);
            println!("{}", serde_json::to_string_pretty(&results).unwrap());
            CommandResult::Continue
        }
        ":repair" => {
            let suggestions = suggest_repairs(store, Some(cache));
            println!("{}", serde_json::to_string_pretty(&suggestions).unwrap());
            CommandResult::Continue
        }
        ":help" | ":h" => {
            println!("Commands:");
            println!("  :schema    — print the manifest schema");
            println!("  :validate  — validate annotations against schema");
            println!("  :repair    — suggest fixes for broken bindings");
            println!("  :quit      — exit the REPL");
            println!();
            println!("Type any selector to run a unified query.");
            CommandResult::Continue
        }
        _ => {
            eprintln!("Unknown command: {input}. Type :help for available commands.");
            CommandResult::Continue
        }
    }
}

fn load_extractors(
    manifest: &Manifest,
    project_root: &std::path::Path,
    store: &mut AnnotationStore,
) {
    let root = ProjectRoot::from(project_root);
    let registry = ExtractorRegistry::with_defaults();
    let results = run_all_extractors(manifest, &root, &registry);
    for result in results {
        if !result.annotations.is_empty() {
            store.load_extractor_output(result.annotations);
        }
    }
}

fn dirs_path() -> PathBuf {
    std::env::var_os("HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("."))
}