http_nu/
engine.rs

1use std::path::Path;
2use std::sync::{atomic::AtomicBool, Arc};
3
4use tokio_util::sync::CancellationToken;
5
6use nu_cli::{add_cli_context, gather_parent_env_vars};
7use nu_cmd_lang::create_default_context;
8use nu_command::add_shell_command_context;
9use nu_engine::eval_block_with_early_return;
10use nu_parser::parse;
11use nu_plugin_engine::{GetPlugin, PluginDeclaration};
12use nu_protocol::engine::Command;
13use nu_protocol::format_cli_error;
14use nu_protocol::{
15    debugger::WithoutDebug,
16    engine::{Closure, EngineState, Redirection, Stack, StateWorkingSet},
17    OutDest, PipelineData, PluginIdentity, RegisteredPlugin, ShellError, Signals, Span, Type,
18    Value,
19};
20
21use crate::commands::{
22    HighlightCommand, HighlightLangCommand, HighlightThemeCommand, MdCommand, MjCommand,
23    MjCompileCommand, MjRenderCommand, PrintCommand, ReverseProxyCommand, StaticCommand, ToSse,
24};
25use crate::logging::log_error;
26use crate::stdlib::load_http_nu_stdlib;
27use crate::Error;
28
29#[derive(Clone)]
30pub struct Engine {
31    pub state: EngineState,
32    pub closure: Option<Closure>,
33    /// Cancellation token triggered on engine reload
34    pub reload_token: CancellationToken,
35}
36
37impl Engine {
38    pub fn new() -> Result<Self, Error> {
39        let mut engine_state = create_default_context();
40
41        engine_state = add_shell_command_context(engine_state);
42        engine_state = add_cli_context(engine_state);
43        engine_state = nu_cmd_extra::extra::add_extra_command_context(engine_state);
44
45        load_http_nu_stdlib(&mut engine_state)?;
46
47        let init_cwd = std::env::current_dir()?;
48        gather_parent_env_vars(&mut engine_state, init_cwd.as_ref());
49
50        Ok(Self {
51            state: engine_state,
52            closure: None,
53            reload_token: CancellationToken::new(),
54        })
55    }
56
57    pub fn add_commands(&mut self, commands: Vec<Box<dyn Command>>) -> Result<(), Error> {
58        let mut working_set = StateWorkingSet::new(&self.state);
59        for command in commands {
60            working_set.add_decl(command);
61        }
62        self.state.merge_delta(working_set.render())?;
63        Ok(())
64    }
65
66    /// Load a Nushell plugin from the given path
67    pub fn load_plugin(&mut self, path: &Path) -> Result<(), Error> {
68        // Canonicalize the path
69        let path = path.canonicalize().map_err(|e| {
70            Error::from(format!("Failed to canonicalize plugin path {path:?}: {e}"))
71        })?;
72
73        // Create the plugin identity
74        let identity = PluginIdentity::new(&path, None).map_err(|_| {
75            Error::from(format!(
76                "Invalid plugin path {path:?}: must be named nu_plugin_*"
77            ))
78        })?;
79
80        let mut working_set = StateWorkingSet::new(&self.state);
81
82        // Add plugin to working set and get handle
83        let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
84
85        // Merge working set to make plugin available
86        self.state.merge_delta(working_set.render())?;
87
88        // Spawn the plugin to get its signatures
89        let interface = plugin.clone().get_plugin(None)?;
90
91        // Set plugin metadata
92        plugin.set_metadata(Some(interface.get_metadata()?));
93
94        // Add command declarations from plugin signatures
95        let mut working_set = StateWorkingSet::new(&self.state);
96        for signature in interface.get_signature()? {
97            let decl = PluginDeclaration::new(plugin.clone(), signature);
98            working_set.add_decl(Box::new(decl));
99        }
100        self.state.merge_delta(working_set.render())?;
101
102        Ok(())
103    }
104
105    pub fn parse_closure(&mut self, script: &str) -> Result<(), Error> {
106        let mut working_set = StateWorkingSet::new(&self.state);
107        let block = parse(&mut working_set, None, script.as_bytes(), false);
108
109        // Handle parse errors
110        if let Some(err) = working_set.parse_errors.first() {
111            let shell_error = ShellError::GenericError {
112                error: "Parse error".into(),
113                msg: format!("{err:?}"),
114                span: Some(err.span()),
115                help: None,
116                inner: vec![],
117            };
118            return Err(Error::from(format_cli_error(
119                None,
120                &working_set,
121                &shell_error,
122                None,
123            )));
124        }
125
126        // Handle compile errors
127        if let Some(err) = working_set.compile_errors.first() {
128            let shell_error = ShellError::GenericError {
129                error: format!("Compile error {err}"),
130                msg: "".into(),
131                span: None,
132                help: None,
133                inner: vec![],
134            };
135            return Err(Error::from(format_cli_error(
136                None,
137                &working_set,
138                &shell_error,
139                None,
140            )));
141        }
142
143        self.state.merge_delta(working_set.render())?;
144
145        let mut stack = Stack::new();
146        let result = eval_block_with_early_return::<WithoutDebug>(
147            &self.state,
148            &mut stack,
149            &block,
150            PipelineData::empty(),
151        )
152        .map_err(|err| {
153            let working_set = StateWorkingSet::new(&self.state);
154            Error::from(format_cli_error(None, &working_set, &err, None))
155        })?;
156
157        let closure = result
158            .body
159            .into_value(Span::unknown())
160            .map_err(|err| {
161                let working_set = StateWorkingSet::new(&self.state);
162                Error::from(format_cli_error(None, &working_set, &err, None))
163            })?
164            .into_closure()
165            .map_err(|err| {
166                let working_set = StateWorkingSet::new(&self.state);
167                Error::from(format_cli_error(None, &working_set, &err, None))
168            })?;
169
170        // Verify closure accepts exactly one argument
171        let block = self.state.get_block(closure.block_id);
172        if block.signature.required_positional.len() != 1 {
173            return Err(format!(
174                "Closure must accept exactly one request argument, found {}",
175                block.signature.required_positional.len()
176            )
177            .into());
178        }
179
180        self.state.merge_env(&mut stack)?;
181
182        self.closure = Some(closure);
183        Ok(())
184    }
185
186    /// Sets the interrupt signal for the engine
187    pub fn set_signals(&mut self, interrupt: Arc<AtomicBool>) {
188        self.state.set_signals(Signals::new(interrupt));
189    }
190
191    /// Sets NU_LIB_DIRS const for module resolution
192    pub fn set_lib_dirs(&mut self, paths: &[std::path::PathBuf]) -> Result<(), Error> {
193        if paths.is_empty() {
194            return Ok(());
195        }
196        let span = Span::unknown();
197        let vals: Vec<Value> = paths
198            .iter()
199            .map(|p| Value::string(p.to_string_lossy(), span))
200            .collect();
201
202        let mut working_set = StateWorkingSet::new(&self.state);
203        let var_id = working_set.add_variable(
204            b"$NU_LIB_DIRS".into(),
205            span,
206            Type::List(Box::new(Type::String)),
207            false,
208        );
209        working_set.set_variable_const_val(var_id, Value::list(vals, span));
210        self.state.merge_delta(working_set.render())?;
211        Ok(())
212    }
213
214    /// Evaluate a script string and return the result value
215    pub fn eval(&self, script: &str) -> Result<Value, Error> {
216        let mut working_set = StateWorkingSet::new(&self.state);
217        let block = parse(&mut working_set, None, script.as_bytes(), false);
218
219        if let Some(err) = working_set.parse_errors.first() {
220            let shell_error = ShellError::GenericError {
221                error: "Parse error".into(),
222                msg: format!("{err:?}"),
223                span: Some(err.span()),
224                help: None,
225                inner: vec![],
226            };
227            return Err(Error::from(format_cli_error(
228                None,
229                &working_set,
230                &shell_error,
231                None,
232            )));
233        }
234
235        if let Some(err) = working_set.compile_errors.first() {
236            let shell_error = ShellError::GenericError {
237                error: format!("Compile error {err}"),
238                msg: "".into(),
239                span: None,
240                help: None,
241                inner: vec![],
242            };
243            return Err(Error::from(format_cli_error(
244                None,
245                &working_set,
246                &shell_error,
247                None,
248            )));
249        }
250
251        // Clone engine state and merge the parsed block
252        let mut engine_state = self.state.clone();
253        engine_state.merge_delta(working_set.render())?;
254
255        let mut stack = Stack::new();
256        let result = eval_block_with_early_return::<WithoutDebug>(
257            &engine_state,
258            &mut stack,
259            &block,
260            PipelineData::empty(),
261        )
262        .map_err(|err| {
263            let working_set = StateWorkingSet::new(&engine_state);
264            Error::from(format_cli_error(None, &working_set, &err, None))
265        })?;
266
267        result.body.into_value(Span::unknown()).map_err(|err| {
268            let working_set = StateWorkingSet::new(&engine_state);
269            Error::from(format_cli_error(None, &working_set, &err, None))
270        })
271    }
272
273    /// Run the parsed closure with input value and pipeline data
274    pub fn run_closure(
275        &self,
276        input: Value,
277        pipeline_data: PipelineData,
278    ) -> Result<PipelineData, Error> {
279        let closure = self.closure.as_ref().ok_or("Closure not parsed")?;
280
281        let mut stack = Stack::new().captures_to_stack(closure.captures.clone());
282        let mut stack =
283            stack.push_redirection(Some(Redirection::Pipe(OutDest::PipeSeparate)), None);
284        let block = self.state.get_block(closure.block_id);
285
286        stack.add_var(
287            block.signature.required_positional[0].var_id.unwrap(),
288            input,
289        );
290
291        eval_block_with_early_return::<WithoutDebug>(&self.state, &mut stack, block, pipeline_data)
292            .map(|exec_data| exec_data.body)
293            .map_err(|err| {
294                let working_set = StateWorkingSet::new(&self.state);
295                Error::from(format_cli_error(None, &working_set, &err, None))
296            })
297    }
298
299    /// Adds http-nu custom commands to the engine
300    pub fn add_custom_commands(&mut self) -> Result<(), Error> {
301        self.add_commands(vec![
302            Box::new(ReverseProxyCommand::new()),
303            Box::new(StaticCommand::new()),
304            Box::new(ToSse {}),
305            Box::new(MjCommand::new()),
306            Box::new(MjCompileCommand::new()),
307            Box::new(MjRenderCommand::new()),
308            Box::new(HighlightCommand::new()),
309            Box::new(HighlightThemeCommand::new()),
310            Box::new(HighlightLangCommand::new()),
311            Box::new(MdCommand::new()),
312            Box::new(PrintCommand::new()),
313        ])
314    }
315
316    /// Adds cross.stream store commands (.cat, .append, .cas, .last) to the engine
317    #[cfg(feature = "cross-stream")]
318    pub fn add_store_commands(&mut self, store: &xs::store::Store) -> Result<(), Error> {
319        self.add_commands(vec![
320            Box::new(xs::nu::commands::cat_stream_command::CatStreamCommand::new(
321                store.clone(),
322            )),
323            Box::new(xs::nu::commands::append_command::AppendCommand::new(
324                store.clone(),
325                serde_json::json!({}),
326            )),
327            Box::new(xs::nu::commands::cas_command::CasCommand::new(
328                store.clone(),
329            )),
330            Box::new(xs::nu::commands::last_stream_command::LastStreamCommand::new(store.clone())),
331            Box::new(xs::nu::commands::get_command::GetCommand::new(
332                store.clone(),
333            )),
334            Box::new(xs::nu::commands::remove_command::RemoveCommand::new(
335                store.clone(),
336            )),
337            Box::new(xs::nu::commands::scru128_command::Scru128Command::new()),
338        ])
339    }
340}
341
342/// Creates an engine from a script by cloning a base engine and parsing the closure.
343/// On error, prints to stderr and emits JSON to stdout, returning None.
344pub fn script_to_engine(base: &Engine, script: &str) -> Option<Engine> {
345    let mut engine = base.clone();
346    // Fresh cancellation token for this engine instance
347    engine.reload_token = CancellationToken::new();
348
349    if let Err(e) = engine.parse_closure(script) {
350        log_error(&nu_utils::strip_ansi_string_likely(e.to_string()));
351        return None;
352    }
353
354    Some(engine)
355}