use std::collections::HashSet;
use std::io::Write;
use std::path::Path;
use crate::cli_bytecode::find_cli_script_bytecode;
use crate::commands::run::{
execute_run, execute_run_with_sandbox_options, CliLlmMockMode, RunOutcome, RunProfileOptions,
RunSandboxOptions,
};
use crate::env_guard::ScopedEnvVar;
pub const JSON_MODE_ENV: &str = "HARN_OUTPUT_JSON";
pub const DISABLE_AOT_ENV: &str = "HARN_DISABLE_AOT_CLI";
pub const CACHE_DEBUG_ENV: &str = "HARN_BYTECODE_CACHE_DEBUG";
const EX_SOFTWARE: i32 = 70;
pub async fn dispatch_to_embedded_script(
script_name: &str,
argv: Vec<String>,
json_mode: bool,
) -> i32 {
let outcome = run_embedded_script(script_name, argv, json_mode).await;
flush_outcome(&outcome);
outcome.exit_code
}
pub async fn dispatch_to_embedded_script_no_sandbox(
script_name: &str,
argv: Vec<String>,
json_mode: bool,
) -> i32 {
let mut outcome = run_embedded_script_with_sandbox(
script_name,
argv,
json_mode,
RunSandboxOptions::disabled(),
)
.await;
outcome.stderr = strip_sandbox_warning(&outcome.stderr);
flush_outcome(&outcome);
outcome.exit_code
}
const SANDBOX_WARNING: &str =
"warning: harn run --no-sandbox disables filesystem, process, and egress sandbox defaults\n";
fn strip_sandbox_warning(stderr: &str) -> String {
if let Some(rest) = stderr.strip_prefix(SANDBOX_WARNING) {
rest.to_string()
} else {
stderr.to_string()
}
}
pub async fn run_embedded_script(
script_name: &str,
argv: Vec<String>,
json_mode: bool,
) -> RunOutcome {
run_embedded_script_inner(script_name, argv, json_mode, None).await
}
pub async fn run_embedded_script_with_sandbox(
script_name: &str,
argv: Vec<String>,
json_mode: bool,
sandbox: RunSandboxOptions,
) -> RunOutcome {
run_embedded_script_inner(script_name, argv, json_mode, Some(sandbox)).await
}
async fn run_embedded_script_inner(
script_name: &str,
argv: Vec<String>,
json_mode: bool,
sandbox: Option<RunSandboxOptions>,
) -> RunOutcome {
let Some(source) = harn_stdlib::find_cli_script(script_name) else {
return RunOutcome {
stdout: String::new(),
stderr: format!(
"internal error: CLI dispatch target '{script_name}' is not embedded.\n\
This is a harn-cli build bug — please file an issue at \
https://github.com/burin-labs/harn/issues.\n"
),
exit_code: EX_SOFTWARE,
};
};
let temp = match write_script_to_tempfile(script_name, source) {
Ok(t) => t,
Err(error) => {
return RunOutcome {
stdout: String::new(),
stderr: format!(
"internal error: failed to materialize embedded CLI script \
'{script_name}': {error}\n"
),
exit_code: EX_SOFTWARE,
};
}
};
let path_str = temp.path().to_string_lossy().into_owned();
let _adjacent = maybe_drop_adjacent_bytecode(script_name, temp.path());
let _scope = json_mode.then(|| ScopedEnvVar::set(JSON_MODE_ENV, "1"));
let outcome = match sandbox {
Some(sandbox) => {
execute_run_with_sandbox_options(
&path_str,
false,
HashSet::new(),
argv,
Vec::new(),
CliLlmMockMode::Off,
None,
RunProfileOptions::default(),
sandbox,
)
.await
}
None => {
execute_run(
&path_str,
false,
HashSet::new(),
argv,
Vec::new(),
CliLlmMockMode::Off,
None,
RunProfileOptions::default(),
)
.await
}
};
drop(temp);
outcome
}
fn flush_outcome(outcome: &RunOutcome) {
if !outcome.stderr.is_empty() {
let _ = std::io::stderr().write_all(outcome.stderr.as_bytes());
}
if !outcome.stdout.is_empty() {
let _ = std::io::stdout().write_all(outcome.stdout.as_bytes());
}
}
struct AdjacentBytecodeGuard {
path: std::path::PathBuf,
}
impl Drop for AdjacentBytecodeGuard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
fn aot_enabled() -> bool {
match std::env::var(DISABLE_AOT_ENV).ok().as_deref() {
Some(value) => matches!(
value.to_ascii_lowercase().as_str(),
"" | "0" | "false" | "no" | "off"
),
None => true,
}
}
fn cache_debug_enabled() -> bool {
std::env::var_os(CACHE_DEBUG_ENV).is_some()
}
fn maybe_drop_adjacent_bytecode(
script_name: &str,
source_tempfile_path: &Path,
) -> Option<AdjacentBytecodeGuard> {
if !aot_enabled() {
return None;
}
let bytes = find_cli_script_bytecode(script_name)?;
let adjacent = harn_vm::bytecode_cache::adjacent_cache_path(source_tempfile_path)?;
match std::fs::write(&adjacent, bytes) {
Ok(()) => Some(AdjacentBytecodeGuard { path: adjacent }),
Err(err) => {
if cache_debug_enabled() {
eprintln!(
"[harn] AOT bytecode drop failed for `{script_name}` at {}: {err}",
adjacent.display()
);
}
None
}
}
}
fn write_script_to_tempfile(name: &str, source: &str) -> std::io::Result<tempfile::NamedTempFile> {
let safe_name = name.replace('/', "-");
let mut file = tempfile::Builder::new()
.prefix(&format!("harn-cli-{safe_name}-"))
.suffix(".harn")
.tempfile()?;
file.write_all(source.as_bytes())?;
file.flush()?;
Ok(file)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn missing_script_returns_software_error() {
let outcome = run_embedded_script("definitely/not/a/real/script", vec![], false).await;
assert_eq!(outcome.exit_code, EX_SOFTWARE);
assert!(
outcome.stderr.contains("not embedded"),
"stderr should explain the dispatch miss; got: {}",
outcome.stderr
);
assert!(outcome.stdout.is_empty());
}
#[tokio::test]
async fn echo_round_trips_argv_as_json_array() {
let outcome = run_embedded_script("echo", vec!["foo".into(), "bar".into()], false).await;
assert_eq!(
outcome.exit_code, 0,
"echo failed: stderr={}",
outcome.stderr
);
assert_eq!(outcome.stdout, "[\"foo\",\"bar\"]\n");
assert!(outcome.stderr.is_empty(), "stderr was: {}", outcome.stderr);
}
#[tokio::test]
async fn echo_handles_empty_argv() {
let outcome = run_embedded_script("echo", vec![], false).await;
assert_eq!(outcome.exit_code, 0, "stderr={}", outcome.stderr);
assert_eq!(outcome.stdout, "[]\n");
}
}