fix(install): Windows prompt timeout + sweep stale OpenCode archive stub (#875)

Two v0.19.28 release-gate follow-ups.

1. install.ps1 prompt timeout: PowerShell prompts had no timeout (only the
   IsInputRedirected gate), so an attached-but-unattended Windows console
   (PsExec/provisioner first-run or -Reconfigure) could hang. Added
   Read-LineWithTimeout (standard KeyAvailable polling) -> same bounded read +
   safe-"no" fallback install.sh already had; default 30s,
   PLANNOTATOR_PROMPT_TIMEOUT=0 waits forever. Real humans answer in time;
   -NonInteractive / redirected runs never prompt; the checkbox picker is only
   reachable after a bounded "yes", so it can't hang unattended.

2. Sweep stale OpenCode archive stub: the /plannotator-archive removal (#873)
   cleaned the skill scopes but not the OpenCode commands dir, leaving an
   npm-postinstall upgrader with a dead plannotator-archive.md (empty no-op).
   Swept in all three installers' cleanup.

install.test.ts: assert the OpenCode stub cleanup (sh/ps1/cmd) and the ps1
prompt timeout. 72 pass.
This commit is contained in:
Michael Ramos
2026-06-08 14:51:55 -07:00
committed by GitHub
parent b19505efd3
commit 6754dea8df
4 changed files with 69 additions and 1 deletions
+6
View File
@@ -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.
+45 -1
View File
@@ -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
+6
View File
@@ -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
+12
View File
@@ -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");