From 053e8125602eec3b4c872fd1b22ec00187e56ce4 Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Mon, 28 Jul 2025 17:01:51 +0200 Subject: [PATCH 01/12] feat(wit)!: remove unnecessary host-api/host-state#set-repl-vars BREAKING CHANGE: break repl-logic backwards compatibility by changing the wit contract --- crates/pluginlab/src/store.rs | 13 ------------- crates/pluginlab/wit/host-api.wit | 1 - .../generated/interfaces/repl-api-host-state.d.ts | 1 - packages/web-host/src/wasm/host/host-state.ts | 6 ------ 4 files changed, 21 deletions(-) diff --git a/crates/pluginlab/src/store.rs b/crates/pluginlab/src/store.rs index 366f9d4..2a2bb5b 100644 --- a/crates/pluginlab/src/store.rs +++ b/crates/pluginlab/src/store.rs @@ -124,19 +124,6 @@ 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 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/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/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); - } -} From 80f8c5f9b094cd58f082aa85f52272e041d121f6 Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Mon, 28 Jul 2025 17:50:32 +0200 Subject: [PATCH 02/12] feat(wit)!: add plugin-api/host-state-plugin#get-repl-var so that plugin can access a variable if they know the name BREAKING CHANGE: break plugin-api backwards compatibility by changing the wit contract --- crates/pluginlab/src/store.rs | 10 ++++++++++ crates/pluginlab/wit/plugin-api.wit | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/crates/pluginlab/src/store.rs b/crates/pluginlab/src/store.rs index 2a2bb5b..04160ec 100644 --- a/crates/pluginlab/src/store.rs +++ b/crates/pluginlab/src/store.rs @@ -82,6 +82,16 @@ 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 { + None + // Can't do the following because `PluginHost` does not have access to repl_vars + // we need to add a repl_vars field to PluginHost that points to the repl_vars field of WasiState + // self.repl_vars.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 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; } From f6e1a77c28c75e831d37a2873c5ec25f12825d13 Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Mon, 28 Jul 2025 19:50:14 +0200 Subject: [PATCH 03/12] feat(pluginlab): safely share repl_vars behind Arc> between WasiState and PluginHost PluginHost needs to have access to repl_vars now that there is plugin-api/host-state-plugin#get-repl-var in wit. Use Arc> instead of direct references to satisfy Send trait requirements for async WebAssembly operations. While the runtime is single-threaded, Wasmtime's async bindings require Send + Sync types. --- crates/pluginlab/src/engine.rs | 8 ++++-- crates/pluginlab/src/helpers.rs | 18 ++++++++++--- crates/pluginlab/src/lib.rs | 46 +++++++++++++++++++++++---------- crates/pluginlab/src/store.rs | 21 ++++++++++----- 4 files changed, 67 insertions(+), 26 deletions(-) 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 04160ec..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 { @@ -85,10 +88,11 @@ 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 { - None - // Can't do the following because `PluginHost` does not have access to repl_vars - // we need to add a repl_vars field to PluginHost that points to the repl_vars field of WasiState - // self.repl_vars.get(&key).cloned() + self.repl_vars + .lock() + .expect("Failed to acquire repl_vars lock") + .get(&key) + .cloned() } } @@ -140,6 +144,8 @@ impl crate::api::host_api::repl::api::host_state::Host for WasiState { { // 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 { @@ -152,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()); } } From 6276ec1f93df0414c5e7b11aeaad4b60831482cf Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Mon, 28 Jul 2025 20:34:01 +0200 Subject: [PATCH 04/12] feat(web-host): implement plugin-api/host-state-plugin#get-repl-var --- .../generated/interfaces/repl-api-host-state-plugin.d.ts | 2 ++ packages/web-host/src/types/generated/plugin-api.d.ts | 1 + packages/web-host/src/wasm/host/host-state-plugin.ts | 5 +++++ packages/web-host/tsconfig.app.json | 3 ++- packages/web-host/vite.config.ts | 6 +++++- 5 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 packages/web-host/src/types/generated/interfaces/repl-api-host-state-plugin.d.ts create mode 100644 packages/web-host/src/wasm/host/host-state-plugin.ts 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/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/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/", From bd984282ccf3e55d1339fa3449d6799568b26a1a Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Mon, 4 Aug 2025 11:58:43 +0200 Subject: [PATCH 05/12] feat(plugin-tee): setup --- Cargo.lock | 7 ++++++ Cargo.toml | 1 + crates/plugin-tee/.gitignore | 1 + crates/plugin-tee/Cargo.toml | 18 ++++++++++++++++ crates/plugin-tee/README.md | 14 ++++++++++++ crates/plugin-tee/src/lib.rs | 42 ++++++++++++++++++++++++++++++++++++ 6 files changed, 83 insertions(+) create mode 100644 crates/plugin-tee/.gitignore create mode 100644 crates/plugin-tee/Cargo.toml create mode 100644 crates/plugin-tee/README.md create mode 100644 crates/plugin-tee/src/lib.rs 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/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..32182fe --- /dev/null +++ b/crates/plugin-tee/src/lib.rs @@ -0,0 +1,42 @@ +#[allow(warnings)] +mod bindings; + +use crate::bindings::exports::repl::api::plugin::Guest; +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 { + Ok(transport::PluginResponse { + status: transport::ReplStatus::Success, + stdout: Some(format!("{}", payload)), + stderr: None, + }) + } +} + +bindings::export!(Component with_types_in bindings); From 8b5e6087aa373e4ded49b5df972bbbab97e45eee Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Mon, 4 Aug 2025 12:17:07 +0200 Subject: [PATCH 06/12] feat(plugin-tee): prepare tests --- crates/pluginlab/tests/e2e_rust_plugins.rs | 138 +++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/crates/pluginlab/tests/e2e_rust_plugins.rs b/crates/pluginlab/tests/e2e_rust_plugins.rs index cc3959b..7c85858 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-read", + &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-read", + &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-read", + &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 data/sample.csv") + .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 data/sample.csv") + .expect("Failed to send command"); + session + .exp_string("id,name,age,city,active\r\n1,Alice,28,New York,true\r\n2,Bob,32,San Francisco,true\r\n3,Charlie,25,Chicago,false\r\n4,Diana,29,Boston,true\r\n5,Eve,35,Seattle,true\r\nhello\r\nrepl(0)>") + .expect("Didn't get expected contents of sample.csv"); + } } From d1e1551fe2f3a51a8fb6915ec2bcc7922f39baf3 Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Wed, 13 Aug 2025 10:05:40 +0200 Subject: [PATCH 07/12] feat(plugin-tee): working version --- crates/plugin-tee/src/lib.rs | 57 ++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/crates/plugin-tee/src/lib.rs b/crates/plugin-tee/src/lib.rs index 32182fe..727a328 100644 --- a/crates/plugin-tee/src/lib.rs +++ b/crates/plugin-tee/src/lib.rs @@ -1,7 +1,10 @@ #[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; @@ -31,11 +34,55 @@ DESCRIPTION } fn run(payload: String) -> Result { - Ok(transport::PluginResponse { - status: transport::ReplStatus::Success, - stdout: Some(format!("{}", payload)), - stderr: None, - }) + 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| format!("Failed to create file '{}': {}", filepath, e))?; + file.write_all(content_as_bytes) + .map_err(|e| format!("Failed to write to file '{}': {}", filepath, e))?; + return Ok(content); + } else { + let mut file = std::fs::File::options() + .append(true) + .open(&filepath) + .map_err(|e| format!("Failed to open file in append mode '{}': {}", filepath, e))?; + // Add a newline before the content in append mode + file.write_all(b"\n") + .map_err(|e| format!("Failed to write newline to file '{}': {}", filepath, e))?; + file.write_all(content_as_bytes) + .map_err(|e| format!("Failed to write to file '{}': {}", filepath, e))?; + return Ok(content); } } From ed5dbcda080eae5d4f25733593e925ae0b6cf33e Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Wed, 13 Aug 2025 10:30:05 +0200 Subject: [PATCH 08/12] feat(plugin-tee): raise permission denied error --- crates/plugin-tee/src/lib.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/plugin-tee/src/lib.rs b/crates/plugin-tee/src/lib.rs index 727a328..b0230ac 100644 --- a/crates/plugin-tee/src/lib.rs +++ b/crates/plugin-tee/src/lib.rs @@ -68,22 +68,32 @@ fn run_inner(payload: String) -> Result { if !is_append { let mut file = std::fs::File::create(&filepath) - .map_err(|e| format!("Failed to create file '{}': {}", filepath, e))?; + .map_err(|e| enhanced_error(e, format!("Failed to create file '{}'", filepath)))?; file.write_all(content_as_bytes) - .map_err(|e| format!("Failed to write to file '{}': {}", filepath, e))?; + .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| format!("Failed to open file in append mode '{}': {}", filepath, e))?; + .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| format!("Failed to write newline to file '{}': {}", filepath, e))?; + 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| format!("Failed to write to file '{}': {}", filepath, e))?; + .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); From ad5344ec2eaf63536681feae4ae10af28038af46 Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Wed, 13 Aug 2025 12:09:56 +0200 Subject: [PATCH 09/12] test(plugin-tee): fix tests --- crates/pluginlab/tests/e2e_cli_host.rs | 34 ++++++++++++++++++++++ crates/pluginlab/tests/e2e_rust_plugins.rs | 14 ++++----- 2 files changed, 41 insertions(+), 7 deletions(-) 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 7c85858..1f8ceb2 100644 --- a/crates/pluginlab/tests/e2e_rust_plugins.rs +++ b/crates/pluginlab/tests/e2e_rust_plugins.rs @@ -181,7 +181,7 @@ mod e2e_rust_plugins { std::env::set_current_dir(&project_root).unwrap(); let mut session = spawn( &format!( - "{} --dir tmp/filesystem --allow-read", + "{} --dir tmp/filesystem --allow-all", &build_command( &["plugin_tee.wasm", "plugin_echo.wasm", "plugin_cat.wasm"], "repl_logic_guest.wasm" @@ -227,7 +227,7 @@ mod e2e_rust_plugins { std::env::set_current_dir(&project_root).unwrap(); let mut session = spawn( &format!( - "{} --dir tmp/filesystem --allow-read", + "{} --dir tmp/filesystem --allow-all", &build_command( &["plugin_tee.wasm", "plugin_echo.wasm", "plugin_cat.wasm"], "repl_logic_guest.wasm" @@ -273,7 +273,7 @@ mod e2e_rust_plugins { std::env::set_current_dir(&project_root).unwrap(); let mut session = spawn( &format!( - "{} --dir tmp/filesystem --allow-read", + "{} --dir tmp/filesystem --allow-all", &build_command( &["plugin_tee.wasm", "plugin_echo.wasm", "plugin_cat.wasm"], "repl_logic_guest.wasm" @@ -299,16 +299,16 @@ mod e2e_rust_plugins { .exp_string("hello\r\nrepl(0)>") .expect("Didn't get expected output from echo plugin"); session - .send_line("tee -a data/sample.csv") + .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 data/sample.csv") + .send_line("cat documents/work/projects/alpha/.gitkeep") .expect("Failed to send command"); session - .exp_string("id,name,age,city,active\r\n1,Alice,28,New York,true\r\n2,Bob,32,San Francisco,true\r\n3,Charlie,25,Chicago,false\r\n4,Diana,29,Boston,true\r\n5,Eve,35,Seattle,true\r\nhello\r\nrepl(0)>") - .expect("Didn't get expected contents of sample.csv"); + .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"); } } From f743315ccb8c5d11b57f0b12b4ac763e90a3ce48 Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Wed, 13 Aug 2025 14:48:18 +0200 Subject: [PATCH 10/12] feat(web-host): task wasm:transpile dynamically picks wasm files --- packages/web-host/package.json | 10 ++-------- scripts/prepare-wasm-files.sh | 1 + 2 files changed, 3 insertions(+), 8 deletions(-) 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/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" ) From 88efeae0eac1f21b25486984a71caf581ae5e4b5 Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Wed, 13 Aug 2025 16:38:03 +0200 Subject: [PATCH 11/12] docs: add plugin-tee --- README.md | 20 +++++++++++++------- crates/pluginlab/README.md | 19 +++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) 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/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] From 1ce3c75248f13e603fb5e2eb7cb6bb758df01598 Mon Sep 17 00:00:00 2001 From: Christophe Rosset Date: Sat, 16 Aug 2025 13:17:46 +0200 Subject: [PATCH 12/12] chore(cursor): add rules for git related to terminal --- .cursor/rules/git-commands.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .cursor/rules/git-commands.md 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`