aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md19
-rw-r--r--mling/Cargo.lock455
-rw-r--r--mling/Cargo.toml38
-rw-r--r--mling/src/cli.rs45
-rw-r--r--mling/src/cli/list.rs117
-rw-r--r--mling/src/cli/namespace_mgr.rs128
-rw-r--r--mling/src/cli/read.rs77
-rw-r--r--mling/src/cli/refresh.rs32
-rw-r--r--mling/src/main.rs14
-rw-r--r--mling/src/namespace_manager.rs113
-rw-r--r--mling/src/project_installer.rs153
-rw-r--r--mling/src/project_solver.rs105
-rw-r--r--mling/tmpl/load.fish75
-rw-r--r--mling/tmpl/load.ps185
-rw-r--r--mling/tmpl/load.sh100
15 files changed, 1547 insertions, 9 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 808b808..7ba069e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,8 @@
#### Features:
-1. **\[macros\]** Completed the `clap` feature: **Mingling** now supports parsing input using `clap::Parser`
+1. **\[mingling\]** Added the scaffolding tool `mling`, which can quickly deploy and test your command-line programs
+2. **\[macros\]** Completed the `clap` feature: **Mingling** now supports parsing input using `clap::Parser`
```rust
#[derive(Groupped, clap::Parser)]
@@ -25,13 +26,13 @@ struct YourCommandEntry {
}
```
-2. **\[clap\]** Added the `stdout_setting.clap_help_print_behaviour` configuration item to `Program`, used to control the behavior of Clap Help
+3. **\[clap\]** Added the `stdout_setting.clap_help_print_behaviour` configuration item to `Program`, used to control the behavior of Clap Help
-3. **\[core\]** Added function `new_with_args` to `Program`
-4. **\[core\]** Added function `dispatch_args_dynamic` to `Program`
-5. **\[core\]** Impl `std::io::Write` trait for `RenderResult`
-6. **\[core\]** Added Help system, which allows binding an event for `--help` to an `Entry` via the `help!` macro
-7. **\[core\]** Added the function `build_comp_script_to` to the `mingling::build` module: supports outputting completion scripts precisely to a specified directory
+4. **\[core\]** Added function `new_with_args` to `Program`
+5. **\[core\]** Added function `dispatch_args_dynamic` to `Program`
+6. **\[core\]** Impl `std::io::Write` trait for `RenderResult`
+7. **\[core\]** Added Help system, which allows binding an event for `--help` to an `Entry` via the `help!` macro
+8. **\[core\]** Added the function `build_comp_script_to` to the `mingling::build` module: supports outputting completion scripts precisely to a specified directory
```rust
#[help]
@@ -40,7 +41,7 @@ fn your_command_help(_prev: YourEntry) {
}
```
-7. **\[macros\]** Added the `route!` macro, which allows quick error routing within the `chain!` function. Usage is as follows:
+9. **\[macros\]** Added the `route!` macro, which allows quick error routing within the `chain!` function. Usage is as follows:
```rust
// Before
@@ -76,7 +77,7 @@ fn parse(prev: PickEntry) -> mingling::ChainProcess<ThisProgram> {
}
```
-8. Added a resource system to `Program` for managing global resources [Details](docs/res/changlog_examples/feat_program_res.rs)
+10. Added a resource system to `Program` for managing global resources [Details](docs/res/changlog_examples/feat_program_res.rs)
```rust
// Define global resource
diff --git a/mling/Cargo.lock b/mling/Cargo.lock
new file mode 100644
index 0000000..7167d1d
--- /dev/null
+++ b/mling/Cargo.lock
@@ -0,0 +1,455 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "colored"
+version = "3.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "just_fmt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e"
+
+[[package]]
+name = "just_template"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225"
+dependencies = [
+ "just_fmt",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libredox"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "mingling"
+version = "0.1.7"
+dependencies = [
+ "mingling_core",
+ "mingling_macros",
+ "serde",
+ "size",
+]
+
+[[package]]
+name = "mingling-cli"
+version = "0.1.0"
+dependencies = [
+ "colored",
+ "dirs",
+ "just_fmt",
+ "mingling",
+ "serde",
+ "serde_json",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "mingling_core"
+version = "0.1.7"
+dependencies = [
+ "just_fmt",
+ "just_template",
+ "once_cell",
+ "ron",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "thiserror",
+ "toml 1.1.2+spec-1.1.0",
+]
+
+[[package]]
+name = "mingling_macros"
+version = "0.1.7"
+dependencies = [
+ "just_fmt",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
+name = "ron"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4147b952f3f819eca0e99527022f7d6a8d05f111aeb0a62960c74eb283bec8fc"
+dependencies = [
+ "bitflags",
+ "once_cell",
+ "serde",
+ "serde_derive",
+ "typeid",
+ "unicode-ident",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "size"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b6709c7b6754dca1311b3c73e79fcce40dd414c782c66d88e8823030093b02b"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.2",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow 1.0.2",
+]
+
+[[package]]
+name = "toml_writer"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+
+[[package]]
+name = "winnow"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
diff --git a/mling/Cargo.toml b/mling/Cargo.toml
new file mode 100644
index 0000000..b722420
--- /dev/null
+++ b/mling/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "mingling-cli"
+version = "0.1.0"
+edition = "2024"
+
+[[bin]]
+name = "mling"
+path = "src/main.rs"
+
+[profile.dev]
+opt-level = 0
+debug = true
+
+[profile.release]
+opt-level = 3
+lto = "fat"
+codegen-units = 1
+panic = "abort"
+strip = true
+
+
+[dependencies]
+mingling = { path = "../mingling", features = [
+ "parser",
+ "comp",
+ "general_renderer",
+] }
+
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+
+colored = "3.1.1"
+dirs = "6.0.0"
+just_fmt = "0.1.2"
+toml = "0.9.8"
+
+[build-dependencies]
+mingling = { path = "../mingling", features = ["comp"] }
diff --git a/mling/src/cli.rs b/mling/src/cli.rs
new file mode 100644
index 0000000..e0dfbe6
--- /dev/null
+++ b/mling/src/cli.rs
@@ -0,0 +1,45 @@
+use mingling::{
+ macros::renderer,
+ setup::{BasicProgramSetup, GeneralRendererSetup},
+};
+
+use crate::{__completion_gen::CompletionDispatcher, DispatcherNotFound, ThisProgram};
+
+pub mod list;
+pub use list::*;
+
+pub mod namespace_mgr;
+pub use namespace_mgr::*;
+
+pub mod read;
+pub use read::*;
+
+pub mod refresh;
+pub use refresh::*;
+
+pub fn cli_entry() {
+ let mut program = ThisProgram::new();
+
+ program.with_setup(BasicProgramSetup);
+ program.with_setup(GeneralRendererSetup);
+ program.with_dispatcher(CompletionDispatcher);
+
+ program.with_dispatcher(ListInstalledCommand);
+ program.with_dispatchers((
+ TrustNamespaceCommand,
+ UntrustNamespaceCommand,
+ SetTrustNamespaceCommand,
+ RemoveNamespaceCommand,
+ ));
+ program.with_dispatcher(RefreshCommand);
+ program.with_dispatchers((
+ ReadTargetDirCommand,
+ ReadWorkspaceRootCommand,
+ ReadBinariesCommand,
+ ));
+
+ program.exec();
+}
+
+#[renderer]
+pub(crate) fn render_help(_prev: DispatcherNotFound) {}
diff --git a/mling/src/cli/list.rs b/mling/src/cli/list.rs
new file mode 100644
index 0000000..9aff22b
--- /dev/null
+++ b/mling/src/cli/list.rs
@@ -0,0 +1,117 @@
+use colored::Colorize;
+use mingling::{
+ Groupped, RenderResult, ShellContext, Suggest,
+ macros::{chain, completion, dispatcher, pack, r_println, renderer, suggest},
+ parser::Picker,
+};
+use serde::Serialize;
+
+use crate::{ThisProgram, namespace_manager::list_namespaces};
+
+dispatcher!("ls-namespace", ListInstalledCommand => ListInstalledEntry);
+
+#[completion(ListInstalledEntry)]
+pub(crate) fn comp_list_installed(ctx: &ShellContext) -> Suggest {
+ if ctx.typing_argument() {
+ return suggest! {
+ "--trusted": "Show only trusted namespaces",
+ "--untrusted": "Show only untrusted namespaces",
+ };
+ }
+ return suggest!();
+}
+
+#[derive(Debug, Serialize, Default, Groupped)]
+pub(crate) enum StateListInstalledOptions {
+ #[default]
+ All,
+ OnlyTrusted,
+ OnlyUntrusted,
+}
+
+pack!(MutexErrorListInstalled = ());
+
+#[chain]
+pub(crate) fn handle_list_installed_entry(prev: ListInstalledEntry) -> NextProcess {
+ let picker = Picker::new(prev.inner);
+ let r = picker
+ .pick::<bool>("--trusted")
+ .pick::<bool>("--untrusted")
+ .unpack();
+
+ let option: StateListInstalledOptions = match r {
+ // (show_trusted, show_untrusted)
+ (true, false) => StateListInstalledOptions::OnlyTrusted,
+ (false, true) => StateListInstalledOptions::OnlyUntrusted,
+ (false, false) => StateListInstalledOptions::All,
+ (true, true) => return MutexErrorListInstalled::default().to_render(),
+ };
+
+ option.to_chain()
+}
+
+#[renderer]
+pub(crate) fn render_list_installed_mutex_error(_prev: MutexErrorListInstalled) {
+ r_println!("Error: cannot use both --trusted and --untrusted options at the same time")
+}
+
+#[derive(Debug, Groupped, Serialize)]
+pub(crate) struct ResultInstalledNamespaces {
+ trusted: Vec<String>,
+ untrusted: Vec<String>,
+ untagged: Vec<String>,
+ option: StateListInstalledOptions,
+}
+
+#[chain]
+pub(crate) fn handle_state_list_installed_option(prev: StateListInstalledOptions) -> NextProcess {
+ ResultInstalledNamespaces {
+ trusted: list_namespaces(true, false, false),
+ untrusted: list_namespaces(false, true, false),
+ untagged: list_namespaces(false, false, true),
+ option: prev,
+ }
+}
+
+#[renderer]
+pub(crate) fn render_installed(prev: ResultInstalledNamespaces) {
+ match prev.option {
+ StateListInstalledOptions::All => {
+ print_list("Trusted".bright_green().bold().to_string(), prev.trusted, r);
+ print_list(
+ "Unrusted".bright_red().bold().to_string(),
+ prev.untrusted,
+ r,
+ );
+ print_list(
+ "Untagged".bright_black().bold().to_string(),
+ prev.untagged,
+ r,
+ );
+ }
+ StateListInstalledOptions::OnlyTrusted => {
+ print_list("Trusted".bright_green().bold().to_string(), prev.trusted, r);
+ }
+ StateListInstalledOptions::OnlyUntrusted => {
+ print_list(
+ "Unrusted".bright_red().bold().to_string(),
+ prev.untrusted,
+ r,
+ );
+ }
+ }
+}
+
+fn print_list(title: String, list: Vec<String>, r: &mut RenderResult) {
+ if list.is_empty() {
+ return;
+ }
+
+ r_println!("{}", title);
+
+ let mut i = 1;
+ for namespace in list.iter() {
+ r_println!(" {}. {}\n", i.to_string(), namespace.bold());
+ i += 1;
+ }
+}
diff --git a/mling/src/cli/namespace_mgr.rs b/mling/src/cli/namespace_mgr.rs
new file mode 100644
index 0000000..9781040
--- /dev/null
+++ b/mling/src/cli/namespace_mgr.rs
@@ -0,0 +1,128 @@
+use mingling::{
+ ShellContext, Suggest, SuggestItem,
+ macros::{chain, completion, dispatcher, pack, r_println, renderer, route, suggest},
+ parser::{Picker, Yes},
+};
+
+use crate::{
+ ThisProgram,
+ namespace_manager::{list_namespaces, remove_namespace, set_namespace_trusted},
+};
+
+dispatcher!("trust", TrustNamespaceCommand => TrustNamespaceEntry);
+dispatcher!("untrust", UntrustNamespaceCommand => UntrustNamespaceEntry);
+
+dispatcher!("set-trust", SetTrustNamespaceCommand => SetTrustNamespaceEntry);
+
+dispatcher!("rm-namespace", RemoveNamespaceCommand => RemoveNamespaceEntry);
+
+pack!(ErrorNamespaceNotProvided = ());
+pack!(ResultNamespaceTrustChanged = ());
+pack!(ResultNamespaceRemoved = ());
+
+#[completion(TrustNamespaceEntry)]
+pub(crate) fn comp_trust(ctx: &ShellContext) -> Suggest {
+ if ctx.previous_word == "trust" {
+ return Suggest::Suggest(
+ list_namespaces(false, true, true)
+ .into_iter()
+ .map(|i| SuggestItem::new(i))
+ .collect::<std::collections::BTreeSet<_>>(),
+ );
+ }
+ return suggest!();
+}
+
+#[completion(UntrustNamespaceEntry)]
+pub(crate) fn comp_untrust(ctx: &ShellContext) -> Suggest {
+ if ctx.previous_word == "untrust" {
+ return Suggest::Suggest(
+ list_namespaces(true, false, true)
+ .into_iter()
+ .map(|i| SuggestItem::new(i))
+ .collect::<std::collections::BTreeSet<_>>(),
+ );
+ }
+ return suggest!();
+}
+
+#[completion(SetTrustNamespaceEntry)]
+pub(crate) fn comp_set_trust(ctx: &ShellContext) -> Suggest {
+ if ctx.typing_argument() {
+ return suggest!(
+ "-t": "Whether to trust this namespace",
+ "--trusted": "Whether to trust this namespace",
+ );
+ }
+ if ctx.filling_argument_first(["-t", "--trusted"]) {
+ return suggest!("yes", "no");
+ }
+ if ctx.previous_word == "set-trust" {
+ return Suggest::Suggest(
+ list_namespaces(true, true, true)
+ .into_iter()
+ .map(|i| SuggestItem::new(i))
+ .collect::<std::collections::BTreeSet<_>>(),
+ );
+ }
+ return suggest!();
+}
+
+#[completion(RemoveNamespaceEntry)]
+pub(crate) fn comp_remove_namespace(ctx: &ShellContext) -> Suggest {
+ if ctx.previous_word == "rm-namespace" {
+ return Suggest::Suggest(
+ list_namespaces(true, true, true)
+ .into_iter()
+ .map(|i| SuggestItem::new(i))
+ .collect::<std::collections::BTreeSet<_>>(),
+ );
+ }
+ return suggest!();
+}
+
+#[chain]
+pub(crate) fn handle_set_trust(p: SetTrustNamespaceEntry) -> NextProcess {
+ let (trusted, namespace) = route!(
+ Picker::new(p.inner)
+ .pick::<Yes>(["-t", "--trusted"])
+ .pick_or_route((), ErrorNamespaceNotProvided::default().to_render())
+ .unpack()
+ );
+ set_namespace_trusted(namespace, trusted.is_yes());
+ ResultNamespaceTrustChanged::default().to_render()
+}
+
+#[chain]
+pub(crate) fn handle_trust(p: TrustNamespaceEntry) -> NextProcess {
+ SetTrustNamespaceEntry::new({
+ let mut args = p.inner.clone();
+ args.extend(vec!["-t".to_string(), "yes".to_string()]);
+ args
+ })
+}
+
+#[chain]
+pub(crate) fn handle_untrust(p: UntrustNamespaceEntry) -> NextProcess {
+ SetTrustNamespaceEntry::new({
+ let mut args = p.inner.clone();
+ args.extend(vec!["-t".to_string(), "no".to_string()]);
+ args
+ })
+}
+
+#[chain]
+pub(crate) fn handle_remove_namespace(p: RemoveNamespaceEntry) -> NextProcess {
+ let namespace = route!(
+ Picker::new(p.inner)
+ .pick_or_route((), ErrorNamespaceNotProvided::default().to_render())
+ .unpack()
+ );
+ remove_namespace(namespace);
+ ResultNamespaceRemoved::default().to_render()
+}
+
+#[renderer]
+pub(crate) fn render_error_namespace_not_provided(_prev: ErrorNamespaceNotProvided) {
+ r_println!("Error: no namespace was provided!")
+}
diff --git a/mling/src/cli/read.rs b/mling/src/cli/read.rs
new file mode 100644
index 0000000..8717932
--- /dev/null
+++ b/mling/src/cli/read.rs
@@ -0,0 +1,77 @@
+use colored::Colorize;
+use std::path::PathBuf;
+
+use mingling::{
+ Groupped,
+ macros::{chain, dispatcher, pack, r_println, renderer},
+};
+use serde::Serialize;
+
+use crate::{
+ ThisProgram,
+ project_solver::{BinaryItem, solve_current_dir},
+};
+
+dispatcher!("show-target-dir", ReadTargetDirCommand => ReadTargetDirEntry);
+dispatcher!("show-workspace-root", ReadWorkspaceRootCommand => ReadWorkspaceRootEntry);
+dispatcher!("show-binaries", ReadBinariesCommand => ReadBinariesEntry);
+
+pack!(ResultDir = PathBuf);
+pack!(ResultTargetDirNotFound = ());
+
+#[derive(Debug, Serialize, Default, Groupped)]
+pub(crate) struct ResultBinaries {
+ bin: Vec<BinaryItem>,
+}
+
+#[chain]
+pub(crate) fn handle_target_dir_entry(_prev: ReadTargetDirEntry) -> NextProcess {
+ match solve_current_dir() {
+ Ok(solved) => {
+ let dir = solved.target_dir;
+ ResultDir::new(dir).to_render()
+ }
+ Err(_) => ResultTargetDirNotFound::new(()).to_render(),
+ }
+}
+
+#[chain]
+pub(crate) fn handle_workspace_root_entry(_prev: ReadWorkspaceRootEntry) -> NextProcess {
+ match solve_current_dir() {
+ Ok(solved) => {
+ let dir = solved.workspace_root;
+ ResultDir::new(dir).to_render()
+ }
+ Err(_) => ResultTargetDirNotFound::new(()).to_render(),
+ }
+}
+
+#[chain]
+pub(crate) fn handle_binaries_entry(_prev: ReadBinariesEntry) -> NextProcess {
+ match solve_current_dir() {
+ Ok(solved) => {
+ let binaries = solved.binaries;
+ ResultBinaries { bin: binaries }.to_render()
+ }
+ Err(_) => ResultTargetDirNotFound::new(()).to_render(),
+ }
+}
+
+#[renderer]
+pub(crate) fn render_dir(prev: ResultDir) {
+ r_println!("{}", prev.inner.display())
+}
+
+#[renderer]
+pub(crate) fn render_binaries(prev: ResultBinaries) {
+ let mut i = 1;
+ for item in prev.bin.iter() {
+ r_println!(
+ "{}. {} ({})",
+ i.to_string(),
+ item.name.bold(),
+ item.path.to_string_lossy().underline().bright_cyan()
+ );
+ i += 1;
+ }
+}
diff --git a/mling/src/cli/refresh.rs b/mling/src/cli/refresh.rs
new file mode 100644
index 0000000..368670e
--- /dev/null
+++ b/mling/src/cli/refresh.rs
@@ -0,0 +1,32 @@
+use mingling::{
+ ShellContext, Suggest,
+ macros::{chain, completion, dispatcher, pack, suggest},
+ parser::Picker,
+};
+
+use crate::{ThisProgram, project_installer::install_all};
+
+dispatcher!("refresh", RefreshCommand => RefreshEntry);
+
+pack!(ResultRefreshCompleted = ());
+
+#[completion(RefreshEntry)]
+pub(crate) fn comp_refresh(ctx: &ShellContext) -> Suggest {
+ if ctx.typing_argument() {
+ return suggest! {
+ "--clean": "Clean build artifacts before installation",
+ "-c": "Clean build artifacts before installation",
+ };
+ }
+ return suggest!();
+}
+
+#[chain]
+pub(crate) fn handle_refresh_entry(prev: RefreshEntry) -> NextProcess {
+ let is_clean_before_build = Picker::new(prev.inner)
+ .pick::<bool>(["--clean", "-c"])
+ .unpack();
+ let _ = install_all(is_clean_before_build);
+
+ ResultRefreshCompleted::new(())
+}
diff --git a/mling/src/main.rs b/mling/src/main.rs
new file mode 100644
index 0000000..7b72dc1
--- /dev/null
+++ b/mling/src/main.rs
@@ -0,0 +1,14 @@
+use mingling::macros::gen_program;
+
+pub mod cli;
+pub mod namespace_manager;
+pub mod project_installer;
+pub mod project_solver;
+
+use crate::cli::*;
+
+fn main() {
+ cli_entry();
+}
+
+gen_program!();
diff --git a/mling/src/namespace_manager.rs b/mling/src/namespace_manager.rs
new file mode 100644
index 0000000..4d36136
--- /dev/null
+++ b/mling/src/namespace_manager.rs
@@ -0,0 +1,113 @@
+use std::path::PathBuf;
+
+use just_fmt::kebab_case;
+
+pub fn list_namespaces(
+ show_trusted: bool,
+ show_untrusted: bool,
+ show_untagged: bool,
+) -> Vec<String> {
+ let wdir = working_dir();
+ if !wdir.exists() {
+ return Vec::new();
+ }
+
+ let mut namespaces = Vec::new();
+ let entries = match std::fs::read_dir(&wdir) {
+ Ok(entries) => entries,
+ Err(_) => return Vec::new(),
+ };
+ for entry in entries {
+ let entry = match entry {
+ Ok(e) => e,
+ Err(_) => continue,
+ };
+ let path = entry.path();
+ if path.is_dir() {
+ if let Some(name) = path.file_name() {
+ if let Some(name_str) = name.to_str() {
+ // Skip directories starting with a dot
+ if name_str.starts_with('.') {
+ continue;
+ }
+ let namespace = name_str.to_string();
+ let is_trusted = is_trusted_namespace(namespace.clone());
+ let is_untrusted = is_untrusted_namespace(namespace.clone());
+ let is_untagged = is_untagged_namespace(namespace.clone());
+
+ if (show_trusted && is_trusted)
+ || (show_untrusted && is_untrusted)
+ || (show_untagged && is_untagged)
+ {
+ namespaces.push(namespace);
+ }
+ }
+ }
+ }
+ }
+
+ namespaces
+}
+
+pub fn set_namespace_trusted(namespace: String, trusted: bool) {
+ let ndir = namespace_dir(namespace);
+ let trusted_file = ndir.join("TRUSTED");
+ let untrusted_file = ndir.join("UNTRUSTED");
+
+ if trusted {
+ // Create TRUSTED file and remove UNTRUSTED if it exists
+ let _ = std::fs::write(&trusted_file, "");
+ let _ = std::fs::remove_file(&untrusted_file);
+ } else {
+ // Remove TRUSTED file
+ let _ = std::fs::remove_file(&trusted_file);
+ }
+}
+
+pub fn remove_namespace(namespace: String) {
+ let ndir = namespace_dir(namespace);
+ if ndir.exists() {
+ let _ = std::fs::remove_dir_all(&ndir);
+ }
+}
+
+pub fn working_dir() -> PathBuf {
+ dirs::data_dir().unwrap().join("mingling")
+}
+
+pub fn namespace_dir(namespace: String) -> PathBuf {
+ working_dir().join(kebab_case!(namespace))
+}
+
+pub fn is_untrusted_namespace(namespace: String) -> bool {
+ let untrusted_file = namespace_dir(namespace).join("UNTRUSTED");
+ untrusted_file.exists()
+}
+
+pub fn is_trusted_namespace(namespace: String) -> bool {
+ let trusted = namespace_dir(namespace).join("TRUSTED");
+ trusted.exists()
+}
+
+pub fn is_untagged_namespace(namespace: String) -> bool {
+ let ndir = namespace_dir(namespace);
+ let trusted = ndir.join("TRUSTED");
+ let untrusted = ndir.join("UNTRUSTED");
+ !trusted.exists() && !untrusted.exists()
+}
+
+pub fn bin_dir(namespace: String) -> PathBuf {
+ namespace_dir(namespace).join("bin")
+}
+
+pub fn comp_dir(namespace: String) -> PathBuf {
+ namespace_dir(namespace).join("comp")
+}
+
+pub fn exe_path(namespace: String, bin_name_without_ext: String) -> PathBuf {
+ if cfg!(target_os = "windows") {
+ bin_dir(namespace).join(bin_name_without_ext + ".exe")
+ } else {
+ bin_dir(namespace).join(bin_name_without_ext)
+ }
+}
diff --git a/mling/src/project_installer.rs b/mling/src/project_installer.rs
new file mode 100644
index 0000000..d004e40
--- /dev/null
+++ b/mling/src/project_installer.rs
@@ -0,0 +1,153 @@
+use std::path::PathBuf;
+
+use mingling::{ShellFlag, build::build_comp_script_to};
+
+use crate::{
+ namespace_manager::{bin_dir, comp_dir, exe_path, working_dir},
+ project_solver::solve,
+};
+
+const SCRIPT_LOAD_BASH: &str = include_str!("../tmpl/load.sh");
+const SCRIPT_LOAD_FISH: &str = include_str!("../tmpl/load.fish");
+const SCRIPT_LOAD_PWSH: &str = include_str!("../tmpl/load.ps1");
+
+#[derive(serde::Deserialize)]
+struct CargoToml {
+ package: Package,
+}
+
+#[derive(serde::Deserialize)]
+struct Package {
+ name: String,
+}
+
+pub fn install_all(clean_before_build: bool) -> Result<(), std::io::Error> {
+ let current = std::env::current_dir()?;
+ install_this_project(current, clean_before_build)?;
+ install_shell_scripts()?;
+ Ok(())
+}
+
+pub fn install_this_project(
+ current: PathBuf,
+ clean_before_build: bool,
+) -> Result<(), std::io::Error> {
+ // Obtain context data
+ let solved = solve(current)?;
+
+ let workspace_root = &solved.workspace_root;
+
+ // If clean_before_build, execute cargo clean in workspace_root first
+ if clean_before_build {
+ let status = std::process::Command::new("cargo")
+ .arg("clean")
+ .current_dir(workspace_root)
+ .status()?;
+ if !status.success() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "exec `cargo clean` failed",
+ ));
+ }
+ }
+
+ // Execute cargo build --release in workspace_root
+ let status = std::process::Command::new("cargo")
+ .args(["build", "--release"])
+ .current_dir(workspace_root)
+ .status()?;
+ if !status.success() {
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "cargo build --release failed",
+ ));
+ }
+
+ // Parse package.name from workspace_root's Cargo.toml as namespace
+ let cargo_toml_content = std::fs::read_to_string(workspace_root.join("Cargo.toml"))?;
+ let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content).map_err(|e| {
+ std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ format!("failed to parse Cargo.toml: {e}"),
+ )
+ })?;
+ let namespace = cargo_toml.package.name;
+
+ // Ensure destination directories exist
+ std::fs::create_dir_all(bin_dir(namespace.clone()))?;
+ std::fs::create_dir_all(comp_dir(namespace.clone()))?;
+
+ // Copy binaries to corresponding exe_path
+ for bin in &solved.binaries {
+ let dst = exe_path(namespace.clone(), bin.name.clone());
+ std::fs::copy(&bin.path, &dst)?;
+ }
+
+ // Copy all completion scripts containing _comp from target/release to comp_dir
+ let target_dir = &solved.target_dir;
+ let release_dir = target_dir.join("release");
+ if release_dir.exists() {
+ for entry in std::fs::read_dir(&release_dir)? {
+ let entry = entry?;
+ let file_name = entry.file_name();
+ let file_name_str = file_name.to_string_lossy();
+ if file_name_str.contains("_comp") {
+ let dest = comp_dir(namespace.clone()).join(file_name.as_os_str());
+ std::fs::copy(entry.path(), &dest)?;
+ }
+ }
+ }
+
+ Ok(())
+}
+
+pub fn install_shell_scripts() -> Result<(), std::io::Error> {
+ // Get the working directory (mingling data dir)
+ let wdir = working_dir();
+ std::fs::create_dir_all(&wdir)?;
+
+ // Build shell completion scripts for the "mling" command based on the current OS
+ let mling_comp = if cfg!(target_os = "windows") {
+ vec![ShellFlag::Powershell]
+ } else if cfg!(target_os = "macos") || cfg!(target_os = "linux") {
+ vec![ShellFlag::Bash, ShellFlag::Zsh, ShellFlag::Fish]
+ } else {
+ vec![ShellFlag::Bash]
+ };
+
+ for flag in mling_comp {
+ build_comp_script_to(
+ &flag,
+ "mling",
+ wdir.join(".comp").display().to_string().as_str(),
+ )?;
+ }
+
+ // Determine which scripts to write based on platform
+ let scripts: Vec<(&str, &str)> = if cfg!(target_os = "windows") {
+ vec![("load.ps1", SCRIPT_LOAD_PWSH)]
+ } else if cfg!(target_os = "macos") || cfg!(target_os = "linux") {
+ vec![
+ ("load.sh", SCRIPT_LOAD_BASH),
+ ("load.fish", SCRIPT_LOAD_FISH),
+ ]
+ } else {
+ // Fallback: write bash script
+ vec![("load.sh", SCRIPT_LOAD_BASH)]
+ };
+
+ for (filename, content) in scripts {
+ let dest = wdir.join(filename);
+ std::fs::write(&dest, content)?;
+ if cfg!(target_os = "linux") {
+ let status = std::process::Command::new("chmod")
+ .args(["+x", &dest.to_string_lossy()])
+ .status()?;
+ if !status.success() {
+ eprintln!("Failed to chmod {}", filename);
+ }
+ }
+ }
+
+ Ok(())
+}
diff --git a/mling/src/project_solver.rs b/mling/src/project_solver.rs
new file mode 100644
index 0000000..381bba2
--- /dev/null
+++ b/mling/src/project_solver.rs
@@ -0,0 +1,105 @@
+use std::path::PathBuf;
+
+use serde::Serialize;
+
+pub type BinaryName = String;
+pub type BinaryTargetPath = PathBuf;
+
+pub struct ProjectSolveResult {
+ pub target_dir: PathBuf,
+ pub workspace_root: PathBuf,
+ pub binaries: Vec<BinaryItem>,
+}
+
+#[derive(Debug, Serialize)]
+pub struct BinaryItem {
+ pub name: String,
+ pub path: PathBuf,
+}
+
+pub fn solve_current_dir() -> Result<ProjectSolveResult, std::io::Error> {
+ let current = std::env::current_dir()?;
+ solve(current)
+}
+
+pub fn solve(current: PathBuf) -> Result<ProjectSolveResult, std::io::Error> {
+ let (target_dir, workspace_root, binaries) = solve_inner(&current)?;
+ Ok(ProjectSolveResult {
+ target_dir,
+ workspace_root,
+ binaries,
+ })
+}
+
+fn solve_inner(current: &PathBuf) -> Result<(PathBuf, PathBuf, Vec<BinaryItem>), std::io::Error> {
+ let output = std::process::Command::new("cargo")
+ .arg("metadata")
+ .arg("--format-version")
+ .arg("1")
+ .current_dir(current)
+ .output()?;
+ if !output.status.success() {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ return Err(std::io::Error::new(
+ std::io::ErrorKind::Other,
+ format!("cargo metadata failed: {}", stderr),
+ ));
+ }
+ let metadata: serde_json::Value = serde_json::from_slice(&output.stdout)
+ .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
+
+ let workspace_root_str = metadata["workspace_root"].as_str().ok_or_else(|| {
+ std::io::Error::new(std::io::ErrorKind::InvalidData, "missing workspace_root")
+ })?;
+ let workspace_root = PathBuf::from(workspace_root_str);
+
+ let target_dir_str = metadata["target_directory"].as_str().ok_or_else(|| {
+ std::io::Error::new(std::io::ErrorKind::InvalidData, "missing target_directory")
+ })?;
+ let target_dir = PathBuf::from(target_dir_str);
+
+ let packages = metadata["packages"].as_array().ok_or_else(|| {
+ std::io::Error::new(std::io::ErrorKind::InvalidData, "missing packages array")
+ })?;
+
+ let mut binaries = Vec::new();
+ let cargo_toml_path = workspace_root.join("Cargo.toml");
+
+ // Find the package whose manifest_path matches workspace_root/Cargo.toml
+ for pkg in packages {
+ let manifest_path = pkg["manifest_path"].as_str().ok_or_else(|| {
+ std::io::Error::new(std::io::ErrorKind::InvalidData, "missing manifest_path")
+ })?;
+ let manifest_path_buf = PathBuf::from(manifest_path);
+ if manifest_path_buf == cargo_toml_path {
+ // Found the workspace root package
+ if let Some(targets) = pkg["targets"].as_array() {
+ for target in targets {
+ let kind = target["kind"].as_array();
+ let is_bin = kind
+ .map(|k| k.iter().any(|v| v.as_str() == Some("bin")))
+ .unwrap_or(false);
+ if is_bin {
+ let name = target["name"].as_str().ok_or_else(|| {
+ std::io::Error::new(
+ std::io::ErrorKind::InvalidData,
+ "missing target name",
+ )
+ })?;
+ let mut binary_path = target_dir.join("release").join(name);
+ if cfg!(target_os = "windows") {
+ binary_path.set_extension("exe");
+ }
+ binaries.push(BinaryItem {
+ name: name.to_string(),
+ path: binary_path,
+ });
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ Ok((target_dir, workspace_root, binaries))
+}
diff --git a/mling/tmpl/load.fish b/mling/tmpl/load.fish
new file mode 100644
index 0000000..19e1ef7
--- /dev/null
+++ b/mling/tmpl/load.fish
@@ -0,0 +1,75 @@
+#!/usr/bin/env fish
+
+# Save original directory
+set -l _load_original_dir $PWD
+
+# Switch to script directory
+set -l _load_dir (dirname (status filename))
+cd $_load_dir
+
+# Load mling.fish from path
+source .comp/mling_comp.fish
+
+# Add all namespace bin directories to PATH
+for _dir in */bin/
+ if test -d $_dir
+ set -gx PATH $PWD/$_dir $PATH
+ end
+end
+
+function _load_comp_script
+ if string match -q '*.fish' -- $argv[1]
+ source $argv[1] 2>/dev/null
+ end
+end
+
+# Iterate through all namespaces
+for _namespace in */
+ set _namespace (string trim -r -c / $_namespace)
+
+ # Skip if UNTRUSTED marked or no comp directory
+ test -f $_namespace/UNTRUSTED && continue
+ test -d $_namespace/comp || continue
+
+ # Find all loadable scripts in comp
+ set _scripts (find $_namespace/comp -maxdepth 1 -type f \( -name '*.sh' -o -name '*.zsh' -o -name '*.fish' \) 2>/dev/null)
+ test -z "$_scripts" && continue
+
+ # Count scripts
+ set _count (count $_scripts)
+
+ # If TRUSTED marked, load directly
+ if test -f $_namespace/TRUSTED
+ for _script in $_scripts
+ _load_comp_script $_script
+ end
+ continue
+ end
+
+ # Ask user
+ read -l -p 'printf "%s has %d completion script(s) to load, do you trust it? [Y/n] " $_namespace $_count' _answer
+ switch $_answer
+ case '' Y y
+ for _script in $_scripts
+ chmod +x $_script
+ end
+ touch $_namespace/TRUSTED
+
+ # Ask whether to load immediately
+ read -l -p 'printf "Load it immediately? [Y/n] "' _load_answer
+ switch $_load_answer
+ case '' Y y
+ for _script in $_scripts
+ _load_comp_script $_script
+ end
+ end
+ case '*'
+ touch $_namespace/UNTRUSTED
+ end
+end
+
+# Restore original directory
+cd $_load_original_dir
+
+# Clean up
+functions -e _load_comp_script
diff --git a/mling/tmpl/load.ps1 b/mling/tmpl/load.ps1
new file mode 100644
index 0000000..d666338
--- /dev/null
+++ b/mling/tmpl/load.ps1
@@ -0,0 +1,85 @@
+#!/usr/bin/env pwsh
+
+# Save original directory, restore after execution
+$_load_original_dir = Get-Location
+
+# Load completion script mling.ps1 from the current directory
+$mlingScript = Join-Path -Path (Get-Location) -ChildPath ".comp/mling_comp.ps1"
+if (Test-Path $mlingScript) {
+ . $mlingScript
+}
+
+# Change to script directory
+$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
+try {
+ Set-Location $scriptPath -ErrorAction Stop
+} catch {
+ Write-Error "load.ps1: failed to cd to script directory"
+ return
+}
+
+# Add bin directories from all namespaces to PATH
+Get-ChildItem -Directory -Path "*/bin/" | ForEach-Object {
+ $env:PATH = "$($_.FullName);$env:PATH"
+}
+
+# Helper function: execute script with appropriate shell
+function _load_script {
+ param([string]$script)
+ # Only handle .ps1 scripts
+ if ($script -like "*.ps1") {
+ & $script 2>$null
+ }
+}
+
+# Iterate over all namespaces
+Get-ChildItem -Directory | ForEach-Object {
+ $_namespace = $_.Name
+
+ # Skip if UNTRUSTED marker exists
+ if (Test-Path "$_namespace\UNTRUSTED") { return }
+
+ $_comp_dir = "$_namespace\comp"
+ if (-not (Test-Path $_comp_dir -PathType Container)) { return }
+
+ # Find all loadable scripts under comp
+ $_scripts = Get-ChildItem -Path $_comp_dir -File -Include "*.ps1" -ErrorAction SilentlyContinue
+ if (-not $_scripts) { return }
+
+ # Count scripts
+ $_count = ($_scripts | Measure-Object).Count
+
+ # If TRUSTED marker exists, load directly
+ if (Test-Path "$_namespace\TRUSTED") {
+ $_scripts | ForEach-Object {
+ _load_script $_.FullName
+ }
+ return
+ }
+
+ # No marker, ask user
+ $answer = Read-Host "'$_namespace' has $_count completion script(s) to load, do you trust it? [Y/n] "
+ if ($answer -eq "" -or $answer -match "^(y|yes)$") {
+ # Mark as TRUSTED
+ New-Item -ItemType File -Path "$_namespace\TRUSTED" -Force | Out-Null
+
+ # Ask whether to load immediately
+ $load_answer = Read-Host "Load it immediately? [Y/n] "
+ if ($load_answer -eq "" -or $load_answer -match "^(y|yes)$") {
+ $_scripts | ForEach-Object {
+ _load_script $_.FullName
+ }
+ }
+ } else {
+ New-Item -ItemType File -Path "$_namespace\UNTRUSTED" -Force | Out-Null
+ }
+}
+
+# Restore original working directory
+try {
+ Set-Location $_load_original_dir -ErrorAction Stop
+} catch {}
+
+# Cleanup
+Remove-Variable -Name _load_original_dir -ErrorAction SilentlyContinue
+Remove-Item Function:_load_script -ErrorAction SilentlyContinue
diff --git a/mling/tmpl/load.sh b/mling/tmpl/load.sh
new file mode 100644
index 0000000..0d48e95
--- /dev/null
+++ b/mling/tmpl/load.sh
@@ -0,0 +1,100 @@
+#!/usr/bin/env bash
+
+# Save original directory, restore after execution
+_load_original_dir="$PWD"
+
+cd "$(dirname "$0")" 2>/dev/null || {
+ echo "load.sh: failed to cd to script directory" >&2
+ return 1
+}
+
+# If in zsh, source mling.zsh, otherwise source mling.sh
+if [ -n "$ZSH_VERSION" ]; then
+ [ -f "./.comp/mling_comp.zsh" ] && source "./.comp/mling_comp.zsh"
+else
+ [ -f "./.comp/mling_comp.sh" ] && source "./.comp/mling_comp.sh"
+fi
+
+# Add bin directories from all namespaces to PATH
+for _dir in */bin/; do
+ [ -d "$_dir" ] && export PATH="$PWD/${_dir%/}:$PATH"
+done
+
+# Helper function: execute script with appropriate shell
+_load_script() {
+ local script="$1"
+ if [ -n "$ZSH_VERSION" ]; then
+ case "$script" in
+ *.zsh|*.sh)
+ source "$script" 2>/dev/null
+ ;;
+ esac
+ else
+ case "$script" in
+ *.sh)
+ bash "$script" 2>/dev/null
+ ;;
+ esac
+ fi
+}
+
+# Iterate over all namespaces
+for _namespace in */; do
+ _namespace="${_namespace%/}"
+ [ "$_namespace" = "*" ] && continue
+
+ # Skip if UNTRUSTED marker exists
+ [ -f "$_namespace/UNTRUSTED" ] && continue
+
+ _comp_dir="$_namespace/comp"
+ [ ! -d "$_comp_dir" ] && continue
+
+ # Find all loadable scripts under comp
+ _scripts=$(find "$_comp_dir" -maxdepth 1 -type f \( -name '*.sh' -o -name '*.zsh' -o -name '*.fish' \) 2>/dev/null)
+ [ -z "$_scripts" ] && continue
+
+ # Count scripts
+ _count=$(echo "$_scripts" | wc -l)
+
+ # If TRUSTED marker exists, load directly
+ if [ -f "$_namespace/TRUSTED" ]; then
+ echo "$_scripts" | while IFS= read -r _script; do
+ _load_script "$_script"
+ done
+ continue
+ fi
+
+ # No marker, ask user
+ printf "'%s' has %d completion script(s) to load, do you trust it? [Y/n] " "$_namespace" "$_count"
+ read _answer
+ case "$_answer" in
+ [Yy]*|"")
+ # Mark as TRUSTED and set executable permissions
+ echo "$_scripts" | while IFS= read -r _script; do
+ chmod +x "$_script"
+ done
+ touch "$_namespace/TRUSTED"
+
+ # Ask whether to load immediately
+ printf "Load it immediately? [Y/n] "
+ read _load_answer
+ case "$_load_answer" in
+ [Yy]*|"")
+ echo "$_scripts" | while IFS= read -r _script; do
+ _load_script "$_script"
+ done
+ ;;
+ esac
+ ;;
+ *)
+ touch "$_namespace/UNTRUSTED"
+ ;;
+ esac
+done
+
+# Restore original working directory
+cd "$_load_original_dir" 2>/dev/null || true
+
+# Cleanup
+unset -f _load_script
+unset _load_original_dir _dir _namespace _comp_dir _scripts _count _answer _load_answer _script