diff --git a/.cursor/rules/git-commands.md b/.cursor/rules/git-commands.md new file mode 100644 index 0000000..9b6f380 --- /dev/null +++ b/.cursor/rules/git-commands.md @@ -0,0 +1,31 @@ +# Git Commands + +Always use `--no-abbrev-commit` and proper formatting flags for all git terminal commands to avoid shell parsing issues. + +## Specific Commands to Use: + +**Instead of:** +```bash +git log --oneline +git show --stat HEAD +git branch -v +``` + +**Use:** +```bash +git log --pretty=format:"%h %s" --no-abbrev-commit +git show --stat --no-abbrev-commit HEAD +git branch --show-current +``` + +## Key Flags: +- `--no-abbrev-commit` - Prevents abbreviated commit hashes +- `--pretty=format:"..."` - Use explicit formatting +- `--porcelain` - For cleaner output when available +- `--no-color` - Remove ANSI color codes +- `--show-current` - For branch operations + +## Common Patterns: +- `git log master..HEAD` → `git log --pretty=format:"%h %s" --no-abbrev-commit master..HEAD` +- `git status` → `git status --porcelain` +- `git diff --name-only master` → `git diff --name-only master` diff --git a/Cargo.lock b/Cargo.lock index b78fce9..c060217 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1457,6 +1457,13 @@ dependencies = [ "wit-bindgen-rt 0.42.1", ] +[[package]] +name = "plugin-tee" +version = "0.1.0" +dependencies = [ + "wit-bindgen-rt 0.42.1", +] + [[package]] name = "plugin-weather" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 0b7e2e7..3785940 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/plugin-ls", "crates/plugin-cat", "crates/plugin-weather", + "crates/plugin-tee", "crates/repl-logic-guest", ] diff --git a/README.md b/README.md index e4c0334..b830172 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ pluginlab\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_echo.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_weather.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm\ + --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_tee.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin-echo-c.wasm\ --allow-all ``` @@ -106,6 +107,7 @@ pluginlab\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_echo.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_weather.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm\ + --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_tee.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin-echo-c.wasm\ --allow-all [Host] Starting REPL host... @@ -115,6 +117,8 @@ pluginlab\ [Host] Loading plugin: https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_echo.wasm [Host] Loading plugin: https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_weather.wasm [Host] Loading plugin: https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm +[Host] Loading plugin: https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_tee.wasm +[Host] Loading plugin: https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin-echo-c.wasm repl(0)> echo foo foo repl(0)> echo $ROOT/$USER @@ -220,6 +224,7 @@ This will (see [justfile](./justfile)): --plugins ./target/wasm32-wasip1/debug/plugin_echo.wasm\ --plugins ./target/wasm32-wasip1/debug/plugin_weather.wasm\ --plugins ./target/wasm32-wasip1/debug/plugin_cat.wasm\ + --plugins ./target/wasm32-wasip1/debug/plugin_tee.wasm\ --plugins ./c_modules/plugin-echo/plugin-echo-c.wasm\ --allow-all ``` @@ -378,13 +383,14 @@ When a git tag is pushed, a pre-release is prepared on github, linked to the tag ```sh pluginlab\ - --repl-logic https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/repl_logic_guest.wasm\ - --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_greet.wasm\ - --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_ls.wasm\ - --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_echo.wasm\ - --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_weather.wasm\ - --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_cat.wasm\ - --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin-echo-c.wasm\ + --repl-logic https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/repl_logic_guest.wasm\ + --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_greet.wasm\ + --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_ls.wasm\ + --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_echo.wasm\ + --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_weather.wasm\ + --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_cat.wasm\ + --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_tee.wasm\ + --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin-echo-c.wasm\ --allow-all ``` diff --git a/crates/plugin-tee/.gitignore b/crates/plugin-tee/.gitignore new file mode 100644 index 0000000..4c8e258 --- /dev/null +++ b/crates/plugin-tee/.gitignore @@ -0,0 +1 @@ +src/bindings.rs diff --git a/crates/plugin-tee/Cargo.toml b/crates/plugin-tee/Cargo.toml new file mode 100644 index 0000000..30abe51 --- /dev/null +++ b/crates/plugin-tee/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "plugin-tee" +version = "0.1.0" +edition = { workspace = true } +publish = false +description = "Example tee plugin for REPL based on WebAssembly Component Model - demonstrates file system access and modification" + +[dependencies] +wit-bindgen-rt = { workspace = true, features = ["bitflags"] } + +[lib] +crate-type = ["cdylib"] + +[package.metadata.component] +package = "repl:api" +target = { path = "../pluginlab/wit", world = "plugin-api" } + +[package.metadata.component.dependencies] diff --git a/crates/plugin-tee/README.md b/crates/plugin-tee/README.md new file mode 100644 index 0000000..2a68ffa --- /dev/null +++ b/crates/plugin-tee/README.md @@ -0,0 +1,14 @@ +# plugin-tee + +Basic plugin for this REPL. Behaves like the `tee` command. + +## Notes + +This crate was initialized with `cargo component new`. + +The building process is handled by the [`justfile`](../../justfile) in the root of the project. + +The `cargo component build` command is used to build the plugin. + +- It generates the `src/bindings.rs` file, based on the `package.metadata.component` section in the `Cargo.toml` file that describes where to find the component definition (wit files). +- It then compiles the plugin to WebAssembly. diff --git a/crates/plugin-tee/src/lib.rs b/crates/plugin-tee/src/lib.rs new file mode 100644 index 0000000..b0230ac --- /dev/null +++ b/crates/plugin-tee/src/lib.rs @@ -0,0 +1,99 @@ +#[allow(warnings)] +mod bindings; + +use std::io::Write; + +use crate::bindings::exports::repl::api::plugin::Guest; +use crate::bindings::repl::api::host_state_plugin; +use crate::bindings::repl::api::transport; + +struct Component; + +impl Guest for Component { + fn name() -> String { + "tee".to_string() + } + + fn man() -> String { + r#" +NAME + tee - Copy $0 content to a file (built with Rust🦀) + +USAGE + tee + tee -a + +OPTIONS + -a, --append Append to the file instead of overwriting it + +DESCRIPTION + Copy $0 content to a file. + + "# + .to_string() + } + + fn run(payload: String) -> Result { + match run_inner(payload) { + Ok(content) => Ok(transport::PluginResponse { + status: transport::ReplStatus::Success, + stdout: Some(format!("{}", content)), + stderr: None, + }), + Err(e) => { + // e.kind() - verify if the error is a permission error + return Ok(transport::PluginResponse { + status: transport::ReplStatus::Error, + stdout: None, + stderr: Some(format!("{}", e)), + }); + } + } + } +} + +fn run_inner(payload: String) -> Result { + let is_append = payload.starts_with("-a") || payload.starts_with("--append"); + let filepath = if is_append { + let Some((_, filepath)) = payload.split_once(" ") else { + return Err("Invalid arguments. Usage: tee or tee -a ".to_string()); + }; + filepath.to_string() + } else { + payload + }; + + let content = host_state_plugin::get_repl_var("0").unwrap_or("".to_string()); + let content_as_bytes = content.as_bytes(); + + if !is_append { + let mut file = std::fs::File::create(&filepath) + .map_err(|e| enhanced_error(e, format!("Failed to create file '{}'", filepath)))?; + file.write_all(content_as_bytes) + .map_err(|e| enhanced_error(e, format!("Failed to write to file '{}'", filepath)))?; + return Ok(content); + } else { + let mut file = std::fs::File::options() + .append(true) + .open(&filepath) + .map_err(|e| { + enhanced_error( + e, + format!("Failed to open file in append mode '{}'", filepath), + ) + })?; + // Add a newline before the content in append mode + file.write_all(b"\n").map_err(|e| { + enhanced_error(e, format!("Failed to write newline to file '{}'", filepath)) + })?; + file.write_all(content_as_bytes) + .map_err(|e| enhanced_error(e, format!("Failed to write to file '{}'", filepath)))?; + return Ok(content); + } +} + +fn enhanced_error(e: std::io::Error, more_info: String) -> String { + format!("{} - {} - {}", e.kind(), more_info, e.to_string()) +} + +bindings::export!(Component with_types_in bindings); diff --git a/crates/pluginlab/README.md b/crates/pluginlab/README.md index 2000ab9..f9d3a9f 100644 --- a/crates/pluginlab/README.md +++ b/crates/pluginlab/README.md @@ -61,6 +61,7 @@ pluginlab\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_echo.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_weather.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm\ + --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_tee.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin-echo-c.wasm\ --allow-all ``` @@ -85,6 +86,7 @@ pluginlab\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_echo.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_weather.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_cat.wasm\ + --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin_tee.wasm\ --plugins https://topheman.github.io/webassembly-component-model-experiments/plugins/plugin-echo-c.wasm\ --allow-all [Host] Starting REPL host... @@ -133,13 +135,14 @@ The plugins are also versioned in [github releases](https://github.com/topheman/ Example of running the CLI host with old versions of the plugins (if you have an old version of pluginlab
 pluginlab\
-  --repl-logic https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/repl_logic_guest.wasm\
-  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_greet.wasm\
-  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_ls.wasm\
-  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_echo.wasm\
-  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_weather.wasm\
-  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_cat.wasm\
-  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin-echo-c.wasm\
+  --repl-logic https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/repl_logic_guest.wasm\
+  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_greet.wasm\
+  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_ls.wasm\
+  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_echo.wasm\
+  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_weather.wasm\
+  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_cat.wasm\
+  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_tee.wasm\
+  --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin-echo-c.wasm\
   --allow-all
     
@@ -163,7 +166,7 @@ pluginlab\ [Host] You are most likely trying to use a plugin not compatible with pluginlab@0.4.1 [Host] [Host] Try using a compatible version of the plugin by passing the following flag: -[Host] --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.4.1/plugin_echo.wasm +[Host] --plugins https://github.com/topheman/webassembly-component-model-experiments/releases/download/pluginlab@0.5.0/plugin_echo.wasm [Host] [Host] If it doesn't work, make sure to use the latest version of pluginlab: `cargo install pluginlab` [Host] diff --git a/crates/pluginlab/src/engine.rs b/crates/pluginlab/src/engine.rs index 10d2731..3a00671 100644 --- a/crates/pluginlab/src/engine.rs +++ b/crates/pluginlab/src/engine.rs @@ -1,7 +1,7 @@ use crate::cli::Cli; use crate::permissions::NetworkPermissions; use anyhow::Result; -use std::collections::HashMap; + use std::path::Path; use wasmtime::component::{Component, Linker as ComponentLinker, ResourceTable}; use wasmtime::{Config, Engine, Store}; @@ -120,6 +120,9 @@ impl WasmEngine { /// Create a new store with WASI context pub fn create_store(&self, wasi_ctx: WasiCtx, cli: &Cli) -> Store { + let repl_vars = + std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())); + Store::new( &self.engine, WasiState { @@ -127,8 +130,9 @@ impl WasmEngine { table: ResourceTable::new(), plugin_host: PluginHost { network_permissions: NetworkPermissions::from(cli), + repl_vars: repl_vars.clone(), }, - repl_vars: HashMap::new(), + repl_vars, plugins_names: Vec::new(), }, ) diff --git a/crates/pluginlab/src/helpers.rs b/crates/pluginlab/src/helpers.rs index 5fb8ab0..452e626 100644 --- a/crates/pluginlab/src/helpers.rs +++ b/crates/pluginlab/src/helpers.rs @@ -1,22 +1,32 @@ use std::collections::HashMap; +use std::sync::{Arc, Mutex}; /// Handles setting exit status codes in REPL variables pub struct StatusHandler; impl StatusHandler { /// Set the exit status in the REPL variables - pub fn set_exit_status(repl_vars: &mut HashMap, success: bool) { + pub fn set_exit_status(repl_vars: &mut Arc>>, success: bool) { let status = if success { "0" } else { "1" }; - repl_vars.insert("?".to_string(), status.to_string()); + repl_vars + .lock() + .expect("Failed to acquire repl_vars lock") + .insert("?".to_string(), status.to_string()); } } pub struct StdoutHandler; impl StdoutHandler { - pub fn print_and_set_last_result(repl_vars: &mut HashMap, result: String) { + pub fn print_and_set_last_result( + repl_vars: &mut Arc>>, + result: String, + ) { println!("{}", result); - repl_vars.insert("0".to_string(), result); + repl_vars + .lock() + .expect("Failed to acquire repl_vars lock") + .insert("0".to_string(), result); } } diff --git a/crates/pluginlab/src/lib.rs b/crates/pluginlab/src/lib.rs index 045867c..211f2c7 100644 --- a/crates/pluginlab/src/lib.rs +++ b/crates/pluginlab/src/lib.rs @@ -57,22 +57,33 @@ pub async fn run_async() -> Result<()> { eprintln!("[Host][Debug] Loaded plugins config: {:?}", plugins_config); } - host.store - .data_mut() - .repl_vars - .insert("ROOT".to_string(), "/Users".to_string()); - host.store - .data_mut() - .repl_vars - .insert("USER".to_string(), "Tophe".to_string()); - host.store - .data_mut() - .repl_vars - .insert("?".to_string(), "0".to_string()); + { + let mut repl_vars = host + .store + .data_mut() + .repl_vars + .lock() + .expect("Failed to acquire repl_vars lock"); + repl_vars.insert("ROOT".to_string(), "/Users".to_string()); + } + { + let mut repl_vars = host + .store + .data_mut() + .repl_vars + .lock() + .expect("Failed to acquire repl_vars lock"); + repl_vars.insert("USER".to_string(), "Tophe".to_string()); + repl_vars.insert("?".to_string(), "0".to_string()); + } if debug { eprintln!( "[Host][Debug] Loaded env vars: {:?}", - host.store.data().repl_vars + host.store + .data() + .repl_vars + .lock() + .expect("Failed to acquire repl_vars lock") ); } @@ -82,7 +93,14 @@ pub async fn run_async() -> Result<()> { loop { let mut line = String::new(); - match host.store.data().repl_vars.get("?") { + match host + .store + .data() + .repl_vars + .lock() + .expect("Failed to acquire repl_vars lock") + .get("?") + { Some(last_status) => { print!("repl({})> ", last_status); } diff --git a/crates/pluginlab/src/store.rs b/crates/pluginlab/src/store.rs index 366f9d4..5b5996e 100644 --- a/crates/pluginlab/src/store.rs +++ b/crates/pluginlab/src/store.rs @@ -1,5 +1,6 @@ use crate::permissions::NetworkPermissions; use std::collections::HashMap; +use std::sync::{Arc, Mutex}; use wasmtime::component::ResourceTable; use wasmtime_wasi::p2::{IoView, WasiCtx, WasiView}; @@ -30,7 +31,7 @@ pub struct WasiState { /// and shared with the guest via the Guest bindings /// Custom environment variables stored by the REPL - pub repl_vars: HashMap, + pub repl_vars: Arc>>, /// Names of the plugins loaded in the host pub plugins_names: Vec, @@ -40,6 +41,8 @@ pub struct WasiState { pub struct PluginHost { /// Network permissions pub network_permissions: NetworkPermissions, + /// Shared reference to repl_vars from WasiState + pub repl_vars: Arc>>, } impl crate::api::plugin_api::repl::api::http_client::Host for PluginHost { @@ -82,6 +85,17 @@ impl crate::api::plugin_api::repl::api::http_client::Host for PluginHost { } } +/// +impl crate::api::plugin_api::repl::api::host_state_plugin::Host for PluginHost { + async fn get_repl_var(&mut self, key: String) -> Option { + self.repl_vars + .lock() + .expect("Failed to acquire repl_vars lock") + .get(&key) + .cloned() + } +} + /// It is necessary to implement this trait on PluginHost because other parts rely on it. impl crate::api::plugin_api::repl::api::transport::Host for PluginHost { // This trait has no methods, so no implementation needed @@ -124,25 +138,14 @@ impl crate::api::host_api::repl::api::host_state::Host for WasiState { self.plugins_names.clone() } - async fn set_repl_vars( - &mut self, - vars: wasmtime::component::__internal::Vec< - crate::api::host_api::repl::api::transport::ReplVar, - >, - ) { - // Store environment variables in the WasiState - for var in vars { - self.repl_vars.insert(var.key.clone(), var.value.clone()); - println!("Setting repl var: {} = {}", var.key, var.value); - } - } - async fn get_repl_vars( &mut self, ) -> wasmtime::component::__internal::Vec { // Return the stored environment variables self.repl_vars + .lock() + .expect("Failed to acquire repl_vars lock") .iter() .map( |(key, value)| crate::api::host_api::repl::api::transport::ReplVar { @@ -155,6 +158,9 @@ impl crate::api::host_api::repl::api::host_state::Host for WasiState { async fn set_repl_var(&mut self, var: crate::api::host_api::repl::api::transport::ReplVar) { // Set a single environment variable - self.repl_vars.insert(var.key.clone(), var.value.clone()); + self.repl_vars + .lock() + .expect("Failed to acquire repl_vars lock") + .insert(var.key.clone(), var.value.clone()); } } diff --git a/crates/pluginlab/tests/e2e_cli_host.rs b/crates/pluginlab/tests/e2e_cli_host.rs index a068bc7..5c07b69 100644 --- a/crates/pluginlab/tests/e2e_cli_host.rs +++ b/crates/pluginlab/tests/e2e_cli_host.rs @@ -89,6 +89,40 @@ mod e2e_cli_host { .expect("Didn't get expected error output"); } + #[test] + fn test_without_permission_allow_all() { + let project_root = find_project_root(); + println!("Setting current directory to: {:?}", project_root); + std::env::set_current_dir(&project_root).unwrap(); + let mut session = spawn( + &format!( + "{} --dir tmp/filesystem --allow-write", + &build_command( + &["target/wasm32-wasip1/debug/plugin_tee.wasm"], + "repl_logic_guest.wasm" + ) + ), + Some(TEST_TIMEOUT), + ) + .expect("Can't launch pluginlab"); + + session + .exp_string("[Host] Starting REPL host...") + .expect("Didn't see startup message"); + session + .exp_string("[Host] Loading plugin:") + .expect("Didn't see plugin loading message"); + session + .exp_string("repl(0)>") + .expect("Didn't see REPL prompt"); + session + .send_line("tee documents/work/projects/alpha/.gitkeep") + .expect("Failed to send command"); + session + .exp_string("permission denied - Failed to create file 'documents/work/projects/alpha/.gitkeep' - Operation not permitted (os error 63)\r\nrepl(1)>") + .expect("Didn't get expected output from tee plugin"); + } + #[test] fn test_with_wrong_version_of_plugin() { let crate_version = env!("CARGO_PKG_VERSION"); diff --git a/crates/pluginlab/tests/e2e_rust_plugins.rs b/crates/pluginlab/tests/e2e_rust_plugins.rs index cc3959b..1f8ceb2 100644 --- a/crates/pluginlab/tests/e2e_rust_plugins.rs +++ b/crates/pluginlab/tests/e2e_rust_plugins.rs @@ -173,4 +173,142 @@ mod e2e_rust_plugins { .exp_string("# Documents") .expect("Didn't get expected contents of README.md"); } + + #[test] + fn test_tee_plugin_new_file() { + let project_root = find_project_root(); + println!("Setting current directory to: {:?}", project_root); + std::env::set_current_dir(&project_root).unwrap(); + let mut session = spawn( + &format!( + "{} --dir tmp/filesystem --allow-all", + &build_command( + &["plugin_tee.wasm", "plugin_echo.wasm", "plugin_cat.wasm"], + "repl_logic_guest.wasm" + ) + ), + Some(TEST_TIMEOUT), + ) + .expect("Can't launch pluginlab with plugin greet"); + + session + .exp_string("[Host] Starting REPL host...") + .expect("Didn't see startup message"); + session + .exp_string("[Host] Loading plugin:") + .expect("Didn't see plugin loading message"); + session + .exp_string("repl(0)>") + .expect("Didn't see REPL prompt"); + session + .send_line("echo hello") + .expect("Failed to send command"); + session + .exp_string("hello\r\nrepl(0)>") + .expect("Didn't get expected output from echo plugin"); + session + .send_line("tee hello.txt") + .expect("Failed to send command"); + session + .exp_string("hello\r\nrepl(0)>") + .expect("Didn't get expected output from tee plugin"); + session + .send_line("cat hello.txt") + .expect("Failed to send command"); + session + .exp_string("hello\r\nrepl(0)>") + .expect("Didn't get expected contents of hello.txt"); + } + + #[test] + fn test_tee_plugin_replace_file() { + let project_root = find_project_root(); + println!("Setting current directory to: {:?}", project_root); + std::env::set_current_dir(&project_root).unwrap(); + let mut session = spawn( + &format!( + "{} --dir tmp/filesystem --allow-all", + &build_command( + &["plugin_tee.wasm", "plugin_echo.wasm", "plugin_cat.wasm"], + "repl_logic_guest.wasm" + ) + ), + Some(TEST_TIMEOUT), + ) + .expect("Can't launch pluginlab with plugin greet"); + + session + .exp_string("[Host] Starting REPL host...") + .expect("Didn't see startup message"); + session + .exp_string("[Host] Loading plugin:") + .expect("Didn't see plugin loading message"); + session + .exp_string("repl(0)>") + .expect("Didn't see REPL prompt"); + session + .send_line("echo hello") + .expect("Failed to send command"); + session + .exp_string("hello\r\nrepl(0)>") + .expect("Didn't get expected output from echo plugin"); + session + .send_line("tee documents/notes.txt") + .expect("Failed to send command"); + session + .exp_string("hello\r\nrepl(0)>") + .expect("Didn't get expected output from tee plugin"); + session + .send_line("cat documents/notes.txt") + .expect("Failed to send command"); + session + .exp_string("hello\r\nrepl(0)>") + .expect("Didn't get expected contents of hello.txt"); + } + + #[test] + fn test_tee_plugin_append_file() { + let project_root = find_project_root(); + println!("Setting current directory to: {:?}", project_root); + std::env::set_current_dir(&project_root).unwrap(); + let mut session = spawn( + &format!( + "{} --dir tmp/filesystem --allow-all", + &build_command( + &["plugin_tee.wasm", "plugin_echo.wasm", "plugin_cat.wasm"], + "repl_logic_guest.wasm" + ) + ), + Some(TEST_TIMEOUT), + ) + .expect("Can't launch pluginlab with plugin greet"); + + session + .exp_string("[Host] Starting REPL host...") + .expect("Didn't see startup message"); + session + .exp_string("[Host] Loading plugin:") + .expect("Didn't see plugin loading message"); + session + .exp_string("repl(0)>") + .expect("Didn't see REPL prompt"); + session + .send_line("echo hello") + .expect("Failed to send command"); + session + .exp_string("hello\r\nrepl(0)>") + .expect("Didn't get expected output from echo plugin"); + session + .send_line("tee -a documents/work/projects/alpha/.gitkeep") + .expect("Failed to send command"); + session + .exp_string("hello\r\nrepl(0)>") + .expect("Didn't get expected output from tee plugin"); + session + .send_line("cat documents/work/projects/alpha/.gitkeep") + .expect("Failed to send command"); + session + .exp_string("# This file ensures the alpha directory is tracked in git\r\n# Deep nested directory for testing\r\n\r\nhello\r\nrepl(0)>") + .expect("Didn't get expected contents of .gitkeep"); + } } diff --git a/crates/pluginlab/wit/host-api.wit b/crates/pluginlab/wit/host-api.wit index ae9e483..df765d0 100644 --- a/crates/pluginlab/wit/host-api.wit +++ b/crates/pluginlab/wit/host-api.wit @@ -5,7 +5,6 @@ interface host-state { use transport.{repl-var}; get-plugins-names: func() -> list; - set-repl-vars: func(vars: list); get-repl-vars: func() -> list; set-repl-var: func(var: repl-var); } diff --git a/crates/pluginlab/wit/plugin-api.wit b/crates/pluginlab/wit/plugin-api.wit index 211a0b4..df40892 100644 --- a/crates/pluginlab/wit/plugin-api.wit +++ b/crates/pluginlab/wit/plugin-api.wit @@ -23,7 +23,12 @@ interface http-client { get: func(url: string, headers: list) -> result; } +interface host-state-plugin { + get-repl-var: func(key: string) -> option; +} + world plugin-api { import http-client; + import host-state-plugin; export plugin; } diff --git a/packages/web-host/package.json b/packages/web-host/package.json index b6b9878..50bd797 100644 --- a/packages/web-host/package.json +++ b/packages/web-host/package.json @@ -14,6 +14,7 @@ "preview": "vite preview --host", "lint": "biome check .", "lint:fix": "biome check --write .", + "list-public-wasm-names": "ls -1 public/plugins/*.wasm|sed 's/.wasm//'|sed 's#public/plugins/##'", "typecheck": "tsc --noEmit -p tsconfig.app.json", "prepareVirtualFs": "node --experimental-strip-types --no-warnings ./clis/prepareFilesystem.ts --path fixtures/filesystem --format ts > src/wasm/virtualFs.ts; biome format --write ./src/wasm/virtualFs.ts", "test:e2e:all": "playwright test", @@ -22,14 +23,7 @@ "test:e2e:ui:preview": "BASE_URL=http://localhost:4173/webassembly-component-model-experiments npm run test:e2e:ui", "test:e2e:report": "playwright show-report", "test:e2e:like-in-ci": "CI=true GITHUB_ACTIONS=true WAIT_FOR_SERVER_AT_URL=http://localhost:4173/webassembly-component-model-experiments/ npm run test:e2e:all:preview", - "wasm:transpile:plugin-echo": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_echo.wasm -o ./src/wasm/generated/plugin_echo/transpiled", - "wasm:transpile:plugin-weather": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_weather.wasm -o ./src/wasm/generated/plugin_weather/transpiled", - "wasm:transpile:plugin-greet": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_greet.wasm -o ./src/wasm/generated/plugin_greet/transpiled", - "wasm:transpile:plugin-ls": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_ls.wasm -o ./src/wasm/generated/plugin_ls/transpiled", - "wasm:transpile:plugin-cat": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_cat.wasm -o ./src/wasm/generated/plugin_cat/transpiled", - "wasm:transpile:plugin-echoc": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin-echo-c.wasm -o ./src/wasm/generated/plugin-echo-c/transpiled", - "wasm:transpile:repl-logic-guest": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/repl_logic_guest.wasm -o ./src/wasm/generated/repl_logic_guest/transpiled", - "wasm:transpile": "npm run wasm:transpile:plugin-echo && npm run wasm:transpile:plugin-weather && npm run wasm:transpile:plugin-greet && npm run wasm:transpile:plugin-ls && npm run wasm:transpile:plugin-cat && npm run wasm:transpile:plugin-echoc && npm run wasm:transpile:repl-logic-guest", + "wasm:transpile": "npm run list-public-wasm-names --silent|xargs -I {} jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/{}.wasm -o ./src/wasm/generated/{}/transpiled", "wit-types:host-api": "jco types --world-name host-api --out-dir ./src/types/generated ../../crates/pluginlab/wit", "wit-types:plugin-api": "jco types --world-name plugin-api --out-dir ./src/types/generated ../../crates/pluginlab/wit", "wit-types": "npm run wit-types:clean && npm run wit-types:host-api && npm run wit-types:plugin-api && biome format --write ./src/types/generated", diff --git a/packages/web-host/src/types/generated/interfaces/repl-api-host-state-plugin.d.ts b/packages/web-host/src/types/generated/interfaces/repl-api-host-state-plugin.d.ts new file mode 100644 index 0000000..fb9ff85 --- /dev/null +++ b/packages/web-host/src/types/generated/interfaces/repl-api-host-state-plugin.d.ts @@ -0,0 +1,2 @@ +/** @module Interface repl:api/host-state-plugin **/ +export function getReplVar(key: string): string | undefined; diff --git a/packages/web-host/src/types/generated/interfaces/repl-api-host-state.d.ts b/packages/web-host/src/types/generated/interfaces/repl-api-host-state.d.ts index d53a2ee..79284e2 100644 --- a/packages/web-host/src/types/generated/interfaces/repl-api-host-state.d.ts +++ b/packages/web-host/src/types/generated/interfaces/repl-api-host-state.d.ts @@ -1,6 +1,5 @@ /** @module Interface repl:api/host-state **/ export function getPluginsNames(): Array; -export function setReplVars(vars: Array): void; export function getReplVars(): Array; export function setReplVar(var_: ReplVar): void; export type ReadlineResponse = diff --git a/packages/web-host/src/types/generated/plugin-api.d.ts b/packages/web-host/src/types/generated/plugin-api.d.ts index bf02cd4..f9ac334 100644 --- a/packages/web-host/src/types/generated/plugin-api.d.ts +++ b/packages/web-host/src/types/generated/plugin-api.d.ts @@ -1,4 +1,5 @@ // world repl:api/plugin-api +export type * as ReplApiHostStatePlugin from "./interfaces/repl-api-host-state-plugin.js"; // import repl:api/host-state-plugin export type * as ReplApiHttpClient from "./interfaces/repl-api-http-client.js"; // import repl:api/http-client export type * as ReplApiTransport from "./interfaces/repl-api-transport.js"; // import repl:api/transport export * as plugin from "./interfaces/repl-api-plugin.js"; // export repl:api/plugin diff --git a/packages/web-host/src/wasm/host/host-state-plugin.ts b/packages/web-host/src/wasm/host/host-state-plugin.ts new file mode 100644 index 0000000..9d29fc3 --- /dev/null +++ b/packages/web-host/src/wasm/host/host-state-plugin.ts @@ -0,0 +1,5 @@ +import { getReplVars } from "./host-state"; + +export function getReplVar(key: string): string | undefined { + return getReplVars().find((replVar) => replVar.key === key)?.value; +} diff --git a/packages/web-host/src/wasm/host/host-state.ts b/packages/web-host/src/wasm/host/host-state.ts index 4d7e1e4..188d1cf 100644 --- a/packages/web-host/src/wasm/host/host-state.ts +++ b/packages/web-host/src/wasm/host/host-state.ts @@ -23,9 +23,3 @@ export function getReplVars(): ReplVar[] { export function setReplVar({ key, value }: { key: string; value: string }) { internalState.replVars.set(key, value); } - -export function setReplVars(vars: ReplVar[]) { - for (const { key, value } of vars) { - internalState.replVars.set(key, value); - } -} diff --git a/packages/web-host/tsconfig.app.json b/packages/web-host/tsconfig.app.json index 3e90edf..3a2dc0f 100644 --- a/packages/web-host/tsconfig.app.json +++ b/packages/web-host/tsconfig.app.json @@ -24,7 +24,8 @@ "noUncheckedSideEffectImports": true, "paths": { "repl:api/host-state": ["./src/wasm/host/host-state.ts"], - "repl:api/http-client": ["./src/wasm/host/http-client.ts"] + "repl:api/http-client": ["./src/wasm/host/http-client.ts"], + "repl:api/host-state-plugin": ["./src/wasm/host/host-state-plugin.ts"] } }, "include": ["src", "clis"] diff --git a/packages/web-host/vite.config.ts b/packages/web-host/vite.config.ts index 9032152..c0730a9 100644 --- a/packages/web-host/vite.config.ts +++ b/packages/web-host/vite.config.ts @@ -1,6 +1,6 @@ +import path from "node:path"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; -import path from "path"; import { defineConfig } from "vite"; // https://vite.dev/config/ @@ -16,6 +16,10 @@ export default defineConfig({ __dirname, "./src/wasm/host/host-state.ts", ), + "repl:api/host-state-plugin": path.resolve( + __dirname, + "./src/wasm/host/host-state-plugin.ts", + ), }, }, base: "/webassembly-component-model-experiments/", diff --git a/scripts/prepare-wasm-files.sh b/scripts/prepare-wasm-files.sh index 94ac01f..0650c5f 100755 --- a/scripts/prepare-wasm-files.sh +++ b/scripts/prepare-wasm-files.sh @@ -40,6 +40,7 @@ prepare_wasm_files() { "target/wasm32-wasip1/$mode/plugin_ls.wasm" "target/wasm32-wasip1/$mode/plugin_cat.wasm" "target/wasm32-wasip1/$mode/plugin_weather.wasm" + "target/wasm32-wasip1/$mode/plugin_tee.wasm" "c_modules/plugin-echo/plugin-echo-c.wasm" "target/wasm32-wasip1/$mode/repl_logic_guest.wasm" )