harn-cli 0.10.27

CLI for the Harn programming language — run, test, REPL, format, and lint
Documentation
use std::path::{Path, PathBuf};
use std::process;

use crate::cli::{CheckArgs, CheckOutputFormat};
use crate::{command_error, json_envelope, package, print_check_error};

use super::{
    build_module_graph_with_parsed_sources, check_files, check_files_independently,
    collect_cross_file_imports, collect_harn_targets, connector_matrix, provider_matrix,
    CheckCliOverrides, CheckReport, CHECK_SCHEMA_VERSION,
};

pub(crate) fn run_check_command(args: CheckArgs) {
    let json_format_alias = !args.json && matches!(args.format, CheckOutputFormat::Json);
    let matrix_format = if args.json {
        if !matches!(args.format, CheckOutputFormat::Text) {
            command_error("`harn check` accepts either `--json` or `--format`, not both");
        }
        CheckOutputFormat::Json
    } else {
        args.format
    };
    if args.provider_matrix {
        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let extensions = package::load_runtime_extensions(&cwd);
        package::install_runtime_extensions(&extensions);
        provider_matrix::run(matrix_format, args.filter.as_deref(), json_format_alias);
        return;
    }
    if args.connector_matrix {
        connector_matrix::run(
            matrix_format,
            args.filter.as_deref(),
            &args.targets,
            json_format_alias,
        );
        return;
    }

    let mut target_strings = args.targets.clone();
    if args.workspace {
        append_workspace_pipelines(&mut target_strings);
    }
    if target_strings.is_empty() {
        check_input_error(
            args.json,
            "missing_targets",
            "`harn check` requires at least one target path, or `--workspace` with `[workspace].pipelines`",
        );
    }
    validate_runtime_manifests(&target_strings, args.json);

    let targets: Vec<&str> = target_strings.iter().map(String::as_str).collect();
    let files = collect_harn_targets(&targets);
    if files.is_empty() {
        check_input_error(
            args.json,
            "no_harn_files",
            "no .harn or .harn.txt files found under the given target(s)",
        );
    }

    let overrides = CheckCliOverrides::from(&args);
    let checked = if args.independent {
        check_files_independently(&files, &overrides, !args.json)
    } else {
        let (module_graph, parsed_sources) = build_module_graph_with_parsed_sources(&files);
        let cross_file_imports = collect_cross_file_imports(&module_graph);
        check_files(
            &files,
            &module_graph,
            parsed_sources,
            &cross_file_imports,
            &overrides,
            !args.json,
        )
    };

    let mut should_fail = false;
    let mut json_files = Vec::new();
    for checked_file in checked {
        should_fail |= checked_file
            .report
            .outcome()
            .should_fail(checked_file.strict);
        if args.json {
            json_files.push(checked_file.report);
        } else {
            checked_file.text.print();
        }
    }
    if args.json {
        print_json_report(json_files, should_fail);
    }
    if should_fail {
        process::exit(1);
    }
}

fn append_workspace_pipelines(targets: &mut Vec<String>) {
    let anchor = targets.first().map(Path::new);
    match package::load_workspace_config(anchor) {
        Some((workspace, manifest_dir)) if !workspace.pipelines.is_empty() => {
            for pipeline in workspace.pipelines {
                let candidate = Path::new(&pipeline);
                let resolved = if candidate.is_absolute() {
                    candidate.to_path_buf()
                } else {
                    manifest_dir.join(candidate)
                };
                targets.push(resolved.to_string_lossy().into_owned());
            }
        }
        Some(_) => {
            command_error("--workspace requires `[workspace].pipelines` in the nearest harn.toml")
        }
        None => {
            command_error("--workspace could not find a harn.toml walking up from the target(s)")
        }
    }
}

fn validate_runtime_manifests(targets: &[String], json: bool) {
    for target in targets {
        if let Err(error) = package::validate_runtime_manifest_extensions(Path::new(target)) {
            check_input_error(
                json,
                "manifest_extension_error",
                &format!("manifest extension validation failed: {error}"),
            );
        }
    }
}

fn check_input_error(json: bool, code: &str, message: &str) -> ! {
    if json {
        print_check_error(code, message);
    }
    command_error(message);
}

fn print_json_report(files: Vec<super::check_cmd::CheckFileReport>, should_fail: bool) {
    let report = CheckReport::from_files(files);
    let envelope = if should_fail {
        json_envelope::JsonEnvelope {
            schema_version: CHECK_SCHEMA_VERSION,
            ok: false,
            data: Some(report),
            error: Some(json_envelope::JsonError {
                code: "check_failed".to_string(),
                message: "one or more files failed `harn check`".to_string(),
                details: serde_json::Value::Null,
            }),
            warnings: Vec::new(),
        }
    } else {
        json_envelope::JsonEnvelope::ok(CHECK_SCHEMA_VERSION, report)
    };
    println!("{}", json_envelope::to_string_pretty(&envelope));
    if should_fail {
        process::exit(1);
    }
}