Repository navigation
mux: plugin manager — install sidebar plugins from git repos - #7701
Conversation
cmux-mux plugin install <git-url> shallow-clones into ~/.local/share/cmux/mux-plugins/<name> (XDG-aware), validates cmux-plugin.toml (kind, sanitized name, run command), runs the optional build step, and verifies the executable. plugin list/use/update/remove manage selection; use writes sidebar.plugin argv+cwd into mux.json via an atomic read-modify-write that preserves unknown keys, and best-effort reload-configs a running server. CLI-only (no protocol changes); e2e test installs from a local file:// git fixture. Docs in spec/cli.md, spec/plugins.md, docs/configuration.md.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds sidebar plugin management to ChangesSidebar Plugin Manager
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 error, 1 warning)
✅ Passed checks (23 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds local sidebar plugin management to
Confidence Score: 4/5This is close, but the config overwrite should be fixed before merging.
mux/crates/mux-tui/src/config.rs Important Files Changed
Reviews (4): Last reviewed commit: "mux: create the fixture source dir (remo..." | Re-trigger Greptile |
| fs::create_dir_all(&root)?; | ||
| let temp_dir = root.join(format!(".install-{}-{}", std::process::id(), now_nanos())); | ||
| let clone_result = | ||
| run_git(["clone", "--depth", "1", positionals[1].as_str()], Some(&temp_dir), None); |
There was a problem hiding this comment.
Plugin URL Runs Unrestricted Code
plugin install passes any supplied repository URL straight to git clone, then the same install/update flow can execute that repo's [build].command and later persist its run command into mux.json. A copied or mistyped URL can therefore run code from an unapproved source during install and make the mux server launch it after plugin use; the installer needs a compulsory trusted-source gate or explicit confirmation before clone/build/use.
| current_dir: Option<&Path>, | ||
| ) -> anyhow::Result<()> { | ||
| let mut command = Command::new("git"); | ||
| command.args(["-c", "protocol.file.allow=always"]).args(args); |
There was a problem hiding this comment.
run_git enables protocol.file.allow=always for every plugin git operation, including URLs passed by end users. That removes git's local-file protection for the new installer, so a local path or file:// remote can be cloned and its manifest build command run without a separate local-source opt-in.
| if !sidebar.is_object() { | ||
| *sidebar = json!({}); |
There was a problem hiding this comment.
When mux.json already contains a non-object sidebar value, plugin use silently replaces that value with {} before adding sidebar.plugin. The PR promises to preserve unrelated config state, but this path destroys the existing sidebar value instead of rejecting the incompatible shape or leaving it untouched.
|
|
||
| fn installed_plugins() -> anyhow::Result<Vec<InstalledPlugin>> { | ||
| let root = install_root()?; | ||
| let selected = selected_plugin_cwd()?; |
There was a problem hiding this comment.
plugin list calls selected_plugin_cwd()? before reading installed plugin directories, so a malformed mux.json makes listing fail with a parse error even though installed plugins are still on disk. The TUI loader ignores invalid config and falls back to defaults, but this new local command cannot list or inspect plugins until the config is manually repaired.
| if !dir.exists() { | ||
| return Err(ManagerError::Failure(anyhow::anyhow!("plugin {name:?} is not installed"))); | ||
| } | ||
| let selected = selected_plugin_cwd()?.is_some_and(|cwd| same_path(&cwd, &dir)); |
There was a problem hiding this comment.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
mux/crates/mux-tui/src/cli.rs (1)
504-547: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick winRename the socket-mode flag before the connection binding
streamis bound twice here: theVerbKind::Socketbool is shadowed bylet mut stream = transport::connect(...), so the laterif streamreads the connection handle instead of the flag and this function no longer compiles. Rename the boolean (for exampleis_stream) and use that for the timeout/dispatch branch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mux/crates/mux-tui/src/cli.rs` around lines 504 - 547, The socket-mode boolean from VerbKind::Socket is being shadowed by the transport::connect result in cli.rs, so the later if stream branch is using the connection handle instead of the mode flag and breaks compilation. Rename the initial flag binding in the match (for example to is_stream) and update the timeout and final dispatch logic in this function to use that renamed boolean while keeping the connected socket variable as the stream handle.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mux/crates/mux-tui/src/plugin_manager.rs`:
- Around line 199-217: The update_command flow re-resolves the plugin after a
pull but never refreshes the persisted selected-plugin config, so mux.json can
keep stale sidebar.plugin data. Add the same selected-plugin check used in
remove_command: after read_manifest/run_build_if_needed/resolved_run_command, if
the updated plugin is currently selected, rewrite the stored run command and cwd
in mux.json to match the newly resolved command from update_command. Use the
existing use_command/remove_command logic as the reference for locating the
selection and config update path.
- Around line 384-402: The run_git helper is forcing git to allow file://
protocols too broadly by using protocol.file.allow=always. Update the Command
setup in run_git to use the user policy instead, since this helper is only used
for user-triggered git clone/pull flows. Keep the change localized to run_git
and only consider broader allow=always handling in any separate fixture-specific
path if needed.
In `@mux/crates/mux-tui/tests/cli.rs`:
- Around line 374-378: The CLI test is comparing non-canonical expected paths
against values produced by plugin_manager::use_command via
canonical_path/fs::canonicalize, which will differ on macOS. Update the
assertions in the cli.rs test to canonicalize the expected installed_dir and
derived bin/sidebar command path before comparing, so the
written["sidebar"]["plugin"]["cwd"] and command[0] checks match the
canonicalized paths used by the implementation.
---
Outside diff comments:
In `@mux/crates/mux-tui/src/cli.rs`:
- Around line 504-547: The socket-mode boolean from VerbKind::Socket is being
shadowed by the transport::connect result in cli.rs, so the later if stream
branch is using the connection handle instead of the mode flag and breaks
compilation. Rename the initial flag binding in the match (for example to
is_stream) and update the timeout and final dispatch logic in this function to
use that renamed boolean while keeping the connected socket variable as the
stream handle.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 57222e27-3826-46d0-8ab6-2dc0285b999b
⛔ Files ignored due to path filters (1)
mux/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (10)
mux/Cargo.tomlmux/crates/mux-tui/Cargo.tomlmux/crates/mux-tui/src/cli.rsmux/crates/mux-tui/src/config.rsmux/crates/mux-tui/src/main.rsmux/crates/mux-tui/src/plugin_manager.rsmux/crates/mux-tui/tests/cli.rsmux/docs/configuration.mdmux/spec/cli.mdmux/spec/plugins.md
| fn update_command(positionals: &[String], options: &CliOptions) -> Result<(), ManagerError> { | ||
| reject_plugin_flags(options, false, false, false)?; | ||
| if positionals.len() != 2 { | ||
| return Err(ManagerError::Usage("usage: cmux-mux plugin update <name>".to_string())); | ||
| } | ||
| let name = &positionals[1]; | ||
| validate_plugin_name(name)?; | ||
| let dir = install_root()?.join(name); | ||
| if !dir.is_dir() { | ||
| return Err(ManagerError::Failure(anyhow::anyhow!("plugin {name:?} is not installed"))); | ||
| } | ||
| run_git(["pull", "--ff-only"], None, Some(&dir))?; | ||
| let manifest = read_manifest(&dir)?; | ||
| run_build_if_needed(&manifest, &dir)?; | ||
| let command = resolved_run_command(&manifest, &dir)?; | ||
| verify_executable(&command[0])?; | ||
| println!("updated {name}{}", version_suffix(&manifest)); | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
update leaves a stale sidebar.plugin for the currently-selected plugin.
use_command persists the resolved run command + cwd into mux.json, and remove_command clears it when the removed plugin is selected. But update re-resolves and verifies the command without rewriting the config. If the pulled version changes run.command (e.g. new args or binary path), the selected plugin in mux.json keeps the old argv until the user manually re-runs plugin use, so a reload picks up stale config. Mirror the selection check already used in remove_command.
🔧 Refresh config when the updated plugin is selected
let command = resolved_run_command(&manifest, &dir)?;
verify_executable(&command[0])?;
+ if selected_plugin_cwd()?.is_some_and(|cwd| same_path(&cwd, &dir)) {
+ let cwd = canonical_path(&dir)?;
+ let path = config::write_sidebar_plugin(Some(&SidebarPluginConfig {
+ command,
+ cwd: Some(cwd.display().to_string()),
+ }))?;
+ println!("refreshed selected sidebar.plugin in {}", path.display());
+ report_reload_config(options);
+ }
println!("updated {name}{}", version_suffix(&manifest));
Ok(())📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn update_command(positionals: &[String], options: &CliOptions) -> Result<(), ManagerError> { | |
| reject_plugin_flags(options, false, false, false)?; | |
| if positionals.len() != 2 { | |
| return Err(ManagerError::Usage("usage: cmux-mux plugin update <name>".to_string())); | |
| } | |
| let name = &positionals[1]; | |
| validate_plugin_name(name)?; | |
| let dir = install_root()?.join(name); | |
| if !dir.is_dir() { | |
| return Err(ManagerError::Failure(anyhow::anyhow!("plugin {name:?} is not installed"))); | |
| } | |
| run_git(["pull", "--ff-only"], None, Some(&dir))?; | |
| let manifest = read_manifest(&dir)?; | |
| run_build_if_needed(&manifest, &dir)?; | |
| let command = resolved_run_command(&manifest, &dir)?; | |
| verify_executable(&command[0])?; | |
| println!("updated {name}{}", version_suffix(&manifest)); | |
| Ok(()) | |
| } | |
| fn update_command(positionals: &[String], options: &CliOptions) -> Result<(), ManagerError> { | |
| reject_plugin_flags(options, false, false, false)?; | |
| if positionals.len() != 2 { | |
| return Err(ManagerError::Usage("usage: cmux-mux plugin update <name>".to_string())); | |
| } | |
| let name = &positionals[1]; | |
| validate_plugin_name(name)?; | |
| let dir = install_root()?.join(name); | |
| if !dir.is_dir() { | |
| return Err(ManagerError::Failure(anyhow::anyhow!("plugin {name:?} is not installed"))); | |
| } | |
| run_git(["pull", "--ff-only"], None, Some(&dir))?; | |
| let manifest = read_manifest(&dir)?; | |
| run_build_if_needed(&manifest, &dir)?; | |
| let command = resolved_run_command(&manifest, &dir)?; | |
| verify_executable(&command[0])?; | |
| if selected_plugin_cwd()?.is_some_and(|cwd| same_path(&cwd, &dir)) { | |
| let cwd = canonical_path(&dir)?; | |
| let path = config::write_sidebar_plugin(Some(&SidebarPluginConfig { | |
| command, | |
| cwd: Some(cwd.display().to_string()), | |
| }))?; | |
| println!("refreshed selected sidebar.plugin in {}", path.display()); | |
| report_reload_config(options); | |
| } | |
| println!("updated {name}{}", version_suffix(&manifest)); | |
| Ok(()) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mux/crates/mux-tui/src/plugin_manager.rs` around lines 199 - 217, The
update_command flow re-resolves the plugin after a pull but never refreshes the
persisted selected-plugin config, so mux.json can keep stale sidebar.plugin
data. Add the same selected-plugin check used in remove_command: after
read_manifest/run_build_if_needed/resolved_run_command, if the updated plugin is
currently selected, rewrite the stored run command and cwd in mux.json to match
the newly resolved command from update_command. Use the existing
use_command/remove_command logic as the reference for locating the selection and
config update path.
| fn run_git<const N: usize>( | ||
| args: [&str; N], | ||
| final_arg_path: Option<&Path>, | ||
| current_dir: Option<&Path>, | ||
| ) -> anyhow::Result<()> { | ||
| let mut command = Command::new("git"); | ||
| command.args(["-c", "protocol.file.allow=always"]).args(args); | ||
| if let Some(path) = final_arg_path { | ||
| command.arg(path); | ||
| } | ||
| if let Some(dir) = current_dir { | ||
| command.current_dir(dir); | ||
| } | ||
| let status = command.status()?; | ||
| if !status.success() { | ||
| anyhow::bail!("git failed with status {status}"); | ||
| } | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
git protocol.file.allow user vs always default top-level clone file protocol
💡 Result:
In Git, the protocol.file.allow configuration determines how the file protocol is handled during operations like cloning, fetching, or pushing [1][2]. The default policy for this protocol was changed to user in 2022 to improve security [3][4]. Policy Definitions: always: The protocol is always enabled [1]. never: The protocol is never enabled [1]. user: The protocol is enabled only when the command is initiated directly by the user (or when the environment variable GIT_PROTOCOL_FROM_USER is unset or set to 1) [1]. This setting blocks the use of the file protocol in automated, non-interactive processes, such as recursive submodule initialization, to prevent potential security vulnerabilities where a malicious repository might trick a user into executing arbitrary commands via a local file path [3][5]. Security Context: Prior to this change, the file protocol was classified as known-safe and defaulted to always [3][5]. By changing the default to user, Git prevents commands that execute without direct user input from accessing local file paths by default, mitigating risks associated with cloning from untrusted sources that might contain malicious submodules [3][4]. If you encounter errors when performing operations like submodule updates with local file paths, you can override this behavior by explicitly setting the configuration for your command: git -c protocol.file.allow=always clone --recurse-submodules [6]
Citations:
- 1: https://code.googlesource.com/git/+/0fae78c9d55efe705877ea537fe42c59164ccd94/Documentation/config/protocol.adoc
- 2: git/git@f1762d7
- 3: git/git@a1d4f67
- 4: peff/git@a1d4f67
- 5: https://public-inbox.org/git/1478292933-7873-1-git-send-email-bmwill@google.com/
- 6: https://stackoverflow.com/questions/78597354/adding-local-repo-as-submodule-always-resolves-to-github
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="mux/crates/mux-tui/src/plugin_manager.rs"
echo "== outline =="
ast-grep outline "$file" --view expanded || true
echo
echo "== relevant lines around run_git =="
sed -n '320,470p' "$file"
echo
echo "== run_git call sites =="
rg -n "run_git\(" "$file"Repository: manaflow-ai/cmux
Length of output: 8486
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="mux/crates/mux-tui/src/plugin_manager.rs"
echo "== install_command =="
sed -n '91,122p' "$file"
echo
echo "== update_command =="
sed -n '199,216p' "$file"
echo
echo "== git clone / pull context =="
rg -n --context 2 'clone|pull|protocol.file.allow' "$file"Repository: manaflow-ai/cmux
Length of output: 3938
🌐 Web query:
git protocol.file.allow=user top-level git pull file:// remote allowed GIT_PROTOCOL_FROM_USER
💡 Result:
In Git, the configuration protocol.file.allow determines whether the file:// transport protocol is permitted for operations like clone, fetch, and push [1][2]. The default policy for the file protocol is set to user [3][4]. When a protocol is set to user policy: * It is allowed when the command is triggered directly by the user [1][2]. * It is disallowed for commands that execute without direct user intervention (e.g., recursive submodule initialization), unless specifically permitted [1][2]. The GIT_PROTOCOL_FROM_USER environment variable is the mechanism Git uses to distinguish between these cases [2][5]. * When GIT_PROTOCOL_FROM_USER is unset or set to 1, Git treats the operation as being initiated by the user, allowing user-policy protocols [1][6]. * When GIT_PROTOCOL_FROM_USER is set to 0, Git treats the operation as automated (or potentially untrusted), blocking user-policy protocols [2][7]. This default was changed to user to mitigate security risks, such as preventing malicious repositories from using recursive submodule cloning to exfiltrate sensitive local data [3][8]. If you encounter errors (such as during submodule operations) due to this policy, you can override it by: * Setting the configuration globally: git config --global protocol.file.allow always [9][10]. * Overriding the configuration for a specific command using the -c flag: git -c protocol.file.allow=always ... [11][10]. Note that setting protocol.file.allow in a local repository configuration (--local) may not be effective for submodules, as the submodule's own Git process does not inherit the superproject's local configuration [11][10].
Citations:
- 1: https://code.googlesource.com/git/+/0fae78c9d55efe705877ea537fe42c59164ccd94/Documentation/config/protocol.adoc
- 2: git/git@f1762d7
- 3: git/git@a1d4f67
- 4: https://public-inbox.org/git/1478292933-7873-1-git-send-email-bmwill@google.com/
- 5: https://github.com/git/git/blob/master/transport.c
- 6: https://git-scm.com/docs/git-config
- 7: https://man7.org/linux/man-pages/man1/git.1.html
- 8: https://public-inbox.org/git/Y69TMzIf%2FbdsZe6%2F@nand.local/
- 9: https://stackoverflow.com/questions/74486167/git-clone-recurse-submodules-throws-error-on-macos-transmission-type-file-n
- 10: https://public-inbox.org/git/011201d91aca$a5db7800$f1926800$@nexbridge.com/T/
- 11: https://public-inbox.org/git/176105772451.7.13453872556381855079.964553330@passmail.net/T/
Use protocol.file.allow=user here
clone and pull are both user-triggered paths, so the default user policy already allows file:// remotes. always only widens access to non-user contexts like recursive submodule fetches, which this helper doesn’t need. If a specific local fixture requires it, scope always to that path only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@mux/crates/mux-tui/src/plugin_manager.rs` around lines 384 - 402, The run_git
helper is forcing git to allow file:// protocols too broadly by using
protocol.file.allow=always. Update the Command setup in run_git to use the user
policy instead, since this helper is only used for user-triggered git clone/pull
flows. Keep the change localized to run_git and only consider broader
allow=always handling in any separate fixture-specific path if needed.
…d-step fixture Rename the VerbKind stream flag to stream_mode (it was shadowed by the connection binding and tested as a bool). Test compares config paths against the canonicalized install dir (/tmp is a /private/tmp symlink on macOS). The install fixture no longer commits the runnable: build.sh (heredoc, shell-verified) creates it, so the build step and executable verification are on the tested path.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@mux/crates/mux-tui/tests/cli.rs`:
- Around line 288-294: The test fixture setup in
plugin_install_use_and_list_work_against_local_git_repo writes cmux-plugin.toml
into source before the directory exists, which will panic. Create the source
directory first in the test setup before calling fs::write, using the existing
dir/source variables in plugin_install_use_and_list_work_against_local_git_repo
so the local git repo fixture is initialized properly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9f85740b-3eca-43f7-8b00-2f9358415e20
📒 Files selected for processing (2)
mux/crates/mux-tui/src/cli.rsmux/crates/mux-tui/tests/cli.rs
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fd4ae0f. Configure here.
| if target.exists() { | ||
| fs::remove_dir_all(&target)?; | ||
| } | ||
| fs::rename(&temp_dir, &target)?; |
There was a problem hiding this comment.
Force install deletes active plugin
High Severity
plugin install --force removes the existing install directory with remove_dir_all before moving the new clone into place, without checking whether that plugin is currently selected or stopping/reloading the sidebar. A running session can keep executing from paths under a tree that was just deleted, while mux.json still points at the old layout.
Reviewed by Cursor Bugbot for commit fd4ae0f. Configure here.
| if !sidebar.is_object() { | ||
| *sidebar = json!({}); | ||
| } |
There was a problem hiding this comment.
When mux.json already has a non-object sidebar value, plugin use reaches this branch and replaces that value with an empty object before adding sidebar.plugin. For example, a user config with "sidebar": "left" is silently rewritten and the original setting is lost. The write path should reject that incompatible shape or leave it unchanged instead of overwriting it.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
mux/crates/mux-tui/tests/cli.rs (1)
288-407: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd test coverage for
plugin updateandplugin remove.The test thoroughly covers install, list, use, and
use --builtin, butplugin update <name>andplugin remove <name>are untested. Both have real logic — update does a git pull and re-runs build/verification; remove deletes the plugin directory and config entry. Regressions in these paths would go undetected.The test infrastructure (fixture setup,
plugin_clihelper, config assertions) is already in place, so adding these cases is straightforward.♻️ Suggested additions
// After the --builtin test (line 404), before cleanup: // --- plugin update --- // Modify the fixture source, commit, then update. fs::write(source.join("cmux-plugin.toml"), r#" [plugin] name = "fixture" kind = "sidebar" version = "0.2.0" description = "Updated fixture sidebar" [run] command = ["bin/sidebar"] [build] command = ["/bin/sh", "build.sh"] "#).unwrap(); git(&source, &["add", "."]); git(&source, &["-c", "user.name=cmux", "-c", "user.email=cmux@example.invalid", "commit", "-m", "v0.2.0"]); let update = plugin_cli(&data_home, &config_path, &["plugin", "update", "fixture"]); assert_success(&update); // --- plugin remove --- let remove = plugin_cli(&data_home, &config_path, &["plugin", "remove", "fixture"]); assert_success(&remove); assert!(!installed_dir.exists()); let list = plugin_cli(&data_home, &config_path, &["--json", "plugin", "list"]); assert_success(&list); let listed: serde_json::Value = serde_json::from_slice(&list.stdout).unwrap(); assert!(listed["plugins"].as_array().unwrap().is_empty());🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@mux/crates/mux-tui/tests/cli.rs` around lines 288 - 407, The `plugin_install_use_and_list_work_against_local_git_repo` test is missing coverage for `plugin update` and `plugin remove`, so extend this fixture-based flow to exercise both commands. After the existing `plugin use --builtin` assertions, modify and recommit the local git fixture, then invoke `plugin_cli` with `plugin update fixture` to verify the update path succeeds and rebuild/verification still works. Next call `plugin_cli` with `plugin remove fixture` and assert the installed plugin directory is deleted and the config entry is removed by checking the same `installed_dir`, `config_path`, and `plugin list` JSON state used elsewhere in the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@mux/crates/mux-tui/tests/cli.rs`:
- Around line 288-407: The
`plugin_install_use_and_list_work_against_local_git_repo` test is missing
coverage for `plugin update` and `plugin remove`, so extend this fixture-based
flow to exercise both commands. After the existing `plugin use --builtin`
assertions, modify and recommit the local git fixture, then invoke `plugin_cli`
with `plugin update fixture` to verify the update path succeeds and
rebuild/verification still works. Next call `plugin_cli` with `plugin remove
fixture` and assert the installed plugin directory is deleted and the config
entry is removed by checking the same `installed_dir`, `config_path`, and
`plugin list` JSON state used elsewhere in the test.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2fd9d872-ac9b-4f87-8e68-42733ae4ed8d
📒 Files selected for processing (1)
mux/crates/mux-tui/tests/cli.rs


Closes the sidebar-plugin loop:
cmux-mux plugin install https://github.com/manaflow-ai/cmux-sidebar-fzf && cmux-mux plugin use fzf, then prefix-S in any session opens the fuzzy finder.plugin install <git-url> [--name] [--force]: shallow clone to ~/.local/share/cmux/mux-plugins/ (XDG-aware), manifest validation (kind=sidebar, name [a-z0-9-_]+, run command), optional build step, executable verification.plugin list/use/update/remove(+use --builtin/disable): selection writes sidebar.plugin argv+cwd into mux.json atomically preserving all other keys, with best-effort reload-config to a running server.Local compile blocked (host zig issue) — CI is the compile gate.
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
The CLI runs user-supplied git URLs, build commands, and plugin binaries and rewrites
mux.json, but changes stay client-side with manifest validation and no server/protocol changes.Overview
Adds CLI-only sidebar plugin management so users can install git-hosted plugins and point
mux.jsonat them without new control-socket commands.cmux-mux pluginsubcommands (install,list,use,disable,update,remove) live in a newplugin_managermodule. Install shallow-clones into XDG-awaremux-plugins/<name>, parsescmux-plugin.toml(via newtomldep), optionally runs[build], checks the resolved[run]binary is executable, and supports--name/--force. Use / disable / builtin atomically patch onlysidebar.plugininmux.jsonwhile preserving other keys, then best-effortreload-configwhen the session socket is up.The CLI layer splits verbs into socket vs local handlers, registers
pluginas local (positional subcommands), adds per-verb help in--help, and documents plugin usage in main usage text. Tests cover manifest validation, help output, config RMW, and an e2efile://git install/use/list flow; spec and configuration docs describe the workflow.Reviewed by Cursor Bugbot for commit fd4ae0f. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds a local plugin manager to install and manage sidebar plugins from git repos. Selecting a plugin writes
sidebar.plugintomux.jsonatomically and best-effort reloads the running session; no socket protocol changes.New Features
pluginverbs:install <git-url> [--name] [--force],list [--json],use <name>|--builtin,disable,update <name>,remove <name>.~/.local/share/cmux/mux-plugins/<name>(XDG-aware), validates manifest, runs optional build, and verifies executables.sidebar.plugin { command, cwd }via atomic RMW while preserving othermux.jsonkeys; best-effortreload-configonuse/disable/removal.--helpnow lists concise per-verb help; integration test installs from a localfile://repo and covers build + exec verification; docs updated inspec/cli.md,spec/plugins.md, anddocs/configuration.md.Bug Fixes
Written for commit fd4ae0f. Summary will update on new commits.
Summary by CodeRabbit
install,list(--json),use,disable,update, andremove.mux.jsonwhile preserving unrelated existing settings.use --builtin/reload behavior works.