Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ This document describes the planned development direction for EGC (Extended Glob

## Unreleased

- `egc <command>` 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)
Expand Down
16 changes: 6 additions & 10 deletions scripts/egc.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,29 +309,25 @@ 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',
}
);

if (result.error) {
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;
}
Expand Down
12 changes: 9 additions & 3 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <tool> --profile full' to add it."
}

if (-not $DryRun) {
Expand Down
6 changes: 5 additions & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@

# 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
Expand Down Expand Up @@ -245,6 +245,10 @@
bash "$ROOT_DIR/.codebuddy/install.sh" ~
fi
fi
elif [ "$DRY_RUN" = false ]; then

Check failure on line 248 in scripts/install.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=Fmarzochi_EGC&issues=AZ_dVeu0jlN6eIyL4ZrA&open=AZ_dVeu0jlN6eIyL4ZrA&pullRequest=1228
# 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 <tool> --profile full' to add it."
fi

# ── MCP auto-registration ─────────────────────────────────────────────────────
Expand Down
11 changes: 11 additions & 0 deletions tests/scripts/egc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
29 changes: 29 additions & 0 deletions tests/scripts/install-ps1.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"'));
Expand Down
Loading