pluginlab/
lib.rs

1pub(crate) mod api;
2pub(crate) mod cli;
3pub(crate) mod engine;
4pub(crate) mod helpers;
5pub(crate) mod permissions;
6pub(crate) mod store;
7pub(crate) mod wasm_host;
8
9pub(crate) use engine::WasmEngine;
10pub(crate) use wasm_host::WasmHost;
11
12use anyhow::Result;
13use api::host_api::repl::api::transport;
14use clap::Parser;
15use cli::{Cli, Commands};
16use helpers::{StatusHandler, StdoutHandler};
17use std::io::Write;
18
19/// Main entry point for the REPL application
20pub async fn run_async() -> Result<()> {
21    // Parse command line arguments
22    let cli = Cli::parse();
23
24    // Handle subcommands first
25    if let Some(command) = &cli.command {
26        match command {
27            Commands::GenerateCompletions { shell } => {
28                return handle_generate_completions(*shell);
29            }
30        }
31    }
32
33    // For REPL mode, repl_logic is required
34    let repl_logic = cli
35        .repl_logic
36        .ok_or_else(|| anyhow::anyhow!("--repl-logic is required when running in REPL mode"))?;
37
38    let debug = cli.debug;
39    let plugins = cli.plugins;
40    let dir = cli.dir;
41    let allow_net = cli.allow_net;
42    let allow_read = cli.allow_read;
43    let allow_write = cli.allow_write;
44    let allow_all = cli.allow_all;
45
46    // Create a new CLI struct for the remaining operations
47    let repl_cli = Cli {
48        command: None,
49        plugins,
50        repl_logic: Some(repl_logic.clone()),
51        debug,
52        dir,
53        allow_net,
54        allow_read,
55        allow_write,
56        allow_all,
57    };
58
59    println!("[Host] Starting REPL host...");
60
61    // Create a WASI context for the host
62    // Binding stdio, args, env, preopened dir ...
63    let wasi_ctx = WasmEngine::build_wasi_ctx(&repl_cli)?;
64
65    // Create the WebAssembly engine
66    let engine = WasmEngine::new()?;
67
68    // Create the host
69    let mut host = WasmHost::new(&engine, wasi_ctx, &repl_cli);
70
71    println!("[Host] Loading REPL logic from: {}", repl_logic);
72    // Override the REPL logic in the binary with the one passed by params
73    host.load_repl_logic(&engine, &repl_logic).await?;
74
75    // Load plugins
76    for plugin_source in &repl_cli.plugins {
77        println!("[Host] Loading plugin: {}", plugin_source);
78        host.load_plugin(&engine, plugin_source).await?;
79    }
80
81    let mut plugins_config: Vec<(String, String)> = Vec::new();
82    for (name, plugin_instance) in &host.plugins {
83        let man = plugin_instance
84            .plugin
85            .repl_api_plugin()
86            .call_man(&mut host.store)
87            .await?;
88        plugins_config.push((name.clone(), man));
89        host.store.data_mut().plugins_names.push(name.clone());
90    }
91    if debug {
92        eprintln!("[Host][Debug] Loaded plugins config: {:?}", plugins_config);
93    }
94
95    {
96        let mut repl_vars = host
97            .store
98            .data_mut()
99            .repl_vars
100            .lock()
101            .expect("Failed to acquire repl_vars lock");
102        repl_vars.insert("ROOT".to_string(), "/Users".to_string());
103    }
104    {
105        let mut repl_vars = host
106            .store
107            .data_mut()
108            .repl_vars
109            .lock()
110            .expect("Failed to acquire repl_vars lock");
111        repl_vars.insert("USER".to_string(), "Tophe".to_string());
112        repl_vars.insert("?".to_string(), "0".to_string());
113    }
114    if debug {
115        eprintln!(
116            "[Host][Debug] Loaded env vars: {:?}",
117            host.store
118                .data()
119                .repl_vars
120                .lock()
121                .expect("Failed to acquire repl_vars lock")
122        );
123    }
124
125    let Some(repl_logic) = host.repl_logic else {
126        return Err(anyhow::anyhow!("No REPL logic loaded"));
127    };
128
129    loop {
130        let mut line = String::new();
131        match host
132            .store
133            .data()
134            .repl_vars
135            .lock()
136            .expect("Failed to acquire repl_vars lock")
137            .get("?")
138        {
139            Some(last_status) => {
140                print!("repl({})> ", last_status);
141            }
142            None => {
143                print!("repl> ");
144            }
145        }
146        std::io::stdout().flush()?;
147        std::io::stdin().read_line(&mut line)?;
148        let result = repl_logic
149            .repl_api_repl_logic()
150            .call_readline(&mut host.store, &line)
151            .await?;
152        // todo retrieve list of reserved commands from the repl-logic guest
153
154        match result {
155            // The built-in commands run in the repl-logic-guest contain stdout, stderr and a status
156            // We only need to output them and set the $? variable
157            transport::ReadlineResponse::Ready(plugin_response) => {
158                if let Some(stdout) = plugin_response.stdout {
159                    StdoutHandler::print_and_set_last_result(
160                        &mut host.store.data_mut().repl_vars,
161                        stdout,
162                    );
163                }
164                if let Some(stderr) = plugin_response.stderr {
165                    eprintln!("{}", stderr);
166                }
167                StatusHandler::set_exit_status(
168                    &mut host.store.data_mut().repl_vars,
169                    plugin_response.status
170                        == api::host_api::repl::api::transport::ReplStatus::Success,
171                );
172            }
173            // The repl-logic-guest parses the command and payload (expanded variables)
174            // We run the command of the plugin from the host, which has access to
175            // - the plugins
176            // - the store
177            transport::ReadlineResponse::ToRun(parsed_line) => {
178                if debug {
179                    eprintln!("[Host][Debug] To run: {:?}", parsed_line);
180                }
181
182                // empty line - do nothing
183                if parsed_line.command == "" {
184                    continue;
185                }
186
187                // this is a man command for plugins, we run it from the host
188                if parsed_line.command == "man" {
189                    let Some(plugin_instance) = host.plugins.get(&parsed_line.payload) else {
190                        println!(
191                            "Unknown command: {}. Try `help` to see available commands.",
192                            parsed_line.payload
193                        );
194                        StatusHandler::set_exit_status(&mut host.store.data_mut().repl_vars, false);
195                        continue;
196                    };
197                    let man = plugin_instance
198                        .plugin
199                        .repl_api_plugin()
200                        .call_man(&mut host.store)
201                        .await?;
202                    StdoutHandler::print_and_set_last_result(
203                        &mut host.store.data_mut().repl_vars,
204                        man,
205                    );
206                    StatusHandler::set_exit_status(&mut host.store.data_mut().repl_vars, true);
207                    continue;
208                }
209
210                // this is a plugin command, we run it from the host
211                match host.plugins.get(&parsed_line.command) {
212                    Some(plugin_instance) => {
213                        let result = plugin_instance
214                            .plugin
215                            .repl_api_plugin()
216                            .call_run(&mut host.store, &parsed_line.payload)
217                            .await?;
218                        if let Ok(result) = result {
219                            if let Some(stdout) = result.stdout {
220                                StdoutHandler::print_and_set_last_result(
221                                    &mut host.store.data_mut().repl_vars,
222                                    stdout,
223                                );
224                            }
225                            if let Some(stderr) = result.stderr {
226                                eprintln!("{}", stderr);
227                            }
228                            StatusHandler::set_exit_status(
229                                &mut host.store.data_mut().repl_vars,
230                                result.status
231                                    == api::plugin_api::repl::api::transport::ReplStatus::Success,
232                            );
233                        } else {
234                            eprintln!("Error: {:?}", result);
235                            StatusHandler::set_exit_status(
236                                &mut host.store.data_mut().repl_vars,
237                                false,
238                            );
239                        }
240                    }
241                    None => {
242                        println!(
243                            "Unknown command: {}. Try `help` to see available commands.",
244                            parsed_line.command
245                        );
246                        StatusHandler::set_exit_status(&mut host.store.data_mut().repl_vars, false);
247                        continue;
248                    }
249                }
250            }
251        }
252    }
253}
254
255/// Handle the generate-completions subcommand
256fn handle_generate_completions(shell: cli::AvailableShells) -> Result<()> {
257    use clap::CommandFactory;
258    use clap_complete::{generate, Shell};
259    use cli::Cli;
260
261    let mut cmd = Cli::command();
262    let shell_type = match shell {
263        cli::AvailableShells::Bash => Shell::Bash,
264        cli::AvailableShells::Fish => Shell::Fish,
265        cli::AvailableShells::Zsh => Shell::Zsh,
266    };
267
268    generate(shell_type, &mut cmd, "pluginlab", &mut std::io::stdout());
269
270    Ok(())
271}