diff --git a/apps/marketing/src/content/docs/getting-started/installation.md b/apps/marketing/src/content/docs/getting-started/installation.md index 913844c5..2eac8bc3 100644 --- a/apps/marketing/src/content/docs/getting-started/installation.md +++ b/apps/marketing/src/content/docs/getting-started/installation.md @@ -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: diff --git a/scripts/install.cmd b/scripts/install.cmd index 35425f45..fdab01a3 100644 --- a/scripts/install.cmd +++ b/scripts/install.cmd @@ -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" ( diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 977b3e9b..9a561f5d 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -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!" diff --git a/scripts/install.sh b/scripts/install.sh index 6ce1c257..f1d8c0bf 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -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!" diff --git a/scripts/install.test.ts b/scripts/install.test.ts index a817ca15..fc4d8d89 100644 --- a/scripts/install.test.ts +++ b/scripts/install.test.ts @@ -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