From 1eae27eb9553e8c81a7a83d11ecefd2f4095c7ea Mon Sep 17 00:00:00 2001 From: Felipe Marzochi Date: Fri, 7 Aug 2026 14:39:08 -0300 Subject: [PATCH 1/2] fix(cli): keep egc subcommands interactive and make the prompt-library gate honest Signed-off-by: Felipe Marzochi --- scripts/egc.js | 16 ++++++---------- scripts/install.ps1 | 12 +++++++++--- scripts/install.sh | 6 +++++- tests/scripts/egc.test.js | 11 +++++++++++ tests/scripts/install-ps1.test.js | 29 +++++++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/scripts/egc.js b/scripts/egc.js index a89bb848..ff467634 100755 --- a/scripts/egc.js +++ b/scripts/egc.js @@ -309,14 +309,18 @@ function runCommand(commandName, args) { throw new Error(`Unknown command: ${commandName}`); } + // stdio 'inherit' keeps the child on the caller's terminal. The piped + // default made every subcommand non-interactive (install.ps1's + // prompt-library Read-Host returned $null through `egc install`, so the + // whole ecosystem step silently vanished) and held all output back + // until the child exited. const result = spawnSync( process.execPath, [path.join(__dirname, command.script), ...args], { cwd: process.cwd(), env: process.env, - encoding: 'utf8', - maxBuffer: 10 * 1024 * 1024, + stdio: 'inherit', } ); @@ -324,14 +328,6 @@ function runCommand(commandName, args) { throw result.error; } - if (result.stdout) { - process.stdout.write(result.stdout); - } - - if (result.stderr) { - process.stderr.write(result.stderr); - } - if (typeof result.status === 'number') { return result.status; } diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 8ed09a7e..29f833b3 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -253,10 +253,10 @@ if ($hasInstallArgs) { } # Interactive ecosystem install (skipped in headless/CI) -$isInteractive = [Environment]::UserInteractive -and -not $env:CI +$isInteractive = [Environment]::UserInteractive -and -not $env:CI -and -not [Console]::IsInputRedirected if ($isInteractive -and -not $DryRun) { - $ans = Read-Host "`n Install prompt library? (62 agents, 228 skills, 74 commands) [Y/n]" - if ($ans -eq '' -or $ans -eq 'Y' -or $ans -eq 'y') { + $ans = Read-Host "`n Install prompt library? (61 agents, 230 skills, 77 commands) [Y/n]" + if ([string]::IsNullOrEmpty($ans) -or $ans -eq 'Y' -or $ans -eq 'y') { if ((Get-Command gemini -ErrorAction SilentlyContinue) -or (Test-Path (Join-Path $env:USERPROFILE ".gemini"))) { Write-Host " installing to Gemini / AGY..." node $EgcInstall --target egc --profile full @@ -294,6 +294,12 @@ if ($isInteractive -and -not $DryRun) { } } } +} elseif (-not $DryRun) { + # A piped or redirected stdin used to reach the Read-Host above and come + # back $null instantly, and `$null -eq ''` is false in PowerShell, so the + # whole ecosystem block vanished without a word (Windows report in #1217: + # install-state left at the previous version). Announce the skip instead. + Write-Host " note: non-interactive session; skipping the prompt-library step. Run 'egc install --target --profile full' to add it." } if (-not $DryRun) { diff --git a/scripts/install.sh b/scripts/install.sh index 320b4597..431ad034 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -215,7 +215,7 @@ node scripts/egc.js doctor --repo-root "$ROOT_DIR" || true # Interactive ecosystem install (skipped in CI/headless environments) if [ -t 0 ] && [ "$DRY_RUN" = false ]; then - printf "\n Install prompt library? (62 agents, 228 skills, 74 commands) [Y/n] " + printf "\n Install prompt library? (61 agents, 230 skills, 77 commands) [Y/n] " read -r _install_ans _install_ans="${_install_ans:-Y}" if [ "$_install_ans" = "Y" ] || [ "$_install_ans" = "y" ]; then @@ -245,6 +245,10 @@ if [ -t 0 ] && [ "$DRY_RUN" = false ]; then bash "$ROOT_DIR/.codebuddy/install.sh" ~ fi fi +elif [ "$DRY_RUN" = false ]; then + # Mirrors install.ps1: a non-TTY stdin skips the prompt-library step. + # Announce it instead of skipping silently. + echo " note: non-interactive session; skipping the prompt-library step. Run 'egc install --target --profile full' to add it." fi # ── MCP auto-registration ───────────────────────────────────────────────────── diff --git a/tests/scripts/egc.test.js b/tests/scripts/egc.test.js index d78379b3..15c3b9af 100644 --- a/tests/scripts/egc.test.js +++ b/tests/scripts/egc.test.js @@ -89,6 +89,17 @@ function main() { assert.strictEqual(result.stdout.trim(), PACKAGE_VERSION); assert.strictEqual(result.stderr, ''); }], + ['runs subcommands on the inherited stdio so prompts and progress reach the terminal', () => { + // The old pipe-and-replay transport made every subcommand + // non-interactive: through `egc install`, install.ps1's prompt-library + // Read-Host got a dead pipe and returned $null, silently skipping the + // whole ecosystem step (#1217), and no output reached the user until + // the child exited. + const source = fs.readFileSync(SCRIPT, 'utf8'); + const runCommandBody = source.slice(source.indexOf('function runCommand('), source.indexOf('async function main(')); + assert.ok(runCommandBody.includes("stdio: 'inherit'"), 'runCommand must hand the terminal to the child'); + assert.ok(!runCommandBody.includes('maxBuffer'), 'no capture buffer: output must stream, not accumulate'); + }], ['delegates explicit install command', () => { const result = runCli(['install', '--dry-run', '--json', 'typescript']); assert.strictEqual(result.status, 0, result.stderr); diff --git a/tests/scripts/install-ps1.test.js b/tests/scripts/install-ps1.test.js index 97fec080..13bad7e9 100644 --- a/tests/scripts/install-ps1.test.js +++ b/tests/scripts/install-ps1.test.js @@ -107,6 +107,35 @@ function runTests() { assert.strictEqual(ps1Floor[1], bashFloor[1], 'install.ps1 Node floor must match install.sh'); })) passed++; else failed++; + if (test('prompt-library counts match install.sh and the README catalog numbers', () => { + const countsOf = (source, label) => { + const m = source.match(/Install prompt library\? \((\d+) agents, (\d+) skills, (\d+) commands\)/); + assert.ok(m, `could not read the prompt-library counts out of ${label}`); + return m.slice(1, 4); + }; + const ps1Counts = countsOf(scriptSource, 'install.ps1'); + const bashCounts = countsOf(bashSource, 'install.sh'); + assert.deepStrictEqual(ps1Counts, bashCounts, 'install.ps1 counts must match install.sh'); + const readme = fs.readFileSync(path.join(__dirname, '..', '..', 'README.md'), 'utf8'); + const readmeCounts = readme.match(/(\d+) agents, (\d+) skills, and (\d+) commands/); + assert.ok(readmeCounts, 'could not read the catalog counts out of README.md'); + assert.deepStrictEqual(ps1Counts, readmeCounts.slice(1, 4), 'installer counts must match the README; they sat at 62/228/74 while the README shipped 61/230/77'); + })) passed++; else failed++; + + if (test('prompt-library gate treats redirected stdin as non-interactive and survives a null Read-Host', () => { + // Through `egc install`, stdin reaches this script as a pipe: + // [Environment]::UserInteractive stays true there, Read-Host returns + // $null immediately, and `$null -eq ''` is false in PowerShell, so the + // ecosystem block used to vanish without a word (Windows report in + // #1217 left install-state at the previous version). + const gateLine = scriptSource.match(/\$isInteractive\s*=.*/); + assert.ok(gateLine, 'could not find the interactivity gate in install.ps1'); + assert.ok(gateLine[0].includes('[Console]::IsInputRedirected'), 'the gate must test IsInputRedirected; UserInteractive alone cannot see a piped stdin'); + assert.ok(scriptSource.includes('[string]::IsNullOrEmpty($ans)'), 'the default-Y branch must accept a null Read-Host result, not just the empty string'); + assert.ok(scriptSource.includes('skipping the prompt-library step'), 'install.ps1 must announce the skip instead of vanishing silently'); + assert.ok(bashSource.includes('skipping the prompt-library step'), 'install.sh must announce the skip too'); + })) passed++; else failed++; + if (test('installs dependencies via a lockfile-aware helper matching install.sh exactly (no npm install fallback)', () => { assert.ok(scriptSource.includes('function Install-Deps'), 'should define the lockfile-aware helper'); assert.ok(scriptSource.includes('Test-Path "package-lock.json"')); From 8eafe90a98dde37993cf42dc67706fbe91cb65fa Mon Sep 17 00:00:00 2001 From: Felipe Marzochi Date: Fri, 7 Aug 2026 14:42:58 -0300 Subject: [PATCH 2/2] docs(roadmap): record the interactive-stdio and prompt-gate fix under Unreleased Signed-off-by: Felipe Marzochi --- docs/ROADMAP.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 198947c3..96235bf0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -4,6 +4,8 @@ This document describes the planned development direction for EGC (Extended Glob ## Unreleased +- `egc ` hands the caller's terminal to its subcommand (`stdio: 'inherit'`) instead of buffering everything through a captured pipe: the piped default made every subcommand non-interactive and held all output until exit, which is how the v1.1.18 Windows report (#1217) ended up with `install.ps1`'s prompt-library step silently skipped and `egc doctor` stuck at the previous version's install-state; the `install.ps1` interactivity gate now also detects a redirected stdin (`[Console]::IsInputRedirected`) and treats a `$null` `Read-Host` answer as the default Y, both installers announce the skip instead of vanishing, and the fossilized prompt-library counts (62/228/74) match the CI-tested README (61/230/77) again behind a new three-way parity test (#1228) + ## v1.1.18: Production Hardening (Released 2026-08-06) - Guardian Bash command validator extended to three more hosts with a genuine pre-action blocking hook: Cursor (#1071), OpenCode (#1072), and Kiro CLI (#1073), each wired through a host-specific translation adapter that reuses shared stdin-parsing and hooks-merge libraries; Token Crusher wired into OpenCode (#1072) and into Antigravity's global hooks.json (#1067)