fix: clean stale plugin command files from the installed plugin checkout (#817)

The installer already manages hooks.json inside
~/.claude/plugins/marketplaces/plannotator/apps/hook/, so the earlier
"don't reach into plugin storage" rationale for leaving the old
namespaced plannotator:* command files there was inconsistent. All
three installers now remove them — same replacement-skill guard as the
bare ~/.claude/commands cleanup — making the #817 duplicate menu
entries die on a single installer run + restart instead of waiting for
/plugin marketplace update. Hints/docs reworded accordingly.
This commit is contained in:
Michael Ramos
2026-06-05 09:09:28 -07:00
parent 60f31438d5
commit 3df37da92c
5 changed files with 88 additions and 20 deletions
@@ -113,7 +113,7 @@ claude --plugin-dir ./apps/hook
Plannotator's slash commands (`/plannotator-review`, `/plannotator-annotate`, `/plannotator-last`, `/plannotator-archive`) are installed as Claude Code skills in `~/.claude/skills` by the install script — Claude Code skills are user-invocable by directory name, so the command names are unchanged. There is no separate `~/.claude/commands` step.
Upgrading from an older version? The installer removes the legacy `~/.claude/commands/plannotator-*.md` files automatically, but the marketplace plugin's old namespaced `plannotator:*` command entries are managed by Claude Code — run `/plugin marketplace update` once so they disappear from the `/` menu.
Upgrading from an older version? The installer removes the legacy `~/.claude/commands/plannotator-*.md` files **and** the old namespaced `plannotator:*` command files from the installed plugin automatically (both only once the replacement skills are on disk). Restart Claude Code afterward, and run `/plugin marketplace update` when convenient to pull the latest hooks-only plugin.
Optional extra skills (compound planning, setup-goal, visual explainer) are not installed by default. Add them with:
+21 -5
View File
@@ -783,12 +783,28 @@ REM Claude Code commands are deprecated in favor of skills. Remove a legacy
REM command file only once its replacement skill is actually on disk — running
REM AFTER the install above guarantees a failed or skipped skill install never
REM leaves users with neither the command nor the skill.
REM Older marketplace plugin versions shipped their own commands dir (the
REM namespaced plannotator:* entries); newer plugin versions are hooks-only.
REM We already manage hooks.json inside this same checkout, so remove the
REM stale plugin command files too — same replacement-skill guard.
if defined CLAUDE_CONFIG_DIR (
set "PLUGIN_COMMANDS_DIR=%CLAUDE_CONFIG_DIR%\plugins\marketplaces\plannotator\apps\hook\commands"
) else (
set "PLUGIN_COMMANDS_DIR=%USERPROFILE%\.claude\plugins\marketplaces\plannotator\apps\hook\commands"
)
for %%C in (plannotator-review plannotator-annotate plannotator-last plannotator-archive) do (
if exist "!CLAUDE_SKILLS_DIR!\%%C" if exist "!CLAUDE_COMMANDS_DIR!\%%C.md" (
del /q "!CLAUDE_COMMANDS_DIR!\%%C.md" >nul 2>&1
echo Removed deprecated Claude command !CLAUDE_COMMANDS_DIR!\%%C.md ^(replaced by the %%C skill^)
if exist "!CLAUDE_SKILLS_DIR!\%%C" (
if exist "!CLAUDE_COMMANDS_DIR!\%%C.md" (
del /q "!CLAUDE_COMMANDS_DIR!\%%C.md" >nul 2>&1
echo Removed deprecated Claude command !CLAUDE_COMMANDS_DIR!\%%C.md ^(replaced by the %%C skill^)
)
if exist "!PLUGIN_COMMANDS_DIR!\%%C.md" (
del /q "!PLUGIN_COMMANDS_DIR!\%%C.md" >nul 2>&1
echo Removed stale plugin command !PLUGIN_COMMANDS_DIR!\%%C.md ^(plugin is hooks-only now^)
)
)
)
rmdir "!PLUGIN_COMMANDS_DIR!" >nul 2>&1
REM Codex no longer hosts core skills (they live in %%USERPROFILE%%\.agents\skills).
REM Core skills are removed only once their replacement exists; the stale
@@ -933,8 +949,8 @@ echo Then install the Claude Code plugin:
echo /plugin marketplace add backnotprop/plannotator
echo /plugin install plannotator@plannotator
echo.
echo Upgrading from an older version? Also run /plugin marketplace update
echo so the plugin drops its old plannotator:* command entries.
echo Upgrading from an older version? Run /plugin marketplace update to pull
echo the latest hooks-only plugin ^(stale plugin commands are cleaned up here^).
echo.
echo The /plannotator-review, /plannotator-annotate, /plannotator-last, and /plannotator-archive skills are ready to use!
if not "!EXTRAS_CHOICE!"=="yes" (
+22 -6
View File
@@ -727,14 +727,30 @@ if ($checkoutFailed) {
# command file only once its replacement skill is actually on disk — running
# AFTER the install above guarantees a failed or skipped skill install never
# leaves users with neither the command nor the skill.
# Older marketplace plugin versions shipped their own commands/ dir (the
# namespaced plannotator:* entries); newer plugin versions are hooks-only.
# We already manage hooks.json inside this same checkout, so remove the stale
# plugin command files too — same replacement-skill guard as the bare ones.
$claudeConfigRoot = if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { "$env:USERPROFILE\.claude" }
$pluginCommandsDir = Join-Path $claudeConfigRoot "plugins\marketplaces\plannotator\apps\hook\commands"
foreach ($cmd in @("plannotator-review", "plannotator-annotate", "plannotator-last", "plannotator-archive")) {
$cmdPath = Join-Path $claudeCommandsDir "$cmd.md"
$skillPath = Join-Path $claudeSkillsDir $cmd
if ((Test-Path $skillPath) -and (Test-Path $cmdPath)) {
Write-Host "Removing stale Claude command $cmdPath (replaced by the $cmd skill)"
Remove-Item -Force $cmdPath -ErrorAction SilentlyContinue
if (Test-Path $skillPath) {
$cmdPath = Join-Path $claudeCommandsDir "$cmd.md"
if (Test-Path $cmdPath) {
Write-Host "Removing stale Claude command $cmdPath (replaced by the $cmd skill)"
Remove-Item -Force $cmdPath -ErrorAction SilentlyContinue
}
$pluginCmdPath = Join-Path $pluginCommandsDir "$cmd.md"
if (Test-Path $pluginCmdPath) {
Write-Host "Removing stale plugin command $pluginCmdPath (plugin is hooks-only now)"
Remove-Item -Force $pluginCmdPath -ErrorAction SilentlyContinue
}
}
}
if ((Test-Path $pluginCommandsDir) -and -not (Get-ChildItem $pluginCommandsDir -ErrorAction SilentlyContinue)) {
Remove-Item -Force $pluginCommandsDir -ErrorAction SilentlyContinue
}
# Codex no longer hosts core skills (they now live in ~/.agents/skills).
# Core skills are removed only once their replacement exists; the stale
@@ -894,8 +910,8 @@ Write-Host "Install the Claude Code plugin:"
Write-Host " /plugin marketplace add backnotprop/plannotator"
Write-Host " /plugin install plannotator@plannotator"
Write-Host ""
Write-Host "Upgrading from an older version? Also run /plugin marketplace update"
Write-Host "so the plugin drops its old plannotator:* command entries."
Write-Host "Upgrading from an older version? Run /plugin marketplace update to pull"
Write-Host "the latest hooks-only plugin (stale plugin commands are cleaned up here)."
Write-Host ""
Write-Host "The /plannotator-review, /plannotator-annotate, /plannotator-last, and /plannotator-archive commands are ready to use after you restart Claude Code!"
+17 -5
View File
@@ -1030,12 +1030,24 @@ fi
# AFTER the install above guarantees a failed or skipped skill install never
# leaves users with neither the command nor the skill.
CLAUDE_COMMANDS_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/commands"
# Older marketplace plugin versions shipped their own commands/ dir (the
# namespaced plannotator:* entries); newer plugin versions are hooks-only.
# We already manage hooks.json inside this same checkout, so remove the stale
# plugin command files too — same replacement-skill guard as the bare ones.
PLUGIN_COMMANDS_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/plugins/marketplaces/plannotator/apps/hook/commands"
for cmd in plannotator-review plannotator-annotate plannotator-last plannotator-archive; do
if [ -d "$CLAUDE_SKILLS_DIR/$cmd" ] && [ -f "$CLAUDE_COMMANDS_DIR/$cmd.md" ]; then
rm -f "$CLAUDE_COMMANDS_DIR/$cmd.md"
echo "Removed legacy Claude command ${CLAUDE_COMMANDS_DIR}/$cmd.md (replaced by the $cmd skill)"
if [ -d "$CLAUDE_SKILLS_DIR/$cmd" ]; then
if [ -f "$CLAUDE_COMMANDS_DIR/$cmd.md" ]; then
rm -f "$CLAUDE_COMMANDS_DIR/$cmd.md"
echo "Removed legacy Claude command ${CLAUDE_COMMANDS_DIR}/$cmd.md (replaced by the $cmd skill)"
fi
if [ -f "$PLUGIN_COMMANDS_DIR/$cmd.md" ]; then
rm -f "$PLUGIN_COMMANDS_DIR/$cmd.md"
echo "Removed stale plugin command ${PLUGIN_COMMANDS_DIR}/$cmd.md (plugin is hooks-only now)"
fi
fi
done
rmdir "$PLUGIN_COMMANDS_DIR" 2>/dev/null || true
# Codex no longer hosts core skills (they now live in ~/.agents/skills).
# Core skills are removed only once their replacement exists; the stale
@@ -1221,8 +1233,8 @@ echo "Install the Claude Code plugin:"
echo " /plugin marketplace add backnotprop/plannotator"
echo " /plugin install plannotator@plannotator"
echo ""
echo "Upgrading from an older version? Also run /plugin marketplace update"
echo "so the plugin drops its old plannotator:* command entries."
echo "Upgrading from an older version? Run /plugin marketplace update to pull"
echo "the latest hooks-only plugin (stale plugin commands are cleaned up here)."
echo ""
echo "The /plannotator-review, /plannotator-annotate, /plannotator-last, and /plannotator-archive commands are ready to use after you restart Claude Code!"
+27 -3
View File
@@ -98,7 +98,11 @@ describe("install.sh", () => {
// A command file may only be removed once its same-name skill exists on
// disk, and the cleanup must run AFTER the skill install — so a failed
// fetch or an old pinned tag never deletes commands without replacement.
expect(script).toContain('if [ -d "$CLAUDE_SKILLS_DIR/$cmd" ] && [ -f "$CLAUDE_COMMANDS_DIR/$cmd.md" ]');
// Outer guard: the replacement skill must exist before any command file
// (bare or plugin-checkout) is removed.
expect(script).toContain('if [ -d "$CLAUDE_SKILLS_DIR/$cmd" ]');
expect(script).toContain('if [ -f "$CLAUDE_COMMANDS_DIR/$cmd.md" ]');
expect(script).toContain('if [ -f "$PLUGIN_COMMANDS_DIR/$cmd.md" ]');
const cleanupIndex = script.indexOf('Removed legacy Claude command');
const installIndex = script.indexOf('copy_skill_if_present apps/skills/core/plannotator-review');
expect(installIndex).toBeGreaterThan(0);
@@ -370,7 +374,11 @@ describe("install.ps1", () => {
expect(script).toContain("claudeCommandsDir");
// Command cleanup is guarded on the replacement skill existing and runs
// after the skill install (parity with install.sh).
expect(script).toContain("(Test-Path $skillPath) -and (Test-Path $cmdPath)");
// Outer guard: the replacement skill must exist before any command file
// (bare or plugin-checkout) is removed.
expect(script).toContain("if (Test-Path $skillPath) {");
expect(script).toContain("if (Test-Path $cmdPath) {");
expect(script).toContain("if (Test-Path $pluginCmdPath) {");
// Legacy ~/.agents review/annotate/last cleanup is gone.
expect(script).not.toContain("legacyAgentsSkillsDir");
// Codex cleanup includes the per-command skills now.
@@ -488,7 +496,11 @@ describe("install.cmd", () => {
expect(script).toContain("CLAUDE_COMMANDS_DIR");
// Command cleanup is guarded on the replacement skill existing and runs
// after the skill install (parity with install.sh / install.ps1).
expect(script).toContain('if exist "!CLAUDE_SKILLS_DIR!\\%%C" if exist "!CLAUDE_COMMANDS_DIR!\\%%C.md"');
// Outer guard: the replacement skill must exist before any command file
// (bare or plugin-checkout) is removed.
expect(script).toContain('if exist "!CLAUDE_SKILLS_DIR!\\%%C" (');
expect(script).toContain('if exist "!CLAUDE_COMMANDS_DIR!\\%%C.md"');
expect(script).toContain('if exist "!PLUGIN_COMMANDS_DIR!\\%%C.md"');
// Legacy ~/.agents review/annotate/last cleanup is gone.
expect(script).not.toContain("LEGACY_AGENTS_SKILLS_DIR");
// Codex cleanup includes the per-command skills now.
@@ -648,6 +660,18 @@ describe("install shared behavior", () => {
}
});
test("stale plugin command files are cleaned from the plugin checkout", () => {
// The installer already manages hooks.json inside the installed plugin
// checkout, so it removes the old namespaced plannotator:* command files
// there too — same replacement-skill guard as the bare commands. This is
// what lets #817's duplicate menu entries die on a single installer run
// instead of waiting for /plugin marketplace update.
const cmdScript = readFileSync(join(scriptsDir, "install.cmd"), "utf-8");
expect(sh).toContain("plugins/marketplaces/plannotator/apps/hook/commands");
expect(ps).toContain("plugins\\marketplaces\\plannotator\\apps\\hook\\commands");
expect(cmdScript).toContain("plugins\\marketplaces\\plannotator\\apps\\hook\\commands");
});
test("all installers respect CODEX_HOME for the Codex home directory", () => {
// Codex stores config and state under $CODEX_HOME when set, falling back
// to ~/.codex (developers.openai.com/codex/config-advanced). #852