Managed plugin install dir + ENTIRE_PLUGIN_DATA_DIR#1121
Managed plugin install dir + ENTIRE_PLUGIN_DATA_DIR#1121ashtom wants to merge 3 commits intosoph/external-command-supportfrom
Conversation
Layered on top of the kubectl-style dispatcher in #1104 — purely additive, no parallel mechanism. - ENTIRE_PLUGIN_DATA_DIR: per-plugin durable storage path. Set in runPlugin's env regardless of where the binary lives, so plugins installed via raw $PATH and via 'entire plugin install' get the same contract. - Managed bin dir at $XDG_DATA_HOME/entire/plugins/bin (override: $ENTIRE_PLUGIN_DIR/bin). main.go prepends it to $PATH at startup so the existing exec.LookPath resolution in resolvePlugin discovers managed installs without any special-casing. - 'entire plugin install/list/remove' for managing the dir. Local-symlink installs only; binary-release and git-clone installs remain deferred until there's demand. Docs in docs/architecture/external-commands.md updated to describe the managed dir and the ENTIRE_PLUGIN_DATA_DIR env var. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: c41bcd1aec30
There was a problem hiding this comment.
Pull request overview
Adds a lightweight “managed plugin install” layer on top of the existing kubectl-style external command dispatcher by (1) prepending a per-user managed bin directory to PATH at startup and (2) injecting a per-plugin durable storage directory via ENTIRE_PLUGIN_DATA_DIR for every plugin invocation.
Changes:
- Prepend a managed plugin bin dir to
PATHbeforeMaybeRunPluginsoexec.LookPathcan discover managed installs without a separate resolution mechanism. - Introduce
ENTIRE_PLUGIN_DATA_DIR(computed from a per-user plugin root + plugin name) and forward it to external commands. - Add
entire plugin install/list/removecommands plus unit/integration coverage for the managed store and the new env var.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/architecture/external-commands.md | Documents managed plugin dir discovery and ENTIRE_PLUGIN_DATA_DIR. |
| cmd/entire/main.go | Prepends managed plugin bin dir to PATH before plugin dispatch. |
| cmd/entire/cli/root.go | Registers the new entire plugin command group. |
| cmd/entire/cli/plugin.go | Injects ENTIRE_PLUGIN_DATA_DIR when running a resolved plugin. |
| cmd/entire/cli/plugin_test.go | Updates unit test call site for runPlugin signature change. |
| cmd/entire/cli/plugin_store.go | Implements managed plugin bin/data directories + install/list/remove helpers. |
| cmd/entire/cli/plugin_store_test.go | Adds unit tests for managed store behaviors and PATH prepending. |
| cmd/entire/cli/plugin_group.go | Implements entire plugin {install,list,remove} Cobra commands. |
| cmd/entire/cli/integration_test/external_command_test.go | Extends integration test to assert ENTIRE_PLUGIN_DATA_DIR. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 5 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 700abdb. Configure here.
- validatePluginName: shared rules, used by PluginDataDir and InstallPluginFromPath. Rejects "."/".." (which would collapse out of the joined path), agent-* (dispatcher reserves it), flag-shaped names, and slashes. isPluginCandidate gets the same "."/".." tightening for defense in depth. - bareNameFromBinaryName: strip .exe/.bat/.cmd only on Windows. On Unix the dispatcher uses exact-match exec.LookPath, so accepting entire-foo.exe would yield a managed entry that could never resolve. - InstallPluginFromPath: refuse self-install when the source path equals the managed destination (path-clean equality only — using os.SameFile would false-fire on the legitimate "previous install is a symlink to src" case). Replace step is now atomic via symlink-to- tmp + rename, so a failed --force never leaves the previous install missing. - plugin_group.go Long help: describe the actual XDG / Windows / ENTIRE_PLUGIN_DIR precedence instead of hard-coding the Linux/macOS default. - external-commands.md: note that the new built-in `entire plugin` command group shadows any pre-existing `entire-plugin` external command (intentional, but worth flagging). Tests: - TestValidatePluginName + TestPluginDataDir_RejectsPathTraversal - TestInstallPluginFromPath_RejectsAgentReservedName - TestInstallPluginFromPath_RejectsSelfInstall (verifies source survives the rejection) - TestInstallPluginFromPath_AtomicForceReplace - TestBareNameFromBinaryName: platform-conditional cases Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 93848fc24cf0
|
Thanks Copilot and Cursor — addressed in 7e1451d. Mapping each comment to the fix: Copilot — Copilot + Cursor — Copilot + Cursor — Cursor — Cursor — failed Copilot — help text hard-codes Copilot — registering
|
| if v := os.Getenv(pluginEnvPluginDir); v != "" { | ||
| return v, nil | ||
| } | ||
| if v := os.Getenv("XDG_DATA_HOME"); v != "" { | ||
| return filepath.Join(v, pluginManagedDirEntireXD), nil | ||
| } |
| // Atomic replace via tmp + rename. Rename is atomic on POSIX and | ||
| // replaces an existing target on Windows (Go's os.Rename uses | ||
| // MoveFileEx with MOVEFILE_REPLACE_EXISTING). If symlink or rename | ||
| // fails, the previously installed plugin (if any) is unaffected. | ||
| tmpDest := dest + ".tmp" | ||
| _ = os.Remove(tmpDest) // best-effort: clean up any stale tmp from a prior failed run | ||
| if err := os.Symlink(src, tmpDest); err != nil { | ||
| return nil, fmt.Errorf("create symlink: %w", err) | ||
| } |
| // Extension stripping is platform-conditional. On Unix the dispatcher's | ||
| // exec.LookPath matches the exact filename, so accepting "entire-pgr.exe" | ||
| // would produce a managed entry that can never be invoked. On Windows the | ||
| // runtime resolves PATHEXT, so .exe/.bat/.cmd entries are valid. |
- pluginParentDir: gate XDG_DATA_HOME to non-Windows. The Windows branch (LOCALAPPDATA) was previously unreachable when XDG_DATA_HOME was set in MSYS/Cygwin environments, producing a surprising location. Tests for both branches. - materializeManagedEntry: new helper that tries symlink → hardlink → copy in that order. Symlinks on Windows require Developer Mode or admin, which would have made `entire plugin install` unusable for typical users. Mirrors the pattern in setup_test.go's copyExecutable. Symlink stays the preferred path so the dev-loop property of "rebuild source, managed entry follows" is preserved wherever it works. - bareNameFromBinaryName comment: clarify that on Unix we don't strip extensions because doing so would create a list/invocation-name mismatch (entry listed as "pgr" but only invocable as "pgr.exe"), not because the entry would be uninvocable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 5671463236d4
|
Second round addressed in 6aab5ce.
|

https://entire.io/gh/entireio/cli/trails/298
Stacked on #1104. Targets `soph/external-command-support` as the base so the diff shows only the additive layer; rebase onto `main` once #1104 lands.
Summary
Two additions on top of the kubectl-style dispatcher in #1104. Both are purely additive — the dispatcher in `plugin.go` keeps its raw `$PATH` model.
Docs at `docs/architecture/external-commands.md` updated with a "Managed install directory" subsection and the new env var row.
Why this shape
This is the smaller follow-up I owed after closing #1116 (gh-style managed store). The kubectl dispatcher in #1104 is the right primitive; this just gives users `entire plugin install` for the local-dev workflow without forking the resolution path.
Two things from #1116 we deliberately did not carry over:
Test plan
🤖 Generated with Claude Code
Note
Medium Risk
Moderate risk because it changes CLI startup behavior by mutating
PATHand expands plugin execution environment; could affect command resolution or plugin expectations across platforms.Overview
Adds a managed plugin installation workflow via a new
entire plugincommand group (install,list,remove) that symlinksentire-<name>executables into a per-user managed bin directory.Updates startup to prepend the managed bin dir to
PATHso existing kubectl-style plugin resolution discovers managed installs without special-casing, and extends plugin execution to always exportENTIRE_PLUGIN_DATA_DIR(per-plugin durable storage path, overrideable viaENTIRE_PLUGIN_DIR).Adds unit/integration coverage for managed-dir behavior and the new env var, and updates external-command architecture docs accordingly.
Reviewed by Cursor Bugbot for commit 700abdb. Configure here.