diff --git a/scripts/install.cmd b/scripts/install.cmd index 8d3890f2..66d83c2e 100644 --- a/scripts/install.cmd +++ b/scripts/install.cmd @@ -818,6 +818,12 @@ for %%D in ("!CLAUDE_SKILLS_DIR!" "!AGENTS_SKILLS_DIR!" "!KIRO_SKILLS_DIR!") do ) ) +REM The /plannotator-archive OpenCode command was removed too — sweep the stub. +if exist "!OPENCODE_COMMANDS_DIR!\plannotator-archive.md" ( + del /q "!OPENCODE_COMMANDS_DIR!\plannotator-archive.md" >nul 2>&1 + echo Removed stale plannotator-archive command from !OPENCODE_COMMANDS_DIR! +) + 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 REM shared-agent extras were never Codex's and are removed unconditionally. diff --git a/scripts/install.ps1 b/scripts/install.ps1 index b3c862f9..ad25061b 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -451,11 +451,49 @@ if (-not $NonInteractive) { $runWizard = $canPrompt -and ($Reconfigure -or -not (Test-Path $prefsFile)) +# Bound interactive prompts so an unattended-but-attached console (e.g. a +# PsExec / provisioner first-run) can't hang the install. Override with +# PLANNOTATOR_PROMPT_TIMEOUT (0 = wait forever); non-numeric/negative -> 30. +$script:promptTimeout = 30 +if ($env:PLANNOTATOR_PROMPT_TIMEOUT) { + $parsed = 0 + if ([int]::TryParse($env:PLANNOTATOR_PROMPT_TIMEOUT, [ref]$parsed) -and $parsed -ge 0) { + $script:promptTimeout = $parsed + } +} + +# Read a line with a timeout (seconds); $null if no input arrives in time. +# 0 waits indefinitely. Echoes typed chars since ReadKey($true) intercepts them. +function Read-LineWithTimeout { + param([int]$TimeoutSeconds) + if ($TimeoutSeconds -le 0) { return [Console]::ReadLine() } + $line = "" + $sw = [System.Diagnostics.Stopwatch]::StartNew() + while ($sw.Elapsed.TotalSeconds -lt $TimeoutSeconds) { + if ([Console]::KeyAvailable) { + $key = [Console]::ReadKey($true) + if ($key.Key -eq "Enter") { Write-Host ""; return $line } + elseif ($key.Key -eq "Backspace") { + if ($line.Length -gt 0) { $line = $line.Substring(0, $line.Length - 1); Write-Host "`b `b" -NoNewline } + } + else { $line += $key.KeyChar; Write-Host $key.KeyChar -NoNewline } + } + else { Start-Sleep -Milliseconds 50 } + } + return $null +} + function Read-YesNo { param([string]$Prompt, [string]$Default) $suffix = if ($Default -eq "yes") { "[Y/n]" } else { "[y/N]" } Write-Host "$Prompt $suffix " -NoNewline - $answer = [Console]::ReadLine() + # On timeout nobody is there -> return the SAFE "no", never $Default, so a + # yes-default prompt (Glimpse) can't auto-run unattended. + $answer = Read-LineWithTimeout $script:promptTimeout + if ($null -eq $answer) { + Write-Host "" + return "no" + } switch -regex ($answer) { '^(y|yes)$' { return "yes" } '^(n|no)$' { return "no" } @@ -756,6 +794,12 @@ foreach ($scope in @($claudeSkillsDir, $agentsSkillsDir, "$env:USERPROFILE\.kiro Remove-Item -Recurse -Force $staleArchivePath -ErrorAction SilentlyContinue } } +# The /plannotator-archive OpenCode command was removed too — sweep the stub. +$staleOpencodeArchive = "$env:USERPROFILE\.config\opencode\commands\plannotator-archive.md" +if (Test-Path $staleOpencodeArchive) { + Write-Host "Removing stale plannotator-archive command $staleOpencodeArchive" + Remove-Item -Force $staleOpencodeArchive -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 diff --git a/scripts/install.sh b/scripts/install.sh index d7188b23..5c830765 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1098,6 +1098,12 @@ for scope in "$CLAUDE_SKILLS_DIR" "$AGENTS_SKILLS_DIR" "$KIRO_SKILLS_DIR"; do echo "Removed stale plannotator-archive skill from ${scope}/plannotator-archive" fi done +# The /plannotator-archive OpenCode command was removed too — sweep the stub +# (only npm-plugin-postinstall users ever had it written here). +if [ -f "$OPENCODE_COMMANDS_DIR/plannotator-archive.md" ]; then + rm -f "$OPENCODE_COMMANDS_DIR/plannotator-archive.md" + echo "Removed stale plannotator-archive command from ${OPENCODE_COMMANDS_DIR}/" +fi # Codex no longer hosts core skills (they now live in ~/.agents/skills). # Core skills are removed only once their replacement exists; the stale diff --git a/scripts/install.test.ts b/scripts/install.test.ts index 60a17da7..85074cc0 100644 --- a/scripts/install.test.ts +++ b/scripts/install.test.ts @@ -205,6 +205,8 @@ describe("install.sh", () => { 'for scope in "$CLAUDE_SKILLS_DIR" "$AGENTS_SKILLS_DIR" "$KIRO_SKILLS_DIR"; do', ); expect(script).toContain('rm -rf "$scope/plannotator-archive"'); + // The removed /plannotator-archive OpenCode command stub is swept too. + expect(script).toContain('rm -f "$OPENCODE_COMMANDS_DIR/plannotator-archive.md"'); }); test("suggests installing extras via npx skills add", () => { @@ -394,6 +396,8 @@ describe("install.ps1", () => { 'foreach ($scope in @($claudeSkillsDir, $agentsSkillsDir, "$env:USERPROFILE\\.kiro\\skills"))', ); expect(script).toContain('Join-Path $scope "plannotator-archive"'); + // The removed /plannotator-archive OpenCode command stub is swept too. + expect(script).toContain('Removing stale plannotator-archive command'); }); test("does not treat a skills-only Codex home as configured", () => { @@ -519,6 +523,8 @@ describe("install.cmd", () => { 'for %%D in ("!CLAUDE_SKILLS_DIR!" "!AGENTS_SKILLS_DIR!" "!KIRO_SKILLS_DIR!") do', ); expect(script).toContain('rmdir /s /q "%%~D\\plannotator-archive"'); + // The removed /plannotator-archive OpenCode command stub is swept too. + expect(script).toContain('del /q "!OPENCODE_COMMANDS_DIR!\\plannotator-archive.md"'); }); test("does not treat a skills-only Codex home as configured", () => { @@ -643,6 +649,12 @@ describe("install shared behavior", () => { expect(sh).toContain("--non-interactive"); expect(ps).toContain("[switch]$NonInteractive"); expect(cmdScript).toContain('"%~1"=="--non-interactive"'); + // Prompts are bounded so an attached-but-unattended console can't hang: + // sh via read -t / PROMPT_TIMEOUT, ps1 via a timed Read-LineWithTimeout, + // both overridable with PLANNOTATOR_PROMPT_TIMEOUT. + expect(sh).toContain("PLANNOTATOR_PROMPT_TIMEOUT"); + expect(ps).toContain("Read-LineWithTimeout"); + expect(ps).toContain("PLANNOTATOR_PROMPT_TIMEOUT"); // The wizard only runs with a real terminal/console attached. expect(sh).toContain("{ : < /dev/tty; } 2>/dev/null"); expect(ps).toContain("[Console]::IsInputRedirected");