Skip to main content

harn_vm/stdlib/
process.rs

1use std::cell::RefCell;
2use std::collections::BTreeMap;
3use std::path::PathBuf;
4use std::rc::Rc;
5
6use crate::value::{VmError, VmValue};
7use crate::vm::Vm;
8
9thread_local! {
10    static VM_SOURCE_DIR: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
11}
12
13/// Set the source directory for the current thread (called by VM on file execution).
14pub(crate) fn set_thread_source_dir(dir: &std::path::Path) {
15    VM_SOURCE_DIR.with(|sd| *sd.borrow_mut() = Some(dir.to_path_buf()));
16}
17
18/// Reset thread-local process state (for test isolation).
19pub(crate) fn reset_process_state() {
20    VM_SOURCE_DIR.with(|sd| *sd.borrow_mut() = None);
21}
22
23pub(crate) fn register_process_builtins(vm: &mut Vm) {
24    vm.register_builtin("env", |args, _out| {
25        let name = args.first().map(|a| a.display()).unwrap_or_default();
26        match std::env::var(&name) {
27            Ok(val) => Ok(VmValue::String(Rc::from(val))),
28            Err(_) => Ok(VmValue::Nil),
29        }
30    });
31
32    vm.register_builtin("timestamp", |_args, _out| {
33        use std::time::{SystemTime, UNIX_EPOCH};
34        let secs = SystemTime::now()
35            .duration_since(UNIX_EPOCH)
36            .map(|d| d.as_secs_f64())
37            .unwrap_or(0.0);
38        Ok(VmValue::Float(secs))
39    });
40
41    vm.register_builtin("exit", |args, _out| {
42        let code = args.first().and_then(|a| a.as_int()).unwrap_or(0);
43        std::process::exit(code as i32);
44    });
45
46    vm.register_builtin("exec", |args, _out| {
47        if args.is_empty() {
48            return Err(VmError::Thrown(VmValue::String(Rc::from(
49                "exec: command is required",
50            ))));
51        }
52        let cmd = args[0].display();
53        let cmd_args: Vec<String> = args[1..].iter().map(|a| a.display()).collect();
54        let output = std::process::Command::new(&cmd)
55            .args(&cmd_args)
56            .output()
57            .map_err(|e| VmError::Thrown(VmValue::String(Rc::from(format!("exec failed: {e}")))))?;
58        Ok(vm_output_to_value(output))
59    });
60
61    vm.register_builtin("shell", |args, _out| {
62        let cmd = args.first().map(|a| a.display()).unwrap_or_default();
63        if cmd.is_empty() {
64            return Err(VmError::Thrown(VmValue::String(Rc::from(
65                "shell: command string is required",
66            ))));
67        }
68        let shell = if cfg!(target_os = "windows") {
69            "cmd"
70        } else {
71            "sh"
72        };
73        let flag = if cfg!(target_os = "windows") {
74            "/C"
75        } else {
76            "-c"
77        };
78        let output = std::process::Command::new(shell)
79            .arg(flag)
80            .arg(&cmd)
81            .output()
82            .map_err(|e| {
83                VmError::Thrown(VmValue::String(Rc::from(format!("shell failed: {e}"))))
84            })?;
85        Ok(vm_output_to_value(output))
86    });
87
88    vm.register_builtin("elapsed", |_args, _out| {
89        static START: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
90        let start = START.get_or_init(std::time::Instant::now);
91        Ok(VmValue::Int(start.elapsed().as_millis() as i64))
92    });
93
94    // --- System attributes for prompt building ---
95
96    vm.register_builtin("username", |_args, _out| {
97        let user = std::env::var("USER")
98            .or_else(|_| std::env::var("USERNAME"))
99            .unwrap_or_default();
100        Ok(VmValue::String(Rc::from(user)))
101    });
102
103    vm.register_builtin("hostname", |_args, _out| {
104        let name = std::env::var("HOSTNAME")
105            .or_else(|_| std::env::var("COMPUTERNAME"))
106            .or_else(|_| {
107                std::process::Command::new("hostname")
108                    .output()
109                    .ok()
110                    .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
111                    .ok_or(std::env::VarError::NotPresent)
112            })
113            .unwrap_or_default();
114        Ok(VmValue::String(Rc::from(name)))
115    });
116
117    vm.register_builtin("platform", |_args, _out| {
118        let os = if cfg!(target_os = "macos") {
119            "darwin"
120        } else if cfg!(target_os = "linux") {
121            "linux"
122        } else if cfg!(target_os = "windows") {
123            "windows"
124        } else {
125            std::env::consts::OS
126        };
127        Ok(VmValue::String(Rc::from(os)))
128    });
129
130    vm.register_builtin("arch", |_args, _out| {
131        Ok(VmValue::String(Rc::from(std::env::consts::ARCH)))
132    });
133
134    vm.register_builtin("home_dir", |_args, _out| {
135        let home = std::env::var("HOME")
136            .or_else(|_| std::env::var("USERPROFILE"))
137            .unwrap_or_default();
138        Ok(VmValue::String(Rc::from(home)))
139    });
140
141    vm.register_builtin("pid", |_args, _out| {
142        Ok(VmValue::Int(std::process::id() as i64))
143    });
144
145    // --- Path / directory introspection ---
146
147    vm.register_builtin("date_iso", |_args, _out| {
148        use crate::stdlib::datetime::vm_civil_from_timestamp;
149        let now = std::time::SystemTime::now()
150            .duration_since(std::time::UNIX_EPOCH)
151            .unwrap_or_default();
152        let total_secs = now.as_secs();
153        let millis = now.subsec_millis();
154        let (y, m, d, hour, minute, second, _) = vm_civil_from_timestamp(total_secs);
155        Ok(VmValue::String(Rc::from(format!(
156            "{y:04}-{m:02}-{d:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z"
157        ))))
158    });
159
160    vm.register_builtin("cwd", |_args, _out| {
161        let dir = std::env::current_dir()
162            .map(|p| p.to_string_lossy().to_string())
163            .unwrap_or_default();
164        Ok(VmValue::String(Rc::from(dir)))
165    });
166}
167
168/// Find the project root by walking up from a base directory looking for harn.toml.
169pub fn find_project_root(base: &std::path::Path) -> Option<std::path::PathBuf> {
170    let mut dir = base.to_path_buf();
171    loop {
172        if dir.join("harn.toml").exists() {
173            return Some(dir);
174        }
175        if !dir.pop() {
176            return None;
177        }
178    }
179}
180
181/// Register builtins that depend on source directory context.
182pub(crate) fn register_path_builtins(vm: &mut Vm) {
183    vm.register_builtin("source_dir", |_args, _out| {
184        let dir = VM_SOURCE_DIR.with(|sd| sd.borrow().clone());
185        match dir {
186            Some(d) => Ok(VmValue::String(Rc::from(d.to_string_lossy().to_string()))),
187            None => {
188                let cwd = std::env::current_dir()
189                    .map(|p| p.to_string_lossy().to_string())
190                    .unwrap_or_default();
191                Ok(VmValue::String(Rc::from(cwd)))
192            }
193        }
194    });
195
196    vm.register_builtin("project_root", |_args, _out| {
197        let base = VM_SOURCE_DIR
198            .with(|sd| sd.borrow().clone())
199            .or_else(|| std::env::current_dir().ok())
200            .unwrap_or_else(|| PathBuf::from("."));
201        match find_project_root(&base) {
202            Some(root) => Ok(VmValue::String(Rc::from(
203                root.to_string_lossy().to_string(),
204            ))),
205            None => Ok(VmValue::Nil),
206        }
207    });
208}
209
210fn vm_output_to_value(output: std::process::Output) -> VmValue {
211    let mut result = BTreeMap::new();
212    result.insert(
213        "stdout".to_string(),
214        VmValue::String(Rc::from(
215            String::from_utf8_lossy(&output.stdout).to_string().as_str(),
216        )),
217    );
218    result.insert(
219        "stderr".to_string(),
220        VmValue::String(Rc::from(
221            String::from_utf8_lossy(&output.stderr).to_string().as_str(),
222        )),
223    );
224    result.insert(
225        "status".to_string(),
226        VmValue::Int(output.status.code().unwrap_or(-1) as i64),
227    );
228    result.insert(
229        "success".to_string(),
230        VmValue::Bool(output.status.success()),
231    );
232    VmValue::Dict(Rc::new(result))
233}