use colored::Colorize;
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;
use std::process::{Command, exit};
use std::time::Duration;
use heroforge_core::rhai_api::{ForgeEngine, SocketClient, SocketServer};
const REPO_FUNCTIONS: &[(&str, &str, &str)] = &[
("repo_open(path)", "Repository", "Open repository read-only"),
(
"repo_open_rw(path)",
"Repository",
"Open repository read-write",
),
("repo_init(path)", "Repository", "Create new repository"),
("repo_rebuild(repo)", "void", "Rebuild repository metadata"),
("repo_project_name(repo)", "string", "Get project name"),
("repo_project_code(repo)", "string", "Get project code"),
];
const FS_FUNCTIONS: &[(&str, &str, &str)] = &[
(
"fs_new(repo, author)",
"FsHandle",
"Create filesystem interface",
),
("fs_read(fs, path)", "string", "Read file as string"),
("fs_write(fs, path, content)", "void", "Write file"),
("fs_delete(fs, path)", "void", "Delete file"),
("fs_exists(fs, path)", "bool", "Check if file exists"),
("fs_list(fs, dir)", "array", "List directory contents"),
("fs_commit(fs)", "string", "Force commit changes"),
("fs_status(fs)", "string", "Get filesystem status"),
];
const FILE_FUNCTIONS: &[(&str, &str, &str)] = &[
("files_list(repo)", "array", "List files on trunk"),
(
"files_list_branch(repo, branch)",
"array",
"List files on branch",
),
("files_read(repo, path)", "string", "Read file from trunk"),
(
"files_read_branch(repo, path, branch)",
"string",
"Read file from branch",
),
(
"files_find(repo, pattern)",
"array",
"Find files by pattern",
),
];
const BRANCH_FUNCTIONS: &[(&str, &str, &str)] = &[
("branches_list(repo)", "array", "List all branches"),
(
"branches_create(repo, name, parent)",
"string",
"Create new branch",
),
("tags_list(repo)", "array", "List all tags"),
(
"tags_create(repo, name, target)",
"string",
"Create new tag",
),
("history_recent(repo, limit)", "array", "Get recent commits"),
];
const MODIFY_FUNCTIONS: &[(&str, &str, &str)] = &[
("modify_new(repo)", "Modify", "Create modify builder"),
("modify_copy(m, src, dst)", "Modify", "Copy file"),
("modify_move(m, src, dst)", "Modify", "Move file"),
("modify_delete(m, path)", "Modify", "Delete file"),
(
"modify_execute(m, author, msg)",
"string",
"Execute modifications",
),
];
const FIND_FUNCTIONS: &[(&str, &str, &str)] = &[
("find_new(repo)", "Find", "Create find builder"),
("find_name(f, pattern)", "Find", "Filter by name pattern"),
("find_extension(f, ext)", "Find", "Filter by extension"),
("find_dir(f, path)", "Find", "Filter by directory"),
("find_execute(f)", "array", "Execute find"),
];
const UTILITY_FUNCTIONS: &[(&str, &str, &str)] = &[
("print(msg)", "void", "Print message"),
("debug(msg)", "void", "Print debug message"),
("sleep(ms)", "void", "Sleep for milliseconds"),
("env(name)", "string", "Get environment variable"),
("cwd()", "string", "Get current directory"),
("home()", "string", "Get home directory"),
("uuid()", "string", "Generate UUID"),
("timestamp()", "i64", "Get Unix timestamp"),
("version()", "string", "Get heroforge-core version"),
];
const REPL_COMMANDS: &[(&str, &str)] = &[
("/help", "Show this help"),
("/functions", "List all available functions"),
("/repo", "Show repository functions"),
("/fs", "Show filesystem functions"),
("/files", "Show file functions"),
("/branches", "Show branch/tag functions"),
("/modify", "Show modify builder functions"),
("/find", "Show find builder functions"),
("/utils", "Show utility functions"),
("/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));
}
let all_functions: Vec<&(&str, &str, &str)> = REPO_FUNCTIONS
.iter()
.chain(FS_FUNCTIONS.iter())
.chain(FILE_FUNCTIONS.iter())
.chain(BRANCH_FUNCTIONS.iter())
.chain(MODIFY_FUNCTIONS.iter())
.chain(FIND_FUNCTIONS.iter())
.chain(UTILITY_FUNCTIONS.iter())
.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)> = REPO_FUNCTIONS
.iter()
.chain(FS_FUNCTIONS.iter())
.chain(FILE_FUNCTIONS.iter())
.chain(BRANCH_FUNCTIONS.iter())
.chain(MODIFY_FUNCTIONS.iter())
.chain(FIND_FUNCTIONS.iter())
.chain(UTILITY_FUNCTIONS.iter())
.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, _forced: bool) -> bool {
true
}
}
fn highlight_word(word: &str) -> String {
if RHAI_KEYWORDS.contains(&word) {
return format!("{}", word.magenta().bold());
}
let all_func_names: Vec<&str> = REPO_FUNCTIONS
.iter()
.chain(FS_FUNCTIONS.iter())
.chain(FILE_FUNCTIONS.iter())
.chain(BRANCH_FUNCTIONS.iter())
.chain(MODIFY_FUNCTIONS.iter())
.chain(FIND_FUNCTIONS.iter())
.chain(UTILITY_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!(
"{}",
"║ Heroforge Core Interactive Shell ║".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!(" {:15} {}", 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_functions() {
print_category("Repository Functions", REPO_FUNCTIONS);
println!();
print_category("Filesystem Functions", FS_FUNCTIONS);
println!();
print_category("File Functions", FILE_FUNCTIONS);
println!();
print_category("Branch/Tag Functions", BRANCH_FUNCTIONS);
println!();
print_category("Modify Builder Functions", MODIFY_FUNCTIONS);
println!();
print_category("Find Builder Functions", FIND_FUNCTIONS);
println!();
print_category("Utility Functions", UTILITY_FUNCTIONS);
}
fn print_category(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_scope(scope: &rhai::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 get_socket_path(args: &[String]) -> PathBuf {
for i in 0..args.len() {
if args[i] == "-s" && i + 1 < args.len() {
return PathBuf::from(&args[i + 1]);
}
}
heroforge_core::rhai_api::socket_server::default_socket_path()
}
fn is_daemon_running(socket_path: &PathBuf) -> bool {
socket_path.exists()
&& tokio::runtime::Runtime::new()
.unwrap()
.block_on(heroforge_core::rhai_api::socket_server::ping(socket_path))
}
fn wait_for_daemon(socket_path: &PathBuf, timeout_secs: u64) -> bool {
let start = std::time::Instant::now();
let timeout = Duration::from_secs(timeout_secs);
while start.elapsed() < timeout {
if is_daemon_running(socket_path) {
return true;
}
std::thread::sleep(Duration::from_millis(100));
}
false
}
fn start_daemon_foreground(socket_path: PathBuf) {
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
rt.block_on(async {
match SocketServer::new(socket_path) {
Ok(server) => {
let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1);
let shutdown_tx_clone = shutdown_tx.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
eprintln!("\nReceived Ctrl+C, shutting down...");
let _ = shutdown_tx_clone.send(());
});
if let Err(e) = server.run_with_shutdown(shutdown_rx).await {
eprintln!("Server error: {}", e);
exit(1);
}
}
Err(e) => {
eprintln!("Failed to create server: {}", e);
exit(1);
}
}
});
}
fn start_daemon_background(socket_path: PathBuf) {
use nix::unistd::{ForkResult, fork, setsid};
match unsafe { fork() } {
Ok(ForkResult::Parent { .. }) => {
if wait_for_daemon(&socket_path, 5) {
println!("Daemon started successfully");
println!("Socket: {}", socket_path.display());
} else {
eprintln!("Warning: Daemon may not have started properly");
exit(1);
}
return;
}
Ok(ForkResult::Child) => {}
Err(e) => {
eprintln!("Fork failed: {}", e);
exit(1);
}
}
if setsid().is_err() {
eprintln!("setsid failed");
exit(1);
}
match unsafe { fork() } {
Ok(ForkResult::Parent { .. }) => {
exit(0);
}
Ok(ForkResult::Child) => {}
Err(e) => {
eprintln!("Second fork failed: {}", e);
exit(1);
}
}
use std::fs::OpenOptions;
use std::os::unix::io::AsRawFd;
if let Ok(devnull) = OpenOptions::new().read(true).write(true).open("/dev/null") {
let fd = devnull.as_raw_fd();
unsafe {
libc::dup2(fd, 0);
libc::dup2(fd, 1);
libc::dup2(fd, 2);
}
}
start_daemon_foreground(socket_path);
}
fn stop_daemon(socket_path: &PathBuf) {
if !socket_path.exists() {
println!("Daemon is not running");
return;
}
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let client = SocketClient::new(socket_path.clone());
client.send_script("// shutdown").await
});
if let Err(e) = std::fs::remove_file(socket_path) {
if e.kind() != std::io::ErrorKind::NotFound {
eprintln!("Warning: Could not remove socket file: {}", e);
}
}
match result {
Ok(_) => println!("Daemon stopped"),
Err(_) => println!("Daemon stopped (or was not running)"),
}
}
fn run_script_via_daemon(socket_path: &PathBuf, script: &str) {
if !is_daemon_running(socket_path) {
eprintln!("Daemon not running, starting...");
let exe = env::current_exe().expect("Failed to get current executable");
let status = Command::new(&exe)
.args(["start", "-bg", "-s", &socket_path.to_string_lossy()])
.status();
match status {
Ok(s) if s.success() => {}
_ => {
eprintln!("Failed to start daemon, running locally instead");
run_script_local(script);
return;
}
}
if !wait_for_daemon(socket_path, 5) {
eprintln!("Daemon failed to start, running locally");
run_script_local(script);
return;
}
}
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
let result = rt.block_on(async {
let client = SocketClient::new(socket_path.clone());
client.send_script_streaming(script).await
});
if let Err(e) = result {
eprintln!("Error: {}", e);
exit(1);
}
}
fn run_script_local(script: &str) {
match ForgeEngine::new() {
Ok(engine) => match engine.run(script) {
Ok(()) => {
let output = heroforge_core::rhai_api::engine::take_output_buffer();
if !output.is_empty() {
print!("{}", output);
}
}
Err(e) => {
eprintln!("{}: {}", "Script error".red().bold(), e);
exit(1);
}
},
Err(e) => {
eprintln!("{}: {}", "Failed to create engine".red().bold(), e);
exit(1);
}
}
}
fn load_script_repl(
file_path: &str,
engine: &rhai::Engine,
scope: &mut rhai::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 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_repl() -> Result<(), Box<dyn std::error::Error>> {
print_banner();
let forge_engine = ForgeEngine::new()?;
let engine = forge_engine.engine();
let mut scope = rhai::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(".heroforge_core_history"))
.unwrap_or_else(|| PathBuf::from(".heroforge_core_history"));
let _ = rl.load_history(&history_path);
let mut multiline_buffer = String::new();
loop {
let prompt = if multiline_buffer.is_empty() {
"forge> "
} 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;
}
"/repo" => {
print_category("Repository Functions", REPO_FUNCTIONS);
continue;
}
"/fs" => {
print_category("Filesystem Functions", FS_FUNCTIONS);
continue;
}
"/files" => {
print_category("File Functions", FILE_FUNCTIONS);
continue;
}
"/branches" => {
print_category("Branch/Tag Functions", BRANCH_FUNCTIONS);
continue;
}
"/modify" => {
print_category("Modify Builder Functions", MODIFY_FUNCTIONS);
continue;
}
"/find" => {
print_category("Find Builder Functions", FIND_FUNCTIONS);
continue;
}
"/utils" => {
print_category("Utility Functions", UTILITY_FUNCTIONS);
continue;
}
"/scope" => {
print_scope(&scope);
continue;
}
"/clear" => {
print!("\x1B[2J\x1B[1;1H");
print_banner();
continue;
}
"/quit" | "/exit" | "/q" => {
println!("{}", "Goodbye!".cyan());
break;
}
_ if line.starts_with("/load ") => {
let file_path = line.strip_prefix("/load ").unwrap().trim();
match load_script_repl(file_path, engine, &mut scope) {
Ok(_) => println!("{}", "Script executed successfully.".green()),
Err(e) => println!("{}: {}", "Error".red().bold(), e),
}
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) => {
let output = heroforge_core::rhai_api::engine::take_output_buffer();
if !output.is_empty() {
print!("{}", output);
}
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 run_script_file(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let script = fs::read_to_string(path)?;
run_script_local(&script);
Ok(())
}
fn run_from_stdin() -> Result<(), Box<dyn std::error::Error>> {
let stdin = io::stdin();
let script: String = stdin
.lock()
.lines()
.filter_map(|l| l.ok())
.collect::<Vec<_>>()
.join("\n");
run_script_local(&script);
Ok(())
}
fn print_usage() {
println!(
"{}",
"heroforge-core - Rhai scripting for Fossil repositories"
.cyan()
.bold()
);
println!();
println!("{}", "USAGE:".yellow().bold());
println!(" heroforge-core Start interactive REPL");
println!(" heroforge-core <script.rhai> Run a script file");
println!(" heroforge-core --ui Start interactive REPL");
println!(" heroforge-core -i Read script from stdin");
println!(" heroforge-core -e <script> Execute inline script");
println!(" heroforge-core --local <script> Run script locally (no daemon)");
println!();
println!("{}", "DAEMON COMMANDS:".yellow().bold());
println!(" heroforge-core start Start daemon in foreground");
println!(" heroforge-core start -bg Start daemon in background");
println!(" heroforge-core stop Stop the daemon");
println!(" heroforge-core status Check daemon status");
println!();
println!("{}", "OPTIONS:".yellow().bold());
println!(" -s <path> Custom socket path");
println!(" --help Show this help");
println!(" --version Show version");
println!();
println!("{}", "EXAMPLES:".yellow().bold());
println!(" {}", "heroforge-core --ui".dimmed());
println!(
" {}",
"heroforge-core -e 'let repo = repo_open(\"my.fossil\"); print(branches_list(repo));'"
.dimmed()
);
println!(
" {}",
"echo 'print(version())' | heroforge-core -i".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;
}
let socket_path = get_socket_path(&args);
let run_local = args.contains(&"--local".to_string());
match args[1].as_str() {
"-h" | "--help" | "help" => {
print_usage();
}
"-V" | "--version" | "version" => {
println!("heroforge-core {}", env!("CARGO_PKG_VERSION"));
}
"--ui" | "--repl" => {
if let Err(e) = run_repl() {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
}
"-i" | "--stdin" => {
if let Err(e) = run_from_stdin() {
eprintln!("{}: {}", "Error".red().bold(), e);
std::process::exit(1);
}
}
"-e" => {
if args.len() < 3 {
eprintln!("{}: -e requires a script argument", "Error".red().bold());
std::process::exit(1);
}
let script = &args[2];
if run_local {
run_script_local(script);
} else {
run_script_via_daemon(&socket_path, script);
}
}
"start" => {
if is_daemon_running(&socket_path) {
println!("Daemon is already running");
return;
}
let background = args.contains(&"-bg".to_string());
if background {
start_daemon_background(socket_path);
} else {
start_daemon_foreground(socket_path);
}
}
"stop" => {
stop_daemon(&socket_path);
}
"status" => {
if is_daemon_running(&socket_path) {
println!("Daemon is running");
println!("Socket: {}", socket_path.display());
} else {
println!("Daemon is not running");
std::process::exit(1);
}
}
"--local" => {
if args.len() < 3 {
eprintln!(
"{}: --local requires a script file or -e <script>",
"Error".red().bold()
);
std::process::exit(1);
}
match args[2].as_str() {
"-e" => {
if args.len() < 4 {
eprintln!("{}: -e requires a script argument", "Error".red().bold());
std::process::exit(1);
}
run_script_local(&args[3]);
}
path => {
let script_path = PathBuf::from(path);
if script_path.exists() {
if run_script_file(&script_path).is_err() {
std::process::exit(1);
}
} else {
eprintln!("{}: File not found: {}", "Error".red().bold(), path);
std::process::exit(1);
}
}
}
}
"-s" => {
if args.len() < 4 {
eprintln!("{}: -s requires a path argument", "Error".red().bold());
std::process::exit(1);
}
print_usage();
}
arg => {
if arg.starts_with('-') {
eprintln!("{}: Unknown option '{}'", "Error".red().bold(), arg);
print_usage();
std::process::exit(1);
}
let script_path = PathBuf::from(arg);
if script_path.exists() {
if run_local {
if run_script_file(&script_path).is_err() {
std::process::exit(1);
}
} else {
let script = fs::read_to_string(&script_path).unwrap_or_else(|e| {
eprintln!("{}: {}", "Error reading file".red().bold(), e);
std::process::exit(1);
});
run_script_via_daemon(&socket_path, &script);
}
} else {
eprintln!("{}: File not found: {}", "Error".red().bold(), arg);
println!();
print_usage();
std::process::exit(1);
}
}
}
}