From ed0678eba30f233d4edab4a2bab27003079a2c30 Mon Sep 17 00:00:00 2001 From: Maksim Soltan Date: Tue, 14 Jul 2026 10:20:43 -0700 Subject: [PATCH 1/4] test: cover Cargo command forwarding Implements [[tasks/meta-cargo-command-forwarding]] --- Cargo.lock | 28 ++- README.md | 33 +++- docs/advanced_usage.md | 6 +- docs/architecture_overview.md | 9 +- tests/cargo.bats | 339 ++++++++++++++++++++++++++++++++++ tests/help.bats | 39 +++- 6 files changed, 429 insertions(+), 25 deletions(-) create mode 100644 tests/cargo.bats diff --git a/Cargo.lock b/Cargo.lock index b0f388b..9ffda6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -895,7 +895,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "loop_cli" -version = "0.2.20" +version = "0.2.22" dependencies = [ "anyhow", "clap", @@ -908,7 +908,7 @@ dependencies = [ [[package]] name = "loop_lib" -version = "0.2.20" +version = "0.2.22" dependencies = [ "anyhow", "colored", @@ -929,7 +929,7 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "meta" -version = "0.2.20" +version = "0.2.22" dependencies = [ "anyhow", "assert_cmd", @@ -958,7 +958,7 @@ dependencies = [ [[package]] name = "meta-mcp" -version = "0.2.20" +version = "0.2.22" dependencies = [ "anyhow", "chrono", @@ -971,7 +971,7 @@ dependencies = [ [[package]] name = "meta_core" -version = "0.2.20" +version = "0.2.22" dependencies = [ "anyhow", "chrono", @@ -985,7 +985,7 @@ dependencies = [ [[package]] name = "meta_git_cli" -version = "0.2.20" +version = "0.2.22" dependencies = [ "anyhow", "chrono", @@ -1011,7 +1011,7 @@ dependencies = [ [[package]] name = "meta_git_lib" -version = "0.2.20" +version = "0.2.22" dependencies = [ "anyhow", "chrono", @@ -1031,7 +1031,7 @@ dependencies = [ [[package]] name = "meta_plugin_protocol" -version = "0.2.20" +version = "0.2.22" dependencies = [ "anyhow", "env_logger", @@ -1043,7 +1043,7 @@ dependencies = [ [[package]] name = "meta_project_cli" -version = "0.2.20" +version = "0.2.22" dependencies = [ "anyhow", "colored", @@ -1058,7 +1058,7 @@ dependencies = [ [[package]] name = "meta_rust_cli" -version = "0.2.20" +version = "0.2.22" dependencies = [ "anyhow", "colored", @@ -1066,6 +1066,8 @@ dependencies = [ "meta_core", "meta_plugin_protocol", "serde_json", + "shell-escape", + "shell-words", "tempfile", ] @@ -1585,6 +1587,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-escape" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" + [[package]] name = "shell-words" version = "1.1.1" diff --git a/README.md b/README.md index dc0a018..b3feeb6 100644 --- a/README.md +++ b/README.md @@ -52,13 +52,13 @@ Most multi-repo tools solve the *cloning* problem. Meta solves the *working* pro This clones the meta repo and all child repositories defined in its `.meta` file. -2. **Run commands across all repos:** +2. **Run workspace commands:** ```bash meta git status meta git pull meta exec npm install - meta exec cargo test + meta cargo test ``` 3. **Filter by tags:** @@ -274,6 +274,33 @@ This enables impact analysis ("what breaks if I change shared-utils?"), topologi | `meta git snapshot list` | List snapshots | | `meta git snapshot restore ` | Restore workspace to snapshot | +### Rust/Cargo Plugin + +`meta cargo` is an explicit pass-through namespace. Meta selects directories that contain a +`Cargo.toml` and runs the requested command in each one; Cargo remains responsible for validating +built-in commands, aliases, and installed `cargo-*` extensions. `meta rust` is an alias that +produces the same canonical `cargo` command. + +```bash +meta cargo clean +meta cargo check --all-targets +meta cargo clippy --all-targets -- -D warnings +meta cargo nextest run +meta rust test +``` + +Put Meta options before the `cargo` or `rust` namespace. Tokens after the namespace belong to +Cargo by default: + +```bash +meta --include api --dry-run cargo check +meta --dry-run --sequential cargo clean --recursive +``` + +For compatibility, postfix `--recursive` is a Meta scope control for `build`, `test`, and `clean`. +Commands that own the flag keep it, so `meta cargo update --recursive` forwards `--recursive` to +Cargo. Meta never inspects or removes arguments after Cargo's `--` separator. + ### Plugin Management | Command | Description | @@ -349,7 +376,7 @@ Plugins are standalone executables that extend meta. They're discovered automati |--------|-------------| | `git` | Clone, status, update, commit, snapshots, SSH multiplexing | | `project` | Project health checks and sync | -| `rust` | Cargo build, test, and command passthrough | +| `rust` | Cargo namespace pass-through; `meta rust` aliases `meta cargo` | ### Writing Plugins diff --git a/docs/advanced_usage.md b/docs/advanced_usage.md index b5856c2..14ed544 100644 --- a/docs/advanced_usage.md +++ b/docs/advanced_usage.md @@ -245,8 +245,8 @@ meta exec --exclude legacy-app -- npm install # Tag filter + directory filter meta --tag backend git status --include api -# For non-plugin commands, use exec -meta exec --tag backend --include api -- cargo test +# Cargo commands use the Rust plugin namespace directly +meta --tag backend --include api cargo test ``` **Filter precedence:** @@ -338,7 +338,7 @@ By default, commands run sequentially with live output. Use `--parallel` for con ```bash meta git status --parallel -meta exec --parallel -- cargo test +meta --parallel cargo test ``` ### Parallel Mode Behavior diff --git a/docs/architecture_overview.md b/docs/architecture_overview.md index f42a176..36f4943 100644 --- a/docs/architecture_overview.md +++ b/docs/architecture_overview.md @@ -151,9 +151,10 @@ Project management plugin: ### meta_rust_cli Rust/Cargo plugin: -- Workspace-aware builds -- Test execution -- Cargo command passthrough +- Owns the explicit `cargo` and `rust` namespaces +- Selects Meta project directories that contain `Cargo.toml` +- Canonicalizes both namespaces to shell-safe `cargo` execution plans +- Leaves command validation, aliases, and installed `cargo-*` extensions to Cargo ## Plugin System @@ -211,7 +212,7 @@ When you run `meta `: 4. **Otherwise** - Show "unrecognized command" error **Important:** Bare commands like `meta npm install` are not supported. You must either: -- Use a plugin command: `meta git status`, `meta rust build` +- Use a plugin command: `meta git status`, `meta cargo check` (`meta rust check` is an alias) - Use explicit exec: `meta exec -- npm install` Special case commands (like `meta git clone`) are fully handled by the plugin rather than passed through to all repos. diff --git a/tests/cargo.bats b/tests/cargo.bats new file mode 100644 index 0000000..3411fa6 --- /dev/null +++ b/tests/cargo.bats @@ -0,0 +1,339 @@ +#!/usr/bin/env bats + +# Integration tests for the explicit `meta cargo` / `meta rust` namespaces. +# A controlled fake Cargo records cwd + argv so these tests never operate on +# real build artifacts. + +setup() { + META_BIN="$BATS_TEST_DIRNAME/../target/debug/meta" + META_RUST_BIN="$BATS_TEST_DIRNAME/../target/debug/meta-rust" + + if [ ! -f "$META_BIN" ] || [ ! -f "$META_RUST_BIN" ]; then + cargo build --workspace --quiet + fi + + TEST_DIR="$(mktemp -d)" + TEST_DIR="$(cd "$TEST_DIR" && pwd -P)" + CARGO_LOG="$TEST_DIR/cargo.log" + + mkdir -p \ + "$TEST_DIR/.meta/plugins" \ + "$TEST_DIR/bin" \ + "$TEST_DIR/rust-app/target" \ + "$TEST_DIR/docs" \ + "$TEST_DIR/nested/nested-rust/target" \ + "$TEST_DIR/nested/nested-docs" \ + "$TEST_DIR/target" + + cp "$META_RUST_BIN" "$TEST_DIR/.meta/plugins/meta-rust" + chmod +x "$TEST_DIR/.meta/plugins/meta-rust" + + cat > "$TEST_DIR/.meta.yaml" <<'YAML' +defaults: + parallel: false + +projects: + rust-app: + repo: git@github.com:org/rust-app.git + docs: + repo: git@github.com:org/docs.git + nested: + repo: git@github.com:org/nested.git + meta: true +YAML + + cat > "$TEST_DIR/nested/.meta.yaml" <<'YAML' +projects: + nested-rust: + repo: git@github.com:org/nested-rust.git + nested-docs: + repo: git@github.com:org/nested-docs.git +YAML + + cat > "$TEST_DIR/Cargo.toml" <<'TOML' +[package] +name = "fixture-root" +version = "0.0.0" +TOML + cat > "$TEST_DIR/rust-app/Cargo.toml" <<'TOML' +[package] +name = "fixture-rust-app" +version = "0.0.0" +TOML + cat > "$TEST_DIR/nested/nested-rust/Cargo.toml" <<'TOML' +[package] +name = "fixture-nested-rust" +version = "0.0.0" +TOML + + touch \ + "$TEST_DIR/target/keep" \ + "$TEST_DIR/rust-app/target/keep" \ + "$TEST_DIR/nested/nested-rust/target/keep" + + cat > "$TEST_DIR/bin/cargo" <<'SH' +#!/bin/sh +set -eu + +: "${CARGO_LOG:?CARGO_LOG must be set}" +{ + printf 'BEGIN\n' + printf 'cwd=<%s>\n' "$PWD" + printf 'argc=<%s>\n' "$#" + for arg in "$@"; do + printf 'arg=<%s>\n' "$arg" + done + printf 'END\n' +} >> "$CARGO_LOG" + +if [ "${1-}" = "definitely-not-a-command" ]; then + printf 'fake cargo: no such command: %s\n' "$1" >&2 + exit 101 +fi +SH + chmod +x "$TEST_DIR/bin/cargo" + + cd "$TEST_DIR" +} + +teardown() { + rm -rf "$TEST_DIR" +} + +run_with_fake_cargo() { + PATH="$TEST_DIR/bin:$PATH" CARGO_LOG="$CARGO_LOG" "$META_BIN" "$@" +} + +@test "cargo clean --recursive runs once in each Rust project" { + run run_with_fake_cargo cargo clean --recursive + + [ "$status" -eq 0 ] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 3 ] + [ "$(grep -F -x -c "cwd=<$TEST_DIR>" "$CARGO_LOG")" -eq 1 ] + [ "$(grep -F -x -c "cwd=<$TEST_DIR/rust-app>" "$CARGO_LOG")" -eq 1 ] + [ "$(grep -F -x -c "cwd=<$TEST_DIR/nested/nested-rust>" "$CARGO_LOG")" -eq 1 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 3 ] + ! grep -F -q 'arg=<--recursive>' "$CARGO_LOG" + ! grep -F -q "cwd=<$TEST_DIR/docs>" "$CARGO_LOG" + ! grep -F -q "cwd=<$TEST_DIR/nested>" "$CARGO_LOG" + ! grep -F -q "cwd=<$TEST_DIR/nested/nested-docs>" "$CARGO_LOG" +} + +@test "cargo clean recursive dry-run prints the exact plan without cleanup" { + run run_with_fake_cargo --dry-run cargo clean --recursive + + [ "$status" -eq 0 ] + [ ! -e "$CARGO_LOG" ] + [ "$(printf '%s\n' "$output" | grep -F -x -c ' cargo clean')" -eq 3 ] + [[ "$output" == *"$TEST_DIR"* ]] + [[ "$output" == *"$TEST_DIR/rust-app"* ]] + [[ "$output" == *"$TEST_DIR/nested/nested-rust"* ]] + [[ "$output" != *"$TEST_DIR/docs"* ]] + [ -f "$TEST_DIR/target/keep" ] + [ -f "$TEST_DIR/rust-app/target/keep" ] + [ -f "$TEST_DIR/nested/nested-rust/target/keep" ] +} + +@test "cargo update retains its recursive argument and does not recurse Meta scope" { + run run_with_fake_cargo cargo update --recursive + + [ "$status" -eq 0 ] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=<--recursive>' "$CARGO_LOG")" -eq 2 ] + ! grep -F -q "cwd=<$TEST_DIR/nested/nested-rust>" "$CARGO_LOG" +} + +@test "Cargo payload after the separator is never intercepted by Meta" { + run run_with_fake_cargo cargo test -- --recursive --help + + [ "$status" -eq 0 ] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'argc=<4>' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=<-->' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=<--recursive>' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=<--help>' "$CARGO_LOG")" -eq 2 ] + ! grep -F -q "cwd=<$TEST_DIR/nested/nested-rust>" "$CARGO_LOG" + [[ "$output" != *"Run any Cargo command across selected Rust projects"* ]] +} + +@test "build check clippy and installed Cargo extensions pass through" { + run run_with_fake_cargo cargo build --release + [ "$status" -eq 0 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=<--release>' "$CARGO_LOG")" -eq 2 ] + + rm -f "$CARGO_LOG" + run run_with_fake_cargo cargo check --all-targets + [ "$status" -eq 0 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=<--all-targets>' "$CARGO_LOG")" -eq 2 ] + + rm -f "$CARGO_LOG" + run run_with_fake_cargo cargo clippy --all-targets -- -D warnings + [ "$status" -eq 0 ] + [ "$(grep -F -x -c 'argc=<5>' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=<-D>' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] + + rm -f "$CARGO_LOG" + run run_with_fake_cargo cargo nextest run --recursive + [ "$status" -eq 0 ] + [ "$(grep -F -x -c 'argc=<3>' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=<--recursive>' "$CARGO_LOG")" -eq 2 ] + ! grep -F -q "cwd=<$TEST_DIR/nested/nested-rust>" "$CARGO_LOG" +} + +@test "Meta controls go before the namespace and postfix controls belong to Cargo" { + run run_with_fake_cargo --verbose --include rust-app cargo check + + [ "$status" -eq 0 ] + [[ "$output" == *"Verbose mode enabled"* ]] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 1 ] + [ "$(grep -F -x -c 'argc=<1>' "$CARGO_LOG")" -eq 1 ] + grep -F -x -q 'arg=' "$CARGO_LOG" + + rm -f "$CARGO_LOG" + run run_with_fake_cargo --include rust-app cargo check --verbose --dry-run + + [ "$status" -eq 0 ] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 1 ] + [ "$(grep -F -x -c 'argc=<3>' "$CARGO_LOG")" -eq 1 ] + grep -F -x -q 'arg=' "$CARGO_LOG" + grep -F -x -q 'arg=<--verbose>' "$CARGO_LOG" + grep -F -x -q 'arg=<--dry-run>' "$CARGO_LOG" +} + +@test "rust is behaviorally equivalent to the canonical cargo namespace" { + run run_with_fake_cargo cargo check --message-format json + [ "$status" -eq 0 ] + cp "$CARGO_LOG" "$TEST_DIR/cargo-namespace.log" + + rm -f "$CARGO_LOG" + run run_with_fake_cargo rust check --message-format json + [ "$status" -eq 0 ] + + cmp -s "$TEST_DIR/cargo-namespace.log" "$CARGO_LOG" +} + +@test "Cargo argument values remain data across the shell plan boundary" { + command_substitution="\$(touch \"$TEST_DIR/command-substitution-ran\")" + semicolon="; touch \"$TEST_DIR/semicolon-ran\"" + + run run_with_fake_cargo \ + --include rust-app \ + --sequential \ + cargo clippy --all-targets -- -D warnings \ + "value with spaces" \ + "$command_substitution" \ + "$semicolon" \ + "left&right" \ + "it's data" + + [ "$status" -eq 0 ] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 1 ] + [ "$(grep -F -x -c 'argc=<10>' "$CARGO_LOG")" -eq 1 ] + grep -F -x -q 'arg=' "$CARGO_LOG" + grep -F -x -q "arg=<$command_substitution>" "$CARGO_LOG" + grep -F -x -q "arg=<$semicolon>" "$CARGO_LOG" + grep -F -x -q 'arg=' "$CARGO_LOG" + grep -F -x -q "arg=" "$CARGO_LOG" + [ ! -e "$TEST_DIR/command-substitution-ran" ] + [ ! -e "$TEST_DIR/semicolon-ran" ] +} + +@test "Cargo diagnoses invalid namespaced commands while top-level typos stay rejected" { + run run_with_fake_cargo cargo definitely-not-a-command + + [ "$status" -ne 0 ] + [[ "$output" == *"fake cargo: no such command"* ]] + [[ "$output" != *"unrecognized command"* ]] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 2 ] + + rm -f "$CARGO_LOG" + run run_with_fake_cargo carg clean + + [ "$status" -eq 1 ] + [[ "$output" == *"unrecognized command 'carg'"* ]] + [ ! -e "$CARGO_LOG" ] +} + +@test "an empty Rust scope reports a clear result without invoking Cargo" { + rm -f "$TEST_DIR/Cargo.toml" "$TEST_DIR/rust-app/Cargo.toml" + + run run_with_fake_cargo cargo check + + [ "$status" -eq 0 ] + [[ "$output" == *"No Rust projects found"* ]] + [ ! -e "$CARGO_LOG" ] +} + +@test "an include or exclude selection with no Rust projects reports a clear result" { + run run_with_fake_cargo --include docs cargo check + + [ "$status" -eq 0 ] + [[ "$output" == *"No Rust projects found"* ]] + [ ! -e "$CARGO_LOG" ] + + run run_with_fake_cargo --exclude "$TEST_DIR" cargo check + + [ "$status" -eq 0 ] + [[ "$output" == *"No Rust projects found"* ]] + [ ! -e "$CARGO_LOG" ] +} + +@test "a worktree without Meta config still uses safe Cargo plugin dispatch" { + rm -f "$TEST_DIR/.meta.yaml" + mkdir -p \ + "$TEST_DIR/.worktrees/no-config/docs" \ + "$TEST_DIR/.worktrees/no-config/rust-app" + + printf 'gitdir: %s\n' \ + "$TEST_DIR/source-docs/.git/worktrees/no-config-docs" \ + > "$TEST_DIR/.worktrees/no-config/docs/.git" + printf 'gitdir: %s\n' \ + "$TEST_DIR/source-rust/.git/worktrees/no-config-rust" \ + > "$TEST_DIR/.worktrees/no-config/rust-app/.git" + cat > "$TEST_DIR/.worktrees/no-config/rust-app/Cargo.toml" <<'TOML' +[package] +name = "worktree-rust-app" +version = "0.0.0" +TOML + + cat > "$TEST_DIR/bin/carg" <<'SH' +#!/bin/sh +touch "${CARGO_LOG:?}.top-level-typo-ran" +SH + chmod +x "$TEST_DIR/bin/carg" + + cd "$TEST_DIR/.worktrees/no-config/rust-app" + dangerous='quoted"&still-data' + run run_with_fake_cargo cargo check "value with spaces" "$dangerous" + + [ "$status" -eq 0 ] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 1 ] + grep -F -x -q "cwd=<$TEST_DIR/.worktrees/no-config/rust-app>" "$CARGO_LOG" + grep -F -x -q 'arg=' "$CARGO_LOG" + grep -F -x -q 'arg=' "$CARGO_LOG" + grep -F -x -q "arg=<$dangerous>" "$CARGO_LOG" + ! grep -F -q "cwd=<$TEST_DIR/.worktrees/no-config/docs>" "$CARGO_LOG" + + cp "$CARGO_LOG" "$TEST_DIR/worktree-cargo.log" + rm -f "$CARGO_LOG" + run run_with_fake_cargo rust check "value with spaces" "$dangerous" + + [ "$status" -eq 0 ] + cmp -s "$TEST_DIR/worktree-cargo.log" "$CARGO_LOG" + + rm -f "$CARGO_LOG" + run run_with_fake_cargo carg clean + + [ "$status" -eq 1 ] + [[ "$output" == *"unrecognized command 'carg'"* ]] + [ ! -e "$CARGO_LOG.top-level-typo-ran" ] + [ ! -e "$CARGO_LOG" ] +} diff --git a/tests/help.bats b/tests/help.bats index c4ee0db..c37cdc2 100644 --- a/tests/help.bats +++ b/tests/help.bats @@ -50,6 +50,8 @@ teardown() { [ "$output" = "$help_flag" ] [[ "$output" == *"git"* ]] [[ "$output" == *"project"* ]] + [[ "$output" == *"cargo"* ]] + [[ "$output" == *"rust"* ]] } @test "built-in command help does not execute commands" { @@ -101,16 +103,43 @@ teardown() { [[ "$output" == *"Current scope:"* ]] } -@test "rust and cargo help use the simple plugin help without side effects" { +@test "rust and cargo namespace help is accurate and side-effect free" { cd "$NO_CONFIG_DIR" - for command in "cargo --help" "cargo build --help" "rust --help" "rust build --help"; do - run "$META_BIN" $command + # A bare namespace or help request must not parse project configuration. + # This malformed file makes accidental dispatch/config discovery observable. + printf 'projects: [\n' > "$NO_CONFIG_DIR/.meta.yaml" + + mkdir -p "$NO_CONFIG_DIR/bin" + cat > "$NO_CONFIG_DIR/bin/cargo" <<'SH' +#!/bin/sh +printf 'cargo executed\n' >> "$CARGO_MARKER" +SH + chmod +x "$NO_CONFIG_DIR/bin/cargo" + + for command in \ + "cargo" \ + "cargo --help" \ + "cargo clean --help" \ + "rust" \ + "rust --help" \ + "rust nextest --help" + do + run env \ + PATH="$NO_CONFIG_DIR/bin:$PATH" \ + CARGO_MARKER="$NO_CONFIG_DIR/cargo-ran" \ + "$META_BIN" $command [ "$status" -eq 0 ] - [[ "$output" == *"meta cargo "* ]] - [[ "$output" == *"Build all Rust projects"* ]] + [[ "$output" == *"Run any Cargo command across selected Rust projects"* ]] + [[ "$output" == *"cargo "* ]] + [[ "$output" == *"cargo clean"* ]] + [[ "$output" == *"cargo check"* ]] + [[ "$output" == *"cargo clippy"* ]] + [[ "$output" == *"cargo nextest"* ]] + [[ "$output" == *"Cargo validates"* ]] [[ "$output" != *"Could not find meta config"* ]] [[ "$output" != *"No Rust projects found"* ]] + [ ! -e "$NO_CONFIG_DIR/cargo-ran" ] done } From 8127c2d5d86cfc6892ccb27de424c448c170b09b Mon Sep 17 00:00:00 2001 From: Maksim Soltan Date: Tue, 14 Jul 2026 11:37:27 -0700 Subject: [PATCH 2/4] test: cover reviewed Cargo forwarding boundaries Implements [[tasks/meta-cargo-command-forwarding]] Related: [[tasks/harmony-677]] --- README.md | 7 ++++++- tests/cargo.bats | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b3feeb6..8220ce7 100644 --- a/README.md +++ b/README.md @@ -299,7 +299,12 @@ meta --dry-run --sequential cargo clean --recursive For compatibility, postfix `--recursive` is a Meta scope control for `build`, `test`, and `clean`. Commands that own the flag keep it, so `meta cargo update --recursive` forwards `--recursive` to -Cargo. Meta never inspects or removes arguments after Cargo's `--` separator. +Cargo. + +Meta also intercepts `-h` and `--help` before Cargo's `--` separator and displays Meta's +side-effect-free Cargo namespace help. For command-specific Cargo help, run +`cargo help ` directly (for example, `cargo help check`). Meta never inspects or removes +arguments after Cargo's `--` separator. ### Plugin Management diff --git a/tests/cargo.bats b/tests/cargo.bats index 3411fa6..aa42e71 100644 --- a/tests/cargo.bats +++ b/tests/cargo.bats @@ -15,10 +15,13 @@ setup() { TEST_DIR="$(mktemp -d)" TEST_DIR="$(cd "$TEST_DIR" && pwd -P)" CARGO_LOG="$TEST_DIR/cargo.log" + HOME="$TEST_DIR/home" + export HOME mkdir -p \ "$TEST_DIR/.meta/plugins" \ "$TEST_DIR/bin" \ + "$HOME" \ "$TEST_DIR/rust-app/target" \ "$TEST_DIR/docs" \ "$TEST_DIR/nested/nested-rust/target" \ @@ -119,6 +122,37 @@ run_with_fake_cargo() { ! grep -F -q "cwd=<$TEST_DIR/nested/nested-docs>" "$CARGO_LOG" } +@test "Cargo and Rust namespace plans ignore Loop aliases" { + cat > "$TEST_DIR/.looprc" <<'JSON' +{"aliases":{"cargo":"true"}} +JSON + + run run_with_fake_cargo cargo clean --recursive + + [ "$status" -eq 0 ] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 3 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 3 ] + + rm -f "$CARGO_LOG" + run run_with_fake_cargo rust clean --recursive + + [ "$status" -eq 0 ] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 3 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 3 ] +} + +@test "leading Cargo global options preserve recursive compatibility" { + run run_with_fake_cargo cargo --locked clean --recursive + + [ "$status" -eq 0 ] + [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 3 ] + [ "$(grep -F -x -c 'arg=<--locked>' "$CARGO_LOG")" -eq 3 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 3 ] + ! grep -F -q 'arg=<--recursive>' "$CARGO_LOG" + grep -F -x -q "cwd=<$TEST_DIR/nested/nested-rust>" "$CARGO_LOG" + ! grep -F -q "cwd=<$TEST_DIR/docs>" "$CARGO_LOG" +} + @test "cargo clean recursive dry-run prints the exact plan without cleanup" { run run_with_fake_cargo --dry-run cargo clean --recursive From 9436332ea0112d3dc9a4daae7e96892106e7bc33 Mon Sep 17 00:00:00 2001 From: Maksim Soltan Date: Tue, 14 Jul 2026 14:43:08 -0700 Subject: [PATCH 3/4] docs: align Cargo forwarding with plugin ownership [[tasks/meta-cargo-command-forwarding]] --- README.md | 16 +++++++--------- tests/cargo.bats | 41 +++++------------------------------------ 2 files changed, 12 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 8220ce7..013c8f6 100644 --- a/README.md +++ b/README.md @@ -276,7 +276,7 @@ This enables impact analysis ("what breaks if I change shared-utils?"), topologi ### Rust/Cargo Plugin -`meta cargo` is an explicit pass-through namespace. Meta selects directories that contain a +`meta cargo` is an explicit plugin namespace. The Rust plugin selects directories that contain a `Cargo.toml` and runs the requested command in each one; Cargo remains responsible for validating built-in commands, aliases, and installed `cargo-*` extensions. `meta rust` is an alias that produces the same canonical `cargo` command. @@ -289,20 +289,18 @@ meta cargo nextest run meta rust test ``` -Put Meta options before the `cargo` or `rust` namespace. Tokens after the namespace belong to -Cargo by default: +Put Meta options before the `cargo` or `rust` namespace: ```bash meta --include api --dry-run cargo check meta --dry-run --sequential cargo clean --recursive ``` -For compatibility, postfix `--recursive` is a Meta scope control for `build`, `test`, and `clean`. -Commands that own the flag keep it, so `meta cargo update --recursive` forwards `--recursive` to -Cargo. +In this first pass, `--recursive` before Cargo's `--` remains a Meta scope control, including +postfix usage such as `meta cargo clean --recursive`. -Meta also intercepts `-h` and `--help` before Cargo's `--` separator and displays Meta's -side-effect-free Cargo namespace help. For command-specific Cargo help, run +The Rust plugin treats `-h` and `--help` before Cargo's `--` separator as side-effect-free +namespace help. For command-specific Cargo help, run `cargo help ` directly (for example, `cargo help check`). Meta never inspects or removes arguments after Cargo's `--` separator. @@ -381,7 +379,7 @@ Plugins are standalone executables that extend meta. They're discovered automati |--------|-------------| | `git` | Clone, status, update, commit, snapshots, SSH multiplexing | | `project` | Project health checks and sync | -| `rust` | Cargo namespace pass-through; `meta rust` aliases `meta cargo` | +| `rust` | Cargo commands across Rust projects; `meta rust` aliases `meta cargo` | ### Writing Plugins diff --git a/tests/cargo.bats b/tests/cargo.bats index aa42e71..c204e69 100644 --- a/tests/cargo.bats +++ b/tests/cargo.bats @@ -141,18 +141,6 @@ JSON [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 3 ] } -@test "leading Cargo global options preserve recursive compatibility" { - run run_with_fake_cargo cargo --locked clean --recursive - - [ "$status" -eq 0 ] - [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 3 ] - [ "$(grep -F -x -c 'arg=<--locked>' "$CARGO_LOG")" -eq 3 ] - [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 3 ] - ! grep -F -q 'arg=<--recursive>' "$CARGO_LOG" - grep -F -x -q "cwd=<$TEST_DIR/nested/nested-rust>" "$CARGO_LOG" - ! grep -F -q "cwd=<$TEST_DIR/docs>" "$CARGO_LOG" -} - @test "cargo clean recursive dry-run prints the exact plan without cleanup" { run run_with_fake_cargo --dry-run cargo clean --recursive @@ -168,16 +156,6 @@ JSON [ -f "$TEST_DIR/nested/nested-rust/target/keep" ] } -@test "cargo update retains its recursive argument and does not recurse Meta scope" { - run run_with_fake_cargo cargo update --recursive - - [ "$status" -eq 0 ] - [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 2 ] - [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] - [ "$(grep -F -x -c 'arg=<--recursive>' "$CARGO_LOG")" -eq 2 ] - ! grep -F -q "cwd=<$TEST_DIR/nested/nested-rust>" "$CARGO_LOG" -} - @test "Cargo payload after the separator is never intercepted by Meta" { run run_with_fake_cargo cargo test -- --recursive --help @@ -213,16 +191,17 @@ JSON [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] rm -f "$CARGO_LOG" - run run_with_fake_cargo cargo nextest run --recursive + run run_with_fake_cargo cargo nextest run --profile ci [ "$status" -eq 0 ] - [ "$(grep -F -x -c 'argc=<3>' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'argc=<4>' "$CARGO_LOG")" -eq 2 ] [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] - [ "$(grep -F -x -c 'arg=<--recursive>' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=<--profile>' "$CARGO_LOG")" -eq 2 ] + [ "$(grep -F -x -c 'arg=' "$CARGO_LOG")" -eq 2 ] ! grep -F -q "cwd=<$TEST_DIR/nested/nested-rust>" "$CARGO_LOG" } -@test "Meta controls go before the namespace and postfix controls belong to Cargo" { +@test "Meta controls before the namespace filter plugin execution" { run run_with_fake_cargo --verbose --include rust-app cargo check [ "$status" -eq 0 ] @@ -230,16 +209,6 @@ JSON [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 1 ] [ "$(grep -F -x -c 'argc=<1>' "$CARGO_LOG")" -eq 1 ] grep -F -x -q 'arg=' "$CARGO_LOG" - - rm -f "$CARGO_LOG" - run run_with_fake_cargo --include rust-app cargo check --verbose --dry-run - - [ "$status" -eq 0 ] - [ "$(grep -c '^BEGIN$' "$CARGO_LOG")" -eq 1 ] - [ "$(grep -F -x -c 'argc=<3>' "$CARGO_LOG")" -eq 1 ] - grep -F -x -q 'arg=' "$CARGO_LOG" - grep -F -x -q 'arg=<--verbose>' "$CARGO_LOG" - grep -F -x -q 'arg=<--dry-run>' "$CARGO_LOG" } @test "rust is behaviorally equivalent to the canonical cargo namespace" { From c858f618aa085284dcf9ae0cbffd6fbff0b6273d Mon Sep 17 00:00:00 2001 From: Maksim Soltan Date: Tue, 14 Jul 2026 15:03:05 -0700 Subject: [PATCH 4/4] fix: ship Rust plugin in release artifacts [[tasks/meta-cargo-command-forwarding]] --- .github/workflows/ci.yml | 3 ++- .github/workflows/release.yml | 8 ++++++++ distribution/homebrew/meta-cli.rb | 1 + install.ps1 | 2 +- install.sh | 2 +- tests/cargo.bats | 2 +- tests/release_packaging.bats | 22 ++++++++++++++++++++++ 7 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 tests/release_packaging.bats diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c3ec57..cf904ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,9 +87,10 @@ jobs: - name: Verify binaries run: | - ls -lh target/debug/meta target/debug/meta-git + ls -lh target/debug/meta target/debug/meta-git target/debug/meta-rust ./target/debug/meta --version ./target/debug/meta-git --meta-plugin-info | jq .name + ./target/debug/meta-rust --meta-plugin-info | jq .name shell: bash - name: Install bats diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 26f3206..96fbddd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,6 +82,7 @@ jobs: cargo build --release --target ${{ matrix.target }} -p meta cargo build --release --target ${{ matrix.target }} -p meta_git_cli cargo build --release --target ${{ matrix.target }} -p meta_project_cli + cargo build --release --target ${{ matrix.target }} -p meta_rust_cli cargo build --release --target ${{ matrix.target }} -p meta-mcp cargo build --release --target ${{ matrix.target }} -p loop_cli @@ -92,6 +93,7 @@ jobs: cp target/${{ matrix.target }}/release/${{ matrix.binary_name }} dist/ cp target/${{ matrix.target }}/release/meta-git dist/ cp target/${{ matrix.target }}/release/meta-project dist/ + cp target/${{ matrix.target }}/release/meta-rust dist/ cp target/${{ matrix.target }}/release/meta-mcp dist/ cp target/${{ matrix.target }}/release/loop dist/ cd dist @@ -106,6 +108,7 @@ jobs: copy target\${{ matrix.target }}\release\${{ matrix.binary_name }} dist\ copy target\${{ matrix.target }}\release\meta-git.exe dist\ copy target\${{ matrix.target }}\release\meta-project.exe dist\ + copy target\${{ matrix.target }}\release\meta-rust.exe dist\ copy target\${{ matrix.target }}\release\meta-mcp.exe dist\ copy target\${{ matrix.target }}\release\loop.exe dist\ cd dist @@ -283,6 +286,10 @@ jobs: run: cargo publish -p meta --token ${{ secrets.CARGO_REGISTRY_TOKEN }} continue-on-error: true + - name: Publish meta_rust_cli + run: cargo publish -p meta_rust_cli --token ${{ secrets.CARGO_REGISTRY_TOKEN }} + continue-on-error: true + # Tier 3: Depends on meta - name: Publish meta_git_lib run: cargo publish -p meta_git_lib --token ${{ secrets.CARGO_REGISTRY_TOKEN }} @@ -374,6 +381,7 @@ jobs: bin.install "meta" bin.install "meta-git" bin.install "meta-project" + bin.install "meta-rust" bin.install "meta-mcp" bin.install "loop" end diff --git a/distribution/homebrew/meta-cli.rb b/distribution/homebrew/meta-cli.rb index 9ca69ef..ebb9951 100644 --- a/distribution/homebrew/meta-cli.rb +++ b/distribution/homebrew/meta-cli.rb @@ -33,6 +33,7 @@ def install bin.install "meta" bin.install "meta-git" bin.install "meta-project" + bin.install "meta-rust" bin.install "meta-mcp" bin.install "loop" end diff --git a/install.ps1 b/install.ps1 index a3ae1a5..afa0245 100644 --- a/install.ps1 +++ b/install.ps1 @@ -79,7 +79,7 @@ function Install-Meta { # Install binaries Write-Info "Installing to $InstallDir..." - $expectedBinaries = @("meta.exe", "meta-git.exe", "meta-project.exe", "meta-mcp.exe", "loop.exe") + $expectedBinaries = @("meta.exe", "meta-git.exe", "meta-project.exe", "meta-rust.exe", "meta-mcp.exe", "loop.exe") foreach ($binary in $expectedBinaries) { $binaryPath = Join-Path $tempDir $binary if (Test-Path $binaryPath) { diff --git a/install.sh b/install.sh index 2f710eb..8cceeb7 100755 --- a/install.sh +++ b/install.sh @@ -86,7 +86,7 @@ install_meta() { # Install binaries info "Installing to ${INSTALL_DIR}..." - local expected_binaries=("meta" "meta-git" "meta-project" "meta-mcp" "loop") + local expected_binaries=("meta" "meta-git" "meta-project" "meta-rust" "meta-mcp" "loop") for binary_name in "${expected_binaries[@]}"; do local binary="$tmp_dir/$binary_name" if [ -f "$binary" ]; then diff --git a/tests/cargo.bats b/tests/cargo.bats index c204e69..da9a822 100644 --- a/tests/cargo.bats +++ b/tests/cargo.bats @@ -104,7 +104,7 @@ teardown() { } run_with_fake_cargo() { - PATH="$TEST_DIR/bin:$PATH" CARGO_LOG="$CARGO_LOG" "$META_BIN" "$@" + SHELL=/bin/sh PATH="$TEST_DIR/bin:$PATH" CARGO_LOG="$CARGO_LOG" "$META_BIN" "$@" } @test "cargo clean --recursive runs once in each Rust project" { diff --git a/tests/release_packaging.bats b/tests/release_packaging.bats new file mode 100644 index 0000000..e5356f3 --- /dev/null +++ b/tests/release_packaging.bats @@ -0,0 +1,22 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$BATS_TEST_DIRNAME/.." +} + +@test "release artifacts include the Rust plugin on every platform" { + local workflow="$REPO_ROOT/.github/workflows/release.yml" + + grep -F -q 'cargo build --release --target ${{ matrix.target }} -p meta_rust_cli' "$workflow" + grep -F -q 'cp target/${{ matrix.target }}/release/meta-rust dist/' "$workflow" + grep -F -q 'copy target\${{ matrix.target }}\release\meta-rust.exe dist\' "$workflow" + grep -F -q 'cargo publish -p meta_rust_cli' "$workflow" + grep -F -q 'bin.install "meta-rust"' "$workflow" +} + +@test "supported installers install the Rust plugin" { + grep -F -q '"meta-rust"' "$REPO_ROOT/install.sh" + grep -F -q '"meta-rust.exe"' "$REPO_ROOT/install.ps1" + grep -F -q 'bin.install "meta-rust"' "$REPO_ROOT/distribution/homebrew/meta-cli.rb" + grep -F -q './target/debug/meta-rust --meta-plugin-info' "$REPO_ROOT/.github/workflows/ci.yml" +}