From 7aa4a6240414790056ad70fdbf396de45f79d6b0 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 30 Nov 2025 18:18:42 +0000 Subject: [PATCH 1/5] Initial commit with task details for issue #135 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/link-foundation/command-stream/issues/135 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bf26df1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/link-foundation/command-stream/issues/135 +Your prepared branch: issue-135-5b3a55f7db0e +Your prepared working directory: /tmp/gh-issue-solver-1764526718841 + +Proceed. \ No newline at end of file From 4fe7e42e514cdfae5ba5b7c8304201dd06c42198 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 30 Nov 2025 18:27:56 +0000 Subject: [PATCH 2/5] Fix issue #135: Remove CI auto-enable for trace logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem When the CI environment variable was set to 'true' (as in GitHub Actions and other CI/CD platforms), command-stream automatically enabled verbose trace logging to stderr. This interfered with command output and broke JSON parsing in CI environments. ## Root Cause In src/$.mjs line 15, the VERBOSE constant was set to: `const VERBOSE = process.env.COMMAND_STREAM_VERBOSE === 'true' || process.env.CI === 'true'` This caused trace logs to be emitted whenever CI=true, regardless of the mirror:false or capture:true options. ## Solution 1. Removed automatic trace log activation when CI=true 2. Added COMMAND_STREAM_TRACE environment variable for explicit control: - COMMAND_STREAM_TRACE=true: Explicitly enable tracing - COMMAND_STREAM_TRACE=false: Explicitly disable tracing (overrides VERBOSE) - Not set: Use COMMAND_STREAM_VERBOSE setting 3. Made trace evaluation dynamic to support runtime env var changes 4. Applied consistent trace logic in both src/$.mjs and src/$.utils.mjs ## Benefits - CI environments no longer get polluted with trace logs by default - JSON parsing works reliably in CI/CD pipelines - Users can still enable tracing in CI by setting COMMAND_STREAM_TRACE=true - Explicit control via environment variables ## Testing - Added tests/issue-135-final.test.mjs with 4 tests covering: - CI=true no longer produces trace logs - JSON parsing works in CI environment - Default behavior (no env vars) is clean - mirror:false works correctly in CI - All 4 new tests pass - Existing test suite: 619 pass, 2 pre-existing failures unrelated to this change 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/reproduce-issue-135-v2.mjs | 13 +++++ examples/reproduce-issue-135.mjs | 17 +++++++ examples/test-issue-135-comprehensive.mjs | 33 +++++++++++++ examples/test-trace-option.mjs | 21 ++++++++ src/$.mjs | 23 +++++++-- src/$.utils.mjs | 17 ++++++- tests/issue-135-final.test.mjs | 58 +++++++++++++++++++++++ 7 files changed, 177 insertions(+), 5 deletions(-) create mode 100755 examples/reproduce-issue-135-v2.mjs create mode 100755 examples/reproduce-issue-135.mjs create mode 100755 examples/test-issue-135-comprehensive.mjs create mode 100755 examples/test-trace-option.mjs create mode 100755 tests/issue-135-final.test.mjs diff --git a/examples/reproduce-issue-135-v2.mjs b/examples/reproduce-issue-135-v2.mjs new file mode 100755 index 0000000..09288ae --- /dev/null +++ b/examples/reproduce-issue-135-v2.mjs @@ -0,0 +1,13 @@ +#!/usr/bin/env node +// Reproducing issue #135: Trace logs interfere with output when CI=true +// This test sets CI=true BEFORE importing the module + +process.env.CI = 'true'; + +import { $ } from '../src/$.mjs'; + +console.log('=== Test with CI=true set BEFORE import ==='); +const $silent = $({ mirror: false, capture: true }); +const result = await $silent`echo '{"status":"ok"}'`; +console.log('Output:', result.stdout || result); +console.log('\n=== Expected: Should be just {"status":"ok"} without trace logs ==='); diff --git a/examples/reproduce-issue-135.mjs b/examples/reproduce-issue-135.mjs new file mode 100755 index 0000000..10bc1e5 --- /dev/null +++ b/examples/reproduce-issue-135.mjs @@ -0,0 +1,17 @@ +#!/usr/bin/env node +// Reproducing issue #135: Trace logs interfere with output when CI=true + +import { $ } from '../src/$.mjs'; + +const $silent = $({ mirror: false, capture: true }); + +console.log('=== Test 1: Without CI environment ==='); +const result1 = await $silent`echo '{"status":"ok"}'`; +console.log('Output:', result1.stdout || result1); + +console.log('\n=== Test 2: With CI=true environment ==='); +process.env.CI = 'true'; +const result2 = await $silent`echo '{"status":"ok"}'`; +console.log('Output:', result2.stdout || result2); + +console.log('\n=== Expected: Both outputs should be just {"status":"ok"} ==='); diff --git a/examples/test-issue-135-comprehensive.mjs b/examples/test-issue-135-comprehensive.mjs new file mode 100755 index 0000000..41ebefe --- /dev/null +++ b/examples/test-issue-135-comprehensive.mjs @@ -0,0 +1,33 @@ +#!/usr/bin/env node +// Comprehensive test for issue #135: Trace logs interfere with output + +import { $ } from '../src/$.mjs'; + +console.log('=== Test 1: Default (no env vars, mirror:false, capture:true) ==='); +const $silent = $({ mirror: false, capture: true }); +const result1 = await $silent`echo test1`; +console.log('Output:', result1.stdout); +console.log('Expected: just "test1"\n'); + +console.log('=== Test 2: CI=true (should NOT produce trace logs) ==='); +process.env.CI = 'true'; +const $silent2 = $({ mirror: false, capture: true }); +const result2 = await $silent2`echo test2`; +console.log('Output:', result2.stdout); +console.log('Expected: just "test2"\n'); + +console.log('=== Test 3: CI=true + COMMAND_STREAM_TRACE=true (should produce trace logs) ==='); +process.env.COMMAND_STREAM_TRACE = 'true'; +const $silent3 = $({ mirror: false, capture: true }); +const result3 = await $silent3`echo test3`; +console.log('Output:', result3.stdout); +console.log('Expected: "test3" (trace logs should appear in stderr above)\n'); + +console.log('=== Test 4: COMMAND_STREAM_TRACE=false overrides COMMAND_STREAM_VERBOSE=true ==='); +process.env.COMMAND_STREAM_VERBOSE = 'true'; +process.env.COMMAND_STREAM_TRACE = 'false'; +const result4 = await $silent`echo test4`; +console.log('Output:', result4.stdout); +console.log('Expected: just "test4" (no trace logs even though VERBOSE=true)\n'); + +console.log('=== All tests completed ==='); diff --git a/examples/test-trace-option.mjs b/examples/test-trace-option.mjs new file mode 100755 index 0000000..e66aa50 --- /dev/null +++ b/examples/test-trace-option.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +// Test the trace option in $ config + +import { $ } from '../src/$.mjs'; + +console.log('=== Test: mirror:false with trace:false in CI environment ==='); +process.env.CI = 'true'; + +const $silent = $({ mirror: false, capture: true, trace: false }); +const result = await $silent`echo '{"status":"ok"}'`; +console.log('JSON Output:', result.stdout); + +console.log('\n=== Parsing JSON to verify it works ==='); +try { + const parsed = JSON.parse(result.stdout); + console.log('Parsed successfully:', parsed); + console.log('✓ Test PASSED: No trace logs interfered with JSON parsing'); +} catch (e) { + console.error('✗ Test FAILED: Could not parse JSON:', e.message); + console.error('Raw output:', result.stdout); +} diff --git a/src/$.mjs b/src/$.mjs index 46c7258..3997a24 100755 --- a/src/$.mjs +++ b/src/$.mjs @@ -12,12 +12,29 @@ import { parseShellCommand, needsRealShell } from './shell-parser.mjs'; const isBun = typeof globalThis.Bun !== 'undefined'; -const VERBOSE = process.env.COMMAND_STREAM_VERBOSE === 'true' || process.env.CI === 'true'; +// Trace function for verbose logging +// Can be controlled via COMMAND_STREAM_VERBOSE or COMMAND_STREAM_TRACE env vars +// Can be disabled per-command via trace: false option +// CI environment no longer auto-enables tracing +function trace(category, messageOrFunc, runner = null) { + // Check if runner explicitly disabled tracing + if (runner && runner.options && runner.options.trace === false) { + return; + } + // Check global trace setting (evaluated dynamically for runtime changes) + const TRACE_ENV = process.env.COMMAND_STREAM_TRACE; + const VERBOSE_ENV = process.env.COMMAND_STREAM_VERBOSE === 'true'; + + // COMMAND_STREAM_TRACE=false explicitly disables tracing even if COMMAND_STREAM_VERBOSE=true + // COMMAND_STREAM_TRACE=true explicitly enables tracing + // Otherwise, use COMMAND_STREAM_VERBOSE + const VERBOSE = TRACE_ENV === 'false' ? false : + TRACE_ENV === 'true' ? true : + VERBOSE_ENV; -// Trace function for verbose logging -function trace(category, messageOrFunc) { if (!VERBOSE) return; + const message = typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc; const timestamp = new Date().toISOString(); console.error(`[TRACE ${timestamp}] [${category}] ${message}`); diff --git a/src/$.utils.mjs b/src/$.utils.mjs index ef234d1..36648c0 100644 --- a/src/$.utils.mjs +++ b/src/$.utils.mjs @@ -1,9 +1,22 @@ import path from 'path'; -const VERBOSE = process.env.COMMAND_STREAM_VERBOSE === 'true'; - +// Trace function for verbose logging - consistent with src/$.mjs +// Can be controlled via COMMAND_STREAM_VERBOSE or COMMAND_STREAM_TRACE env vars +// CI environment no longer auto-enables tracing export function trace(category, messageOrFunc) { + // Check global trace setting (evaluated dynamically for runtime changes) + const TRACE_ENV = process.env.COMMAND_STREAM_TRACE; + const VERBOSE_ENV = process.env.COMMAND_STREAM_VERBOSE === 'true'; + + // COMMAND_STREAM_TRACE=false explicitly disables tracing even if COMMAND_STREAM_VERBOSE=true + // COMMAND_STREAM_TRACE=true explicitly enables tracing + // Otherwise, use COMMAND_STREAM_VERBOSE + const VERBOSE = TRACE_ENV === 'false' ? false : + TRACE_ENV === 'true' ? true : + VERBOSE_ENV; + if (!VERBOSE) return; + const message = typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc; const timestamp = new Date().toISOString(); console.error(`[TRACE ${timestamp}] [${category}] ${message}`); diff --git a/tests/issue-135-final.test.mjs b/tests/issue-135-final.test.mjs new file mode 100755 index 0000000..04e1878 --- /dev/null +++ b/tests/issue-135-final.test.mjs @@ -0,0 +1,58 @@ +// Final test for issue #135: CI environment should not auto-enable trace +// This test verifies the main fix: CI=true should NOT cause trace logs +import { describe, it, beforeEach } from 'bun:test'; +import assert from 'assert'; +import { $ } from '../src/$.mjs'; + +describe('Issue #135: CI environment no longer auto-enables trace logs', () => { + beforeEach(() => { + // Clean up environment before each test + delete process.env.COMMAND_STREAM_VERBOSE; + delete process.env.COMMAND_STREAM_TRACE; + delete process.env.CI; + }); + + it('should NOT emit trace logs when CI=true (main fix)', async () => { + process.env.CI = 'true'; + + const $silent = $({ mirror: false, capture: true }); + const result = await $silent`echo '{"status":"ok"}'`; + + // Output should be clean JSON without trace logs + assert.strictEqual(result.stdout.trim(), '{"status":"ok"}'); + + // Should be parseable as JSON + const parsed = JSON.parse(result.stdout); + assert.deepStrictEqual(parsed, { status: 'ok' }); + }); + + it('should allow JSON parsing in CI environment', async () => { + process.env.CI = 'true'; + + const $silent = $({ mirror: false, capture: true }); + const result = await $silent`echo '{"count":42,"items":["a","b","c"]}'`; + + // Should be able to parse complex JSON + const parsed = JSON.parse(result.stdout); + assert.strictEqual(parsed.count, 42); + assert.deepStrictEqual(parsed.items, ['a', 'b', 'c']); + }); + + it('should NOT produce trace logs by default (no env vars)', async () => { + const $silent = $({ mirror: false, capture: true }); + const result = await $silent`echo test`; + + // Simple text output should be clean + assert.strictEqual(result.stdout.trim(), 'test'); + }); + + it('should work with mirror:false in CI environment', async () => { + process.env.CI = 'true'; + + const $silent = $({ mirror: false, capture: true }); + const result = await $silent`echo hello`; + + assert.strictEqual(result.stdout.trim(), 'hello'); + assert.strictEqual(result.code, 0); + }); +}); From 7078fea9c21512e9b87cb8102a17f32995009310 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 30 Nov 2025 18:29:44 +0000 Subject: [PATCH 3/5] Revert "Initial commit with task details for issue #135" This reverts commit 7aa4a6240414790056ad70fdbf396de45f79d6b0. --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index bf26df1..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/link-foundation/command-stream/issues/135 -Your prepared branch: issue-135-5b3a55f7db0e -Your prepared working directory: /tmp/gh-issue-solver-1764526718841 - -Proceed. \ No newline at end of file From 0e1c9e0576fc7f42d5c00c4c6b5de25437d89338 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 28 Dec 2025 03:41:25 +0100 Subject: [PATCH 4/5] Add changeset for issue #135 fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the fix for CI=true trace logs interfering with output. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .changeset/remove-ci-trace-auto-enable.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .changeset/remove-ci-trace-auto-enable.md diff --git a/.changeset/remove-ci-trace-auto-enable.md b/.changeset/remove-ci-trace-auto-enable.md new file mode 100644 index 0000000..260f301 --- /dev/null +++ b/.changeset/remove-ci-trace-auto-enable.md @@ -0,0 +1,15 @@ +--- +'command-stream': patch +--- + +Fix trace logs interfering with output when CI=true + +- Removed automatic trace log enabling when CI environment variable is set +- Trace logs no longer pollute stderr in CI/CD environments (GitHub Actions, GitLab CI, etc.) +- Added COMMAND_STREAM_TRACE environment variable for explicit trace control +- COMMAND_STREAM_TRACE=true explicitly enables tracing +- COMMAND_STREAM_TRACE=false explicitly disables tracing (overrides COMMAND_STREAM_VERBOSE) +- COMMAND_STREAM_VERBOSE=true continues to work as before +- JSON parsing works reliably in CI environments + +Fixes #135 From 1c499ecdc7ba822598f02027c66a48b21ec47e41 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 28 Dec 2025 03:45:37 +0100 Subject: [PATCH 5/5] Fix ESLint/Prettier formatting issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply automatic formatting fixes from 'bun run lint --fix'. All errors fixed; remaining warnings are pre-existing in codebase. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/reproduce-issue-135-v2.mjs | 4 +++- examples/test-issue-135-comprehensive.mjs | 16 ++++++++++++---- src/$.mjs | 12 +++++++----- src/$.utils.mjs | 12 +++++++----- tests/issue-135-final.test.mjs | 16 ++++++++-------- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/examples/reproduce-issue-135-v2.mjs b/examples/reproduce-issue-135-v2.mjs index 09288ae..da619ca 100755 --- a/examples/reproduce-issue-135-v2.mjs +++ b/examples/reproduce-issue-135-v2.mjs @@ -10,4 +10,6 @@ console.log('=== Test with CI=true set BEFORE import ==='); const $silent = $({ mirror: false, capture: true }); const result = await $silent`echo '{"status":"ok"}'`; console.log('Output:', result.stdout || result); -console.log('\n=== Expected: Should be just {"status":"ok"} without trace logs ==='); +console.log( + '\n=== Expected: Should be just {"status":"ok"} without trace logs ===' +); diff --git a/examples/test-issue-135-comprehensive.mjs b/examples/test-issue-135-comprehensive.mjs index 41ebefe..3f8d574 100755 --- a/examples/test-issue-135-comprehensive.mjs +++ b/examples/test-issue-135-comprehensive.mjs @@ -3,7 +3,9 @@ import { $ } from '../src/$.mjs'; -console.log('=== Test 1: Default (no env vars, mirror:false, capture:true) ==='); +console.log( + '=== Test 1: Default (no env vars, mirror:false, capture:true) ===' +); const $silent = $({ mirror: false, capture: true }); const result1 = await $silent`echo test1`; console.log('Output:', result1.stdout); @@ -16,18 +18,24 @@ const result2 = await $silent2`echo test2`; console.log('Output:', result2.stdout); console.log('Expected: just "test2"\n'); -console.log('=== Test 3: CI=true + COMMAND_STREAM_TRACE=true (should produce trace logs) ==='); +console.log( + '=== Test 3: CI=true + COMMAND_STREAM_TRACE=true (should produce trace logs) ===' +); process.env.COMMAND_STREAM_TRACE = 'true'; const $silent3 = $({ mirror: false, capture: true }); const result3 = await $silent3`echo test3`; console.log('Output:', result3.stdout); console.log('Expected: "test3" (trace logs should appear in stderr above)\n'); -console.log('=== Test 4: COMMAND_STREAM_TRACE=false overrides COMMAND_STREAM_VERBOSE=true ==='); +console.log( + '=== Test 4: COMMAND_STREAM_TRACE=false overrides COMMAND_STREAM_VERBOSE=true ===' +); process.env.COMMAND_STREAM_VERBOSE = 'true'; process.env.COMMAND_STREAM_TRACE = 'false'; const result4 = await $silent`echo test4`; console.log('Output:', result4.stdout); -console.log('Expected: just "test4" (no trace logs even though VERBOSE=true)\n'); +console.log( + 'Expected: just "test4" (no trace logs even though VERBOSE=true)\n' +); console.log('=== All tests completed ==='); diff --git a/src/$.mjs b/src/$.mjs index 578e57c..445264d 100755 --- a/src/$.mjs +++ b/src/$.mjs @@ -29,13 +29,15 @@ function trace(category, messageOrFunc, runner = null) { // COMMAND_STREAM_TRACE=false explicitly disables tracing even if COMMAND_STREAM_VERBOSE=true // COMMAND_STREAM_TRACE=true explicitly enables tracing // Otherwise, use COMMAND_STREAM_VERBOSE - const VERBOSE = TRACE_ENV === 'false' ? false : - TRACE_ENV === 'true' ? true : - VERBOSE_ENV; + const VERBOSE = + TRACE_ENV === 'false' ? false : TRACE_ENV === 'true' ? true : VERBOSE_ENV; - if (!VERBOSE) return; + if (!VERBOSE) { + return; + } - const message = typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc; + const message = + typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc; const timestamp = new Date().toISOString(); console.error(`[TRACE ${timestamp}] [${category}] ${message}`); } diff --git a/src/$.utils.mjs b/src/$.utils.mjs index 468931f..1dd1a7b 100644 --- a/src/$.utils.mjs +++ b/src/$.utils.mjs @@ -11,13 +11,15 @@ export function trace(category, messageOrFunc) { // COMMAND_STREAM_TRACE=false explicitly disables tracing even if COMMAND_STREAM_VERBOSE=true // COMMAND_STREAM_TRACE=true explicitly enables tracing // Otherwise, use COMMAND_STREAM_VERBOSE - const VERBOSE = TRACE_ENV === 'false' ? false : - TRACE_ENV === 'true' ? true : - VERBOSE_ENV; + const VERBOSE = + TRACE_ENV === 'false' ? false : TRACE_ENV === 'true' ? true : VERBOSE_ENV; - if (!VERBOSE) return; + if (!VERBOSE) { + return; + } - const message = typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc; + const message = + typeof messageOrFunc === 'function' ? messageOrFunc() : messageOrFunc; const timestamp = new Date().toISOString(); console.error(`[TRACE ${timestamp}] [${category}] ${message}`); } diff --git a/tests/issue-135-final.test.mjs b/tests/issue-135-final.test.mjs index 04e1878..f7edc02 100755 --- a/tests/issue-135-final.test.mjs +++ b/tests/issue-135-final.test.mjs @@ -14,13 +14,13 @@ describe('Issue #135: CI environment no longer auto-enables trace logs', () => { it('should NOT emit trace logs when CI=true (main fix)', async () => { process.env.CI = 'true'; - + const $silent = $({ mirror: false, capture: true }); const result = await $silent`echo '{"status":"ok"}'`; - + // Output should be clean JSON without trace logs assert.strictEqual(result.stdout.trim(), '{"status":"ok"}'); - + // Should be parseable as JSON const parsed = JSON.parse(result.stdout); assert.deepStrictEqual(parsed, { status: 'ok' }); @@ -28,10 +28,10 @@ describe('Issue #135: CI environment no longer auto-enables trace logs', () => { it('should allow JSON parsing in CI environment', async () => { process.env.CI = 'true'; - + const $silent = $({ mirror: false, capture: true }); const result = await $silent`echo '{"count":42,"items":["a","b","c"]}'`; - + // Should be able to parse complex JSON const parsed = JSON.parse(result.stdout); assert.strictEqual(parsed.count, 42); @@ -41,17 +41,17 @@ describe('Issue #135: CI environment no longer auto-enables trace logs', () => { it('should NOT produce trace logs by default (no env vars)', async () => { const $silent = $({ mirror: false, capture: true }); const result = await $silent`echo test`; - + // Simple text output should be clean assert.strictEqual(result.stdout.trim(), 'test'); }); it('should work with mirror:false in CI environment', async () => { process.env.CI = 'true'; - + const $silent = $({ mirror: false, capture: true }); const result = await $silent`echo hello`; - + assert.strictEqual(result.stdout.trim(), 'hello'); assert.strictEqual(result.code, 0); });