use colored::Colorize;
use rhai::{Engine, EvalAltResult, Scope, module_resolvers::FileModuleResolver};
use rustyline::completion::{Completer, Pair};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::{Hinter, HistoryHinter};
use rustyline::history::DefaultHistory;
use rustyline::validate::Validator;
use rustyline::{Config, Context, Editor, Helper};
use std::borrow::Cow;
use std::env;
use std::fs;
use std::io::{self, BufRead};
use std::path::PathBuf;
const TEXT_FUNCTIONS: &[(&str, &str, &str)] = &[
(
"replacer_new()",
"TextReplacerBuilder",
"Create a new text replacer builder",
),
(
"template_open(path)",
"TemplateBuilder",
"Open a template file",
),
("name_fix(s)", "string", "Fix/normalize a name"),
("path_fix(s)", "string", "Fix/normalize a path"),
("dedent(s)", "string", "Remove common leading whitespace"),
("prefix(s, p)", "string", "Add prefix to each line"),
];
const TEXT_REPLACER_METHODS: &[(&str, &str)] = &[
(".pattern(p)", "Set pattern to search for"),
(".replacement(r)", "Set replacement text"),
(".regex(yes)", "Use regex matching"),
(".case_insensitive(yes)", "Case-insensitive matching"),
(".and()", "Chain another replacement"),
(".build()", "Build the TextReplacer"),
(".replace(input)", "Apply replacements to text"),
(".replace_file(path)", "Replace in file, return result"),
(".replace_file_in_place(path)", "Replace file in place"),
];
const TEMPLATE_METHODS: &[(&str, &str)] = &[
(".add_var(name, value)", "Add variable"),
(".add_vars(map)", "Add multiple variables"),
(".render()", "Render template to string"),
(".render_to_file(path)", "Render to file"),
];
const NET_FUNCTIONS: &[(&str, &str, &str)] = &[
("tcp_check(host, port)", "bool", "Check TCP connectivity"),
("http_check(url)", "bool", "Check HTTP endpoint"),
("ssh_check(host)", "bool", "Check SSH connectivity"),
];
const HEROSCRIPT_FUNCTIONS: &[(&str, &str, &str)] = &[
("heroscript_parse(text)", "map", "Parse HeroScript text"),
(
"heroscript_parse_file(path)",
"map",
"Parse HeroScript file",
),
];
const OS_FUNCTIONS: &[(&str, &str, &str)] = &[
("copy(src, dest)", "string", "Copy file or directory"),
("copy_bin(src)", "string", "Copy binary to bin directory"),
("exist(path)", "bool", "Check if path exists"),
("find_file(dir, pattern)", "string", "Find a file"),
("find_files(dir, pattern)", "array", "Find multiple files"),
("find_dir(dir, pattern)", "string", "Find a directory"),
(
"find_dirs(dir, pattern)",
"array",
"Find multiple directories",
),
("delete(path)", "string", "Delete file or directory"),
("mkdir(path)", "string", "Create directory"),
("file_size(path)", "i64", "Get file size in bytes"),
("file_read(path)", "string", "Read file contents"),
("file_write(path, content)", "string", "Write to file"),
(
"file_write_append(path, content)",
"string",
"Append to file",
),
("mv(src, dest)", "string", "Move file or directory"),
("rsync(src, dest)", "string", "Sync directories"),
("chdir(path)", "string", "Change directory"),
("chmod_exec(path)", "string", "Make file executable"),
("which(cmd)", "string", "Find command in PATH"),
("cmd_ensure_exists(cmds)", "string", "Ensure commands exist"),
("download(url, dest, min_kb)", "string", "Download file"),
(
"download_file(url, dest, min_kb)",
"string",
"Download to file",
),
(
"download_install(url, min_kb)",
"string",
"Download and install",
),
("package_install(pkg)", "string", "Install package"),
("package_remove(pkg)", "string", "Remove package"),
("package_update()", "string", "Update package lists"),
("package_upgrade()", "string", "Upgrade packages"),
("package_list()", "array", "List installed packages"),
("package_search(query)", "array", "Search packages"),
("package_is_installed(pkg)", "bool", "Check if installed"),
("package_platform()", "string", "Get platform name"),
("platform_is_osx()", "bool", "Check if macOS"),
("platform_is_linux()", "bool", "Check if Linux"),
("platform_is_arm()", "bool", "Check if ARM"),
("platform_is_x86()", "bool", "Check if x86"),
];
const PROCESS_FUNCTIONS: &[(&str, &str, &str)] = &[
("run(cmd)", "CommandBuilder", "Create command builder"),
("run_command(cmd)", "CommandResult", "Run command (legacy)"),
("run_silent(cmd)", "CommandResult", "Run silently (legacy)"),
("kill(pattern)", "string", "Kill processes by pattern"),
("process_list(pattern)", "array", "List matching processes"),
("process_get(pattern)", "ProcessInfo", "Get single process"),
];
const BUILDER_METHODS: &[(&str, &str)] = &[
(".silent()", "Suppress output"),
(".ignore_error()", "Don't die on error"),
(".log()", "Enable logging"),
(".execute()", "Execute the command"),
];
const GIT_FUNCTIONS: &[(&str, &str, &str)] = &[
("git_tree_new(path)", "GitTree", "Create git tree manager"),
("git_clone(url)", "GitRepo", "Clone a repository"),
(
"parse_git_url_extended(url)",
"ParsedGitUrl",
"Parse git URL",
),
];
const QCOW2_FUNCTIONS: &[(&str, &str, &str)] = &[
(
"qcow2_create(path, size_gb)",
"string",
"Create qcow2 image",
),
("qcow2_info(path)", "map", "Get qcow2 image info"),
(
"qcow2_snapshot_create(path, name)",
"void",
"Create snapshot",
),
(
"qcow2_snapshot_delete(path, name)",
"void",
"Delete snapshot",
),
("qcow2_snapshot_list(path)", "array", "List snapshots"),
];
const BUILDAH_FUNCTIONS: &[(&str, &str, &str)] = &[
(
"bah(name, image)",
"Bah",
"Create container builder (fluent)",
),
(
"bah_new(name, image)",
"Builder",
"Create container builder",
),
];
const NERDCTL_FUNCTIONS: &[(&str, &str, &str)] = &[
(
"nerdctl_container_new(name)",
"Container",
"Create container",
),
(
"nerdctl_container_from_image(name, image)",
"Container",
"Create from image",
),
("nerdctl_run(image)", "CommandResult", "Run container"),
(
"nerdctl_run_with_name(image, name)",
"CommandResult",
"Run with name",
),
(
"nerdctl_exec(container, cmd)",
"CommandResult",
"Execute in container",
),
("nerdctl_stop(container)", "CommandResult", "Stop container"),
(
"nerdctl_remove(container)",
"CommandResult",
"Remove container",
),
("nerdctl_list(all)", "CommandResult", "List containers"),
(
"nerdctl_logs(container)",
"CommandResult",
"Get container logs",
),
("nerdctl_images()", "CommandResult", "List images"),
("nerdctl_image_pull(image)", "CommandResult", "Pull image"),
(
"nerdctl_image_remove(image)",
"CommandResult",
"Remove image",
),
];
const REDIS_FUNCTIONS: &[(&str, &str, &str)] = &[
("redis_new(url)", "RedisClient", "Create Redis client"),
("redis_get(key)", "string", "Get value by key"),
("redis_set(key, value)", "void", "Set key-value pair"),
("redis_del(key)", "void", "Delete key"),
("redis_exists(key)", "bool", "Check if key exists"),
("redis_keys(pattern)", "array", "Get keys matching pattern"),
("redis_hget(key, field)", "string", "Get hash field"),
("redis_hset(key, field, value)", "void", "Set hash field"),
("redis_hgetall(key)", "map", "Get all hash fields"),
];
const POSTGRES_FUNCTIONS: &[(&str, &str, &str)] = &[
(
"postgres_new(url)",
"PostgresClient",
"Create Postgres client",
),
(
"postgres_execute(sql, params)",
"i64",
"Execute SQL statement",
),
(
"postgres_query(sql, params)",
"array",
"Query and return rows",
),
("postgres_query_one(sql, params)", "map", "Query single row"),
];
const MQTT_FUNCTIONS: &[(&str, &str, &str)] = &[
("mqtt_new(host, port)", "MqttClient", "Create MQTT client"),
("mqtt_connect()", "void", "Connect to broker"),
("mqtt_disconnect()", "void", "Disconnect from broker"),
("mqtt_publish(topic, payload)", "void", "Publish message"),
("mqtt_subscribe(topic)", "void", "Subscribe to topic"),
];
const MYCELIUM_FUNCTIONS: &[(&str, &str, &str)] = &[
("mycelium_get_node_info()", "map", "Get node information"),
("mycelium_get_peers()", "array", "Get connected peers"),
];
const HETZNER_FUNCTIONS: &[(&str, &str, &str)] = &[
(
"hetzner_new(token)",
"HetznerClient",
"Create Hetzner client",
),
("hetzner_server_list()", "array", "List all servers"),
("hetzner_server_get(id)", "map", "Get server by ID"),
("hetzner_server_create(spec)", "map", "Create new server"),
("hetzner_server_delete(id)", "void", "Delete server"),
("hetzner_server_power_on(id)", "void", "Power on server"),
("hetzner_server_power_off(id)", "void", "Power off server"),
("hetzner_server_reboot(id)", "void", "Reboot server"),
];
const REPL_COMMANDS: &[(&str, &str)] = &[
("/help", "Show this help"),
("/functions", "List all available functions"),
("/core", "Show core functions (text, net, heroscript)"),
("/text", "Show text functions"),
("/net", "Show network functions"),
("/os", "Show OS functions"),
("/process", "Show process functions"),
("/git", "Show git functions"),
("/virt", "Show virt functions (qcow2, buildah, nerdctl)"),
(
"/clients",
"Show client functions (redis, postgres, mqtt, etc)",
),
("/redis", "Show Redis functions"),
("/postgres", "Show PostgreSQL functions"),
("/mqtt", "Show MQTT functions"),
("/hetzner", "Show Hetzner functions"),
("/builder", "Show command builder methods"),
("/scope", "Show current scope variables"),
("/clear", "Clear screen"),
("/load <file>", "Load and execute a .rhai script"),
("/quit", "Exit REPL (or Ctrl+D)"),
];
const RHAI_KEYWORDS: &[&str] = &[
"let", "const", "if", "else", "while", "loop", "for", "in", "break", "continue", "return",
"throw", "try", "catch", "fn", "private", "import", "export", "as", "true", "false", "null",
];
#[derive(Helper)]
struct ReplHelper {
hinter: HistoryHinter,
}
impl Completer for ReplHelper {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &Context<'_>,
) -> rustyline::Result<(usize, Vec<Pair>)> {
let mut completions = Vec::new();
let line_to_cursor = &line[..pos];
let word_start = line_to_cursor
.rfind(|c: char| !c.is_alphanumeric() && c != '_')
.map(|i| i + 1)
.unwrap_or(0);
let word = &line_to_cursor[word_start..];
if word.is_empty() {
return Ok((pos, completions));
}
if word.starts_with('/') {
for (cmd, desc) in REPL_COMMANDS {
let cmd_name = cmd.split_whitespace().next().unwrap_or(cmd);
if cmd_name.starts_with(word) {
completions.push(Pair {
display: format!("{} - {}", cmd, desc),
replacement: cmd_name.to_string(),
});
}
}
return Ok((word_start, completions));
}
if word.starts_with('.') || line_to_cursor.ends_with('.') {
let method_word = if word.starts_with('.') { word } else { "." };
for (method, desc) in BUILDER_METHODS
.iter()
.chain(TEXT_REPLACER_METHODS.iter())
.chain(TEMPLATE_METHODS.iter())
{
if method.starts_with(method_word) {
let replacement = method.split('(').next().unwrap_or(method);
completions.push(Pair {
display: format!("{} - {}", method, desc),
replacement: replacement.to_string(),
});
}
}
let start = if word.starts_with('.') {
word_start
} else {
pos
};
return Ok((start, completions));
}
let all_functions: Vec<(&str, &str, &str)> = TEXT_FUNCTIONS
.iter()
.chain(NET_FUNCTIONS.iter())
.chain(HEROSCRIPT_FUNCTIONS.iter())
.chain(OS_FUNCTIONS.iter())
.chain(PROCESS_FUNCTIONS.iter())
.chain(GIT_FUNCTIONS.iter())
.chain(QCOW2_FUNCTIONS.iter())
.chain(BUILDAH_FUNCTIONS.iter())
.chain(NERDCTL_FUNCTIONS.iter())
.chain(REDIS_FUNCTIONS.iter())
.chain(POSTGRES_FUNCTIONS.iter())
.chain(MQTT_FUNCTIONS.iter())
.chain(MYCELIUM_FUNCTIONS.iter())
.chain(HETZNER_FUNCTIONS.iter())
.copied()
.collect();
for (func, ret, desc) in all_functions {
let func_name = func.split('(').next().unwrap_or(func);
if func_name.starts_with(word) {
completions.push(Pair {
display: format!("{} -> {} - {}", func, ret, desc),
replacement: func_name.to_string(),
});
}
}
for kw in RHAI_KEYWORDS {
if kw.starts_with(word) && !completions.iter().any(|p| p.replacement == *kw) {
completions.push(Pair {
display: format!("{} (keyword)", kw),
replacement: kw.to_string(),
});
}
}
Ok((word_start, completions))
}
}
impl Hinter for ReplHelper {
type Hint = String;
fn hint(&self, line: &str, pos: usize, ctx: &Context<'_>) -> Option<String> {
if let Some(hint) = self.hinter.hint(line, pos, ctx) {
return Some(hint);
}
let line_to_cursor = &line[..pos];
let all_functions: Vec<(&str, &str, &str)> = TEXT_FUNCTIONS
.iter()
.chain(NET_FUNCTIONS.iter())
.chain(HEROSCRIPT_FUNCTIONS.iter())
.chain(OS_FUNCTIONS.iter())
.chain(PROCESS_FUNCTIONS.iter())
.chain(GIT_FUNCTIONS.iter())
.chain(QCOW2_FUNCTIONS.iter())
.chain(BUILDAH_FUNCTIONS.iter())
.chain(NERDCTL_FUNCTIONS.iter())
.chain(REDIS_FUNCTIONS.iter())
.chain(POSTGRES_FUNCTIONS.iter())
.chain(MQTT_FUNCTIONS.iter())
.chain(MYCELIUM_FUNCTIONS.iter())
.chain(HETZNER_FUNCTIONS.iter())
.copied()
.collect();
for (func, ret, desc) in all_functions {
let func_name = func.split('(').next().unwrap_or(func);
if line_to_cursor.ends_with(func_name) {
let signature = &func[func_name.len()..];
return Some(
format!("{} -> {} | {}", signature, ret, desc)
.dimmed()
.to_string(),
);
}
}
None
}
}
impl Highlighter for ReplHelper {
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
let mut result = String::with_capacity(line.len() * 2);
let mut chars = line.chars().peekable();
let mut current_word = String::new();
while let Some(c) = chars.next() {
if c.is_alphanumeric() || c == '_' {
current_word.push(c);
} else {
if !current_word.is_empty() {
result.push_str(&highlight_word(¤t_word));
current_word.clear();
}
if c == '"' {
result.push_str(&format!("{}", "\"".green()));
let mut string_content = String::new();
while let Some(&next) = chars.peek() {
chars.next();
if next == '"' {
result.push_str(&format!("{}", string_content.green()));
result.push_str(&format!("{}", "\"".green()));
break;
} else if next == '\\' {
string_content.push(next);
if let Some(escaped) = chars.next() {
string_content.push(escaped);
}
} else {
string_content.push(next);
}
}
}
else if c == '/' && chars.peek() == Some(&'/') {
result.push_str(&format!(
"{}",
format!("//{}", chars.collect::<String>()).dimmed()
));
break;
}
else if "=+-*/<>!&|".contains(c) {
result.push_str(&format!("{}", c.to_string().yellow()));
}
else if "()[]{}".contains(c) {
result.push_str(&format!("{}", c.to_string().cyan()));
} else {
result.push(c);
}
}
}
if !current_word.is_empty() {
result.push_str(&highlight_word(¤t_word));
}
Cow::Owned(result)
}
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
_default: bool,
) -> Cow<'b, str> {
Cow::Owned(format!("{}", prompt.cyan().bold()))
}
fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
Cow::Owned(format!("{}", hint.dimmed()))
}
fn highlight_char(
&self,
_line: &str,
_pos: usize,
_kind: rustyline::highlight::CmdKind,
) -> bool {
true
}
}
fn highlight_word(word: &str) -> String {
if RHAI_KEYWORDS.contains(&word) {
return format!("{}", word.magenta().bold());
}
let all_func_names: Vec<&str> = TEXT_FUNCTIONS
.iter()
.chain(NET_FUNCTIONS.iter())
.chain(HEROSCRIPT_FUNCTIONS.iter())
.chain(OS_FUNCTIONS.iter())
.chain(PROCESS_FUNCTIONS.iter())
.chain(GIT_FUNCTIONS.iter())
.chain(QCOW2_FUNCTIONS.iter())
.chain(BUILDAH_FUNCTIONS.iter())
.chain(NERDCTL_FUNCTIONS.iter())
.chain(REDIS_FUNCTIONS.iter())
.chain(POSTGRES_FUNCTIONS.iter())
.chain(MQTT_FUNCTIONS.iter())
.chain(MYCELIUM_FUNCTIONS.iter())
.chain(HETZNER_FUNCTIONS.iter())
.map(|(f, _, _)| f.split('(').next().unwrap_or(f))
.collect();
if all_func_names.contains(&word) {
return format!("{}", word.blue().bold());
}
if word.chars().all(|c| c.is_numeric() || c == '.') {
return format!("{}", word.yellow());
}
if word == "true" || word == "false" {
return format!("{}", word.yellow().bold());
}
word.to_string()
}
impl Validator for ReplHelper {}
fn print_banner() {
println!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
println!(
"{}",
"║ Herodo Interactive Shell ║".cyan()
);
println!(
"{}",
"║ SAL Aggregator: Core + OS + Clients ║".cyan()
);
println!(
"{}",
"╠═══════════════════════════════════════════════════════════╣".cyan()
);
println!(
"{} {}",
"║".cyan(),
format!(
"{:<56} {}",
"Tab: completion | Up/Down: history | Ctrl+D: exit", "║"
)
.cyan()
);
println!(
"{} {}",
"║".cyan(),
format!(
"{:<56} {}",
"Type /help for commands, /functions for API", "║"
)
.cyan()
);
println!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
println!();
}
fn print_help() {
println!("{}", "REPL Commands:".yellow().bold());
for (cmd, desc) in REPL_COMMANDS {
println!(" {:20} {}", cmd.cyan(), desc);
}
println!();
println!("{}", "Tips:".yellow().bold());
println!(" - Press {} to autocomplete function names", "Tab".cyan());
println!(" - Use {} arrows for command history", "Up/Down".cyan());
println!(
" - Multi-line input: end lines with {} or use {}",
"\\".cyan(),
"{ }".cyan()
);
}
fn print_function_group(title: &str, functions: &[(&str, &str, &str)]) {
println!("{}", format!("=== {} ===", title).yellow().bold());
for (func, ret, desc) in functions {
println!(
" {} {} {} {}",
func.blue(),
"->".dimmed(),
ret.green(),
format!("- {}", desc).dimmed()
);
}
}
fn print_method_group(title: &str, methods: &[(&str, &str)]) {
println!("{}", format!("--- {} ---", title).cyan().bold());
for (method, desc) in methods {
println!(" {:30} {}", method.cyan(), desc);
}
}
fn print_functions() {
print_function_group("Core - Text Functions", TEXT_FUNCTIONS);
print_method_group("TextReplacer Methods", TEXT_REPLACER_METHODS);
print_method_group("Template Methods", TEMPLATE_METHODS);
println!();
print_function_group("Core - Network Functions", NET_FUNCTIONS);
println!();
print_function_group("Core - HeroScript Functions", HEROSCRIPT_FUNCTIONS);
println!();
print_function_group("OS Functions", OS_FUNCTIONS);
println!();
print_function_group("Process Functions", PROCESS_FUNCTIONS);
print_method_group("Command Builder Methods", BUILDER_METHODS);
println!();
print_function_group("Git Functions", GIT_FUNCTIONS);
println!();
print_function_group("Virt - QCOW2 Functions", QCOW2_FUNCTIONS);
print_function_group("Virt - Buildah Functions", BUILDAH_FUNCTIONS);
print_function_group("Virt - Nerdctl Functions", NERDCTL_FUNCTIONS);
println!();
print_function_group("Redis Functions", REDIS_FUNCTIONS);
println!();
print_function_group("PostgreSQL Functions", POSTGRES_FUNCTIONS);
println!();
print_function_group("MQTT Functions", MQTT_FUNCTIONS);
println!();
print_function_group("Mycelium Functions", MYCELIUM_FUNCTIONS);
println!();
print_function_group("Hetzner Functions", HETZNER_FUNCTIONS);
}
fn print_core_functions() {
print_function_group("Text Functions", TEXT_FUNCTIONS);
print_method_group("TextReplacer Methods", TEXT_REPLACER_METHODS);
print_method_group("Template Methods", TEMPLATE_METHODS);
println!();
print_function_group("Network Functions", NET_FUNCTIONS);
println!();
print_function_group("HeroScript Functions", HEROSCRIPT_FUNCTIONS);
}
fn print_client_functions() {
print_function_group("Redis Functions", REDIS_FUNCTIONS);
println!();
print_function_group("PostgreSQL Functions", POSTGRES_FUNCTIONS);
println!();
print_function_group("MQTT Functions", MQTT_FUNCTIONS);
println!();
print_function_group("Mycelium Functions", MYCELIUM_FUNCTIONS);
println!();
print_function_group("Hetzner Functions", HETZNER_FUNCTIONS);
}
fn print_virt_functions() {
print_function_group("QCOW2 Functions", QCOW2_FUNCTIONS);
println!();
print_function_group("Buildah Functions", BUILDAH_FUNCTIONS);
println!();
print_function_group("Nerdctl Functions", NERDCTL_FUNCTIONS);
}
fn print_scope(scope: &Scope) {
if scope.is_empty() {
println!("{}", "Scope is empty.".dimmed());
return;
}
println!("{}", "Current scope:".yellow().bold());
for (name, _constant, value) in scope.iter() {
println!(" {} = {:?}", name.cyan(), value);
}
}
fn create_engine() -> Result<Engine, Box<EvalAltResult>> {
create_engine_with_base_path(None)
}
fn create_engine_with_base_path(
base_path: Option<&std::path::Path>,
) -> Result<Engine, Box<EvalAltResult>> {
let mut engine = Engine::new();
herolib_core::rhai::register_core_module(&mut engine)?;
herolib_crypt::rhai::register_crypt_module(&mut engine)?;
herolib_os::rhai::register_system_module(&mut engine)?;
herolib_clients::rhai::register_clients_module(&mut engine)?;
herolib_virt::rhai::register_kubernetes_module(&mut engine)?;
let resolver = if let Some(path) = base_path {
FileModuleResolver::new_with_path(path)
} else {
FileModuleResolver::new_with_path(
std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
)
};
engine.set_module_resolver(resolver);
engine.on_print(|s| {
println!("{}", s);
});
engine.on_debug(|s, source, pos| {
let location = match source {
Some(src) => format!("[{}:{}] ", src, pos),
None if !pos.is_none() => format!("[{}] ", pos),
None => String::new(),
};
println!("{} {}{}", "[DEBUG]".dimmed(), location.dimmed(), s);
});
Ok(engine)
}
fn load_script(file_path: &str, scope: &mut Scope) -> Result<(), String> {
let expanded_path = if file_path.starts_with("~/") {
dirs::home_dir()
.map(|h| h.join(&file_path[2..]))
.unwrap_or_else(|| PathBuf::from(file_path))
} else {
PathBuf::from(file_path)
};
if !expanded_path.exists() {
return Err(format!("File not found: {}", expanded_path.display()));
}
println!("{} {}", "Loading:".green(), expanded_path.display());
let base_path = expanded_path
.parent()
.unwrap_or_else(|| std::path::Path::new("."));
let engine = create_engine_with_base_path(Some(base_path))
.map_err(|e| format!("Failed to create engine: {}", e))?;
let script =
fs::read_to_string(&expanded_path).map_err(|e| format!("Failed to read file: {}", e))?;
engine
.run_with_scope(scope, &script)
.map_err(|e| format!("{}", e))
}
fn run_script_file(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let base_path = path.parent().unwrap_or_else(|| std::path::Path::new("."));
let engine = create_engine_with_base_path(Some(base_path))?;
let mut scope = Scope::new();
let script = fs::read_to_string(path)?;
match engine.run_with_scope(&mut scope, &script) {
Ok(_) => Ok(()),
Err(e) => {
eprintln!("{}: {}", "Script error".red().bold(), e);
Err(e.into())
}
}
}
fn run_from_stdin() -> Result<(), Box<dyn std::error::Error>> {
let engine = create_engine()?;
let mut scope = Scope::new();
let stdin = io::stdin();
let script: String = stdin
.lock()
.lines()
.filter_map(|l| l.ok())
.collect::<Vec<_>>()
.join("\n");
match engine.run_with_scope(&mut scope, &script) {
Ok(_) => Ok(()),
Err(e) => {
eprintln!("{}: {}", "Script error".red().bold(), e);
Err(e.into())
}
}
}
fn run_repl() -> Result<(), Box<dyn std::error::Error>> {
print_banner();
let engine = create_engine()?;
let mut scope = Scope::new();
let config = Config::builder()
.history_ignore_space(true)
.completion_type(rustyline::CompletionType::List)
.edit_mode(rustyline::EditMode::Emacs)
.build();
let helper = ReplHelper {
hinter: HistoryHinter::new(),
};
let mut rl: Editor<ReplHelper, DefaultHistory> = Editor::with_config(config)?;
rl.set_helper(Some(helper));
let history_path = dirs::home_dir()
.map(|h| h.join(".herodo_history"))
.unwrap_or_else(|| PathBuf::from(".herodo_history"));
let _ = rl.load_history(&history_path);
let mut multiline_buffer = String::new();
loop {
let prompt = if multiline_buffer.is_empty() {
"herodo> "
} else {
" ... "
};
match rl.readline(prompt) {
Ok(line) => {
let line = line.trim();
if multiline_buffer.is_empty() {
match line {
"/help" | "/h" => {
print_help();
continue;
}
"/functions" | "/fn" => {
print_functions();
continue;
}
"/core" => {
print_core_functions();
continue;
}
"/text" => {
print_function_group("Text Functions", TEXT_FUNCTIONS);
continue;
}
"/net" => {
print_function_group("Network Functions", NET_FUNCTIONS);
continue;
}
"/os" => {
print_function_group("OS Functions", OS_FUNCTIONS);
continue;
}
"/process" | "/proc" => {
print_function_group("Process Functions", PROCESS_FUNCTIONS);
print_method_group("Command Builder Methods", BUILDER_METHODS);
continue;
}
"/git" => {
print_function_group("Git Functions", GIT_FUNCTIONS);
continue;
}
"/virt" => {
print_virt_functions();
continue;
}
"/clients" => {
print_client_functions();
continue;
}
"/redis" => {
print_function_group("Redis Functions", REDIS_FUNCTIONS);
continue;
}
"/postgres" => {
print_function_group("PostgreSQL Functions", POSTGRES_FUNCTIONS);
continue;
}
"/mqtt" => {
print_function_group("MQTT Functions", MQTT_FUNCTIONS);
continue;
}
"/hetzner" => {
print_function_group("Hetzner Functions", HETZNER_FUNCTIONS);
continue;
}
"/builder" => {
print_method_group("Command Builder Methods", BUILDER_METHODS);
print_method_group("TextReplacer Methods", TEXT_REPLACER_METHODS);
print_method_group("Template Methods", TEMPLATE_METHODS);
continue;
}
"/scope" => {
print_scope(&scope);
continue;
}
"/clear" => {
print!("\x1B[2J\x1B[1;1H");
print_banner();
continue;
}
"/quit" | "/exit" | "/q" => {
println!("{}", "Goodbye!".cyan());
break;
}
"" => continue,
_ if line.starts_with("/load ") => {
let file_path = line.strip_prefix("/load ").unwrap().trim();
if !file_path.ends_with(".rhai") {
println!(
"{}: File must have .rhai extension, got: {}",
"Error".red().bold(),
file_path
);
println!();
continue;
}
match load_script(file_path, &mut scope) {
Ok(_) => println!("{}", "Script executed successfully.".green()),
Err(e) => println!("{}: {}", "Error".red().bold(), e),
}
println!();
continue;
}
_ if line.starts_with('/') => {
println!(
"{}: Unknown command '{}'. Type /help for commands.",
"Error".red().bold(),
line
);
continue;
}
_ => {}
}
}
multiline_buffer.push_str(line);
multiline_buffer.push('\n');
let trimmed = line.trim_end();
if trimmed.ends_with('\\') {
multiline_buffer = multiline_buffer
.trim_end()
.strip_suffix('\\')
.unwrap_or(&multiline_buffer)
.to_string();
multiline_buffer.push('\n');
continue;
}
let open_braces = multiline_buffer.matches('{').count();
let close_braces = multiline_buffer.matches('}').count();
let open_brackets = multiline_buffer.matches('[').count();
let close_brackets = multiline_buffer.matches(']').count();
let open_parens = multiline_buffer.matches('(').count();
let close_parens = multiline_buffer.matches(')').count();
if open_braces > close_braces
|| open_brackets > close_brackets
|| open_parens > close_parens
{
continue;
}
let script = std::mem::take(&mut multiline_buffer);
let script = script.trim();
if script.is_empty() {
continue;
}
let _ = rl.add_history_entry(script);
match engine.eval_with_scope::<rhai::Dynamic>(&mut scope, script) {
Ok(result) => {
if !result.is_unit() {
println!("{} {}", "=>".green(), result);
}
}
Err(e) => {
println!("{}: {}", "Error".red().bold(), e);
}
}
println!();
}
Err(ReadlineError::Interrupted) => {
if !multiline_buffer.is_empty() {
multiline_buffer.clear();
println!("{}", "^C (input cleared)".dimmed());
} else {
println!("{}", "^C (use /quit or Ctrl+D to exit)".dimmed());
}
}
Err(ReadlineError::Eof) => {
println!("{}", "Goodbye!".cyan());
break;
}
Err(err) => {
println!("{}: {:?}", "Error".red(), err);
break;
}
}
}
let _ = rl.save_history(&history_path);
Ok(())
}
fn print_usage(program: &str) {
println!(
"{}",
"herodo - Interactive Rhai shell aggregating SAL packages"
.cyan()
.bold()
);
println!();
println!("{}", "USAGE:".yellow().bold());
println!(" {} Start interactive REPL", program);
println!(
" {} <script.rhai> Run a .rhai script file (must end with .rhai)",
program
);
println!(" {} --ui Start interactive REPL", program);
println!(" {} -i Read script from stdin", program);
println!(" {} --help Show this help", program);
println!();
println!("{}", "MODULES:".yellow().bold());
println!(
" {} - Text processing, networking, HeroScript",
"herolib-core".blue()
);
println!(
" {} - OS operations, process, git, virt",
"herolib-os".blue()
);
println!(
" {} - Redis, PostgreSQL, MQTT, Mycelium, Hetzner",
"herolib-clients".blue()
);
println!();
println!("{}", "EXAMPLES:".yellow().bold());
println!(" {}", format!("{} examples/test.rhai", program).dimmed());
println!(" {}", format!("{} --ui", program).dimmed());
println!(
" {}",
format!("echo 'print(\"hello\")' | {} -i", program).dimmed()
);
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
if let Err(e) = run_repl() {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
return;
}
match args[1].as_str() {
"-i" | "--stdin" => {
if let Err(e) = run_from_stdin() {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
}
"--ui" | "--repl" => {
if let Err(e) = run_repl() {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
}
"-h" | "--help" | "help" => {
print_usage(&args[0]);
}
arg => {
let script_path = PathBuf::from(arg);
if !script_path.to_string_lossy().ends_with(".rhai") {
eprintln!(
"{}: File must have .rhai extension, got: {}",
"Error".red().bold(),
arg
);
println!();
print_usage(&args[0]);
std::process::exit(1);
}
if script_path.exists() {
if run_script_file(&script_path).is_err() {
std::process::exit(1);
}
} else {
eprintln!("{}: File not found: {}", "Error".red().bold(), arg);
println!();
print_usage(&args[0]);
std::process::exit(1);
}
}
}
}