mirror of
https://github.com/backnotprop/plannotator.git
synced 2026-09-14 14:17:26 +08:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user