From f9389d6d859a6abde66bea288c6d2212cba512e5 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 9 Sep 2025 20:57:55 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #38 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/38 --- 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..cf448fe --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/link-foundation/command-stream/issues/38 +Your prepared branch: issue-38-8ff5f784 +Your prepared working directory: /tmp/gh-issue-solver-1757440670957 + +Proceed. \ No newline at end of file From bee63ef4a17d9884e5362dd8f60b547ecf022e67 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 9 Sep 2025 20:58:13 +0300 Subject: [PATCH 2/3] Remove CLAUDE.md - PR created successfully --- 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 cf448fe..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/link-foundation/command-stream/issues/38 -Your prepared branch: issue-38-8ff5f784 -Your prepared working directory: /tmp/gh-issue-solver-1757440670957 - -Proceed. \ No newline at end of file From 0678f170c59e3f6ed1f4d0ee7646969bb6ea4f9b Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 9 Sep 2025 21:04:40 +0300 Subject: [PATCH 3/3] Add exitCode property as alias for code in error objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change addresses issue #38 by adding error.exitCode as an alias for error.code to maintain compatibility with Node.js standard error handling patterns while preserving backward compatibility. Changes: - Add error.exitCode property alongside error.code in all error creation locations - Fix $.exit.mjs virtual command to throw proper Error objects instead of plain objects - Add comprehensive tests for exitCode compatibility - Add example script demonstrating both old and new error handling patterns Both error.code and error.exitCode now contain the same exit code value, allowing developers to use either the traditional command-stream pattern or the standard Node.js pattern. šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- examples/test-exitcode-compatibility.mjs | 110 +++++++++++++++++++++++ src/$.mjs | 12 +++ src/commands/$.exit.mjs | 5 +- tests/exitcode-compatibility.test.mjs | 95 ++++++++++++++++++++ 4 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 examples/test-exitcode-compatibility.mjs create mode 100644 tests/exitcode-compatibility.test.mjs diff --git a/examples/test-exitcode-compatibility.mjs b/examples/test-exitcode-compatibility.mjs new file mode 100644 index 0000000..e2251b1 --- /dev/null +++ b/examples/test-exitcode-compatibility.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node + +/** + * Test script to verify that both error.code and error.exitCode work + * This validates the fix for issue #38 + */ + +import { $, shell } from '../src/$.mjs'; + +// Enable errexit to make commands throw on non-zero exit codes +shell.errexit(true); + +console.log('Testing exitCode alias for error.code...\n'); + +// Test 1: Test that error.exitCode is available alongside error.code +async function testExitCodeAlias() { + console.log('Test 1: Checking error.exitCode alias...'); + + try { + // This should fail with exit code 1 + await $`ls /nonexistent/directory/that/does/not/exist`; + console.log('āŒ Expected command to fail'); + } catch (error) { + console.log(`āœ… error.code: ${error.code} (traditional property)`); + console.log(`āœ… error.exitCode: ${error.exitCode} (Node.js standard property)`); + + if (error.code === error.exitCode) { + console.log('āœ… Both properties contain the same value'); + } else { + console.log(`āŒ Properties don't match: code=${error.code}, exitCode=${error.exitCode}`); + } + + if (error.exitCode === 2) { // ls returns exit code 2 for "No such file or directory" + console.log('āœ… Exit code is correct (2 for ls no such file)'); + } else { + console.log(`ā„¹ļø Exit code is ${error.exitCode} (may vary by system)`); + } + } +} + +// Test 2: Test specific exit codes with exit command +async function testSpecificExitCode() { + console.log('\nTest 2: Testing specific exit code (42)...'); + + try { + await $`exit 42`; + console.log('āŒ Expected command to fail with exit code 42'); + } catch (error) { + console.log(`āœ… error.code: ${error.code}`); + console.log(`āœ… error.exitCode: ${error.exitCode}`); + + if (error.code === 42 && error.exitCode === 42) { + console.log('āœ… Both properties correctly contain exit code 42'); + } else { + console.log(`āŒ Expected both properties to be 42, got code=${error.code}, exitCode=${error.exitCode}`); + } + } +} + +// Test 3: Ensure backward compatibility - existing code using error.code still works +function testBackwardCompatibility() { + console.log('\nTest 3: Testing backward compatibility...'); + + // This is how developers currently handle errors in command-stream + const handleErrorOldWay = (error) => { + if (error.code === 1) { + return 'Handle exit code 1'; + } + return 'Unknown error'; + }; + + // This is the new Node.js standard way + const handleErrorNewWay = (error) => { + if (error.exitCode === 1) { + return 'Handle exit code 1'; + } + return 'Unknown error'; + }; + + // Create a mock error like command-stream would + const mockError = new Error('Test error'); + mockError.code = 1; + mockError.exitCode = 1; + + const oldResult = handleErrorOldWay(mockError); + const newResult = handleErrorNewWay(mockError); + + if (oldResult === newResult) { + console.log('āœ… Both old and new error handling patterns work identically'); + } else { + console.log(`āŒ Compatibility issue: old="${oldResult}", new="${newResult}"`); + } +} + +// Run all tests +async function runAllTests() { + try { + await testExitCodeAlias(); + await testSpecificExitCode(); + testBackwardCompatibility(); + + console.log('\nšŸŽ‰ All tests completed! Issue #38 should be resolved.'); + console.log('Both error.code and error.exitCode are now available.'); + } catch (err) { + console.error('Test failed:', err); + process.exit(1); + } +} + +runAllTests(); \ No newline at end of file diff --git a/src/$.mjs b/src/$.mjs index 46c7258..81614c4 100755 --- a/src/$.mjs +++ b/src/$.mjs @@ -2106,6 +2106,7 @@ class ProcessRunner extends StreamEmitter { const error = new Error(`Command failed with exit code ${this.result.code}`); error.code = this.result.code; + error.exitCode = this.result.code; error.stdout = this.result.stdout; error.stderr = this.result.stderr; error.result = this.result; @@ -2534,6 +2535,8 @@ class ProcessRunner extends StreamEmitter { if (globalShellSettings.errexit && result.code !== 0) { const error = new Error(`Command failed with exit code ${result.code}`); error.code = result.code; + error.exitCode = result.code; + error.exitCode = result.code; error.stdout = result.stdout; error.stderr = result.stderr; error.result = result; @@ -2746,6 +2749,7 @@ class ProcessRunner extends StreamEmitter { if (failedIndex !== -1) { const error = new Error(`Pipeline command at index ${failedIndex} failed with exit code ${exitCodes[failedIndex]}`); error.code = exitCodes[failedIndex]; + error.exitCode = exitCodes[failedIndex]; throw error; } } @@ -2764,6 +2768,7 @@ class ProcessRunner extends StreamEmitter { if (globalShellSettings.errexit && result.code !== 0) { const error = new Error(`Pipeline failed with exit code ${result.code}`); error.code = result.code; + error.exitCode = result.code; error.stdout = result.stdout; error.stderr = result.stderr; error.result = result; @@ -2922,6 +2927,7 @@ class ProcessRunner extends StreamEmitter { if (failedIndex !== -1) { const error = new Error(`Pipeline command at index ${failedIndex} failed with exit code ${exitCodes[failedIndex]}`); error.code = exitCodes[failedIndex]; + error.exitCode = exitCodes[failedIndex]; throw error; } } @@ -2940,6 +2946,7 @@ class ProcessRunner extends StreamEmitter { if (globalShellSettings.errexit && result.code !== 0) { const error = new Error(`Pipeline failed with exit code ${result.code}`); error.code = result.code; + error.exitCode = result.code; error.stdout = result.stdout; error.stderr = result.stderr; error.result = result; @@ -3271,6 +3278,7 @@ class ProcessRunner extends StreamEmitter { if (globalShellSettings.errexit && finalResult.code !== 0) { const error = new Error(`Pipeline failed with exit code ${finalResult.code}`); error.code = finalResult.code; + error.exitCode = finalResult.code; error.stdout = finalResult.stdout; error.stderr = finalResult.stderr; error.result = finalResult; @@ -3283,6 +3291,7 @@ class ProcessRunner extends StreamEmitter { if (globalShellSettings.errexit && result.code !== 0) { const error = new Error(`Pipeline command failed with exit code ${result.code}`); error.code = result.code; + error.exitCode = result.code; error.stdout = result.stdout; error.stderr = result.stderr; error.result = result; @@ -3480,6 +3489,7 @@ class ProcessRunner extends StreamEmitter { if (globalShellSettings.pipefail && result.code !== 0) { const error = new Error(`Pipeline command '${commandStr}' failed with exit code ${result.code}`); error.code = result.code; + error.exitCode = result.code; error.stdout = result.stdout; error.stderr = result.stderr; throw error; @@ -3520,6 +3530,7 @@ class ProcessRunner extends StreamEmitter { if (globalShellSettings.errexit && finalResult.code !== 0) { const error = new Error(`Pipeline failed with exit code ${finalResult.code}`); error.code = finalResult.code; + error.exitCode = finalResult.code; error.stdout = finalResult.stdout; error.stderr = finalResult.stderr; error.result = finalResult; @@ -4289,6 +4300,7 @@ class ProcessRunner extends StreamEmitter { if (globalShellSettings.errexit && result.code !== 0) { const error = new Error(`Command failed with exit code ${result.code}`); error.code = result.code; + error.exitCode = result.code; error.stdout = result.stdout; error.stderr = result.stderr; error.result = result; diff --git a/src/commands/$.exit.mjs b/src/commands/$.exit.mjs index ab57091..0b4c7d6 100644 --- a/src/commands/$.exit.mjs +++ b/src/commands/$.exit.mjs @@ -2,7 +2,10 @@ export default function createExitCommand(globalShellSettings) { return async function exit({ args }) { const code = parseInt(args[0] || 0); if (globalShellSettings.errexit || code !== 0) { - throw { code, message: `Command failed with exit code ${code}` }; + const error = new Error(`Command failed with exit code ${code}`); + error.code = code; + error.exitCode = code; + throw error; } return { stdout: '', code }; }; diff --git a/tests/exitcode-compatibility.test.mjs b/tests/exitcode-compatibility.test.mjs new file mode 100644 index 0000000..afd8395 --- /dev/null +++ b/tests/exitcode-compatibility.test.mjs @@ -0,0 +1,95 @@ +/** + * Tests for issue #38: The library uses error.code instead of error.exitCode + * Verifies that both error.code and error.exitCode are available for backward compatibility + * and Node.js standard compatibility. + */ + +import { describe, test, expect } from 'bun:test'; +import { $, shell } from '../src/$.mjs'; + +describe('exitCode compatibility (issue #38)', () => { + test('should provide both error.code and error.exitCode properties', async () => { + shell.errexit(true); + + try { + await $`exit 42`; + expect(true).toBe(false); // Should not reach here + } catch (error) { + // Both properties should exist and be equal + expect(error.code).toBe(42); + expect(error.exitCode).toBe(42); + expect(error.code).toBe(error.exitCode); + + // Standard Node.js error properties should also exist + expect(error.message).toContain('Command failed with exit code 42'); + expect(error.result).toBeDefined(); + expect(error.result.code).toBe(42); + } + }); + + test('should maintain backward compatibility with existing error.code usage', async () => { + shell.errexit(true); + + try { + await $`exit 5`; + expect(true).toBe(false); + } catch (error) { + // Traditional command-stream pattern should still work + if (error.code === 5) { + expect(true).toBe(true); // This should execute + } else { + expect(true).toBe(false); // This should not execute + } + + // New Node.js standard pattern should also work + if (error.exitCode === 5) { + expect(true).toBe(true); // This should execute + } else { + expect(true).toBe(false); // This should not execute + } + } + }); + + test('should provide exitCode in pipeline errors', async () => { + shell.errexit(true); + shell.pipefail(true); + + try { + await $`echo "test" | exit 3 | echo "after"`; + expect(true).toBe(false); + } catch (error) { + expect(error.code).toBe(3); + expect(error.exitCode).toBe(3); + expect(error.code).toBe(error.exitCode); + } + }); + + test('should work with different exit codes', async () => { + shell.errexit(true); + const testCodes = [1, 2, 127, 255]; + + for (const code of testCodes) { + try { + await $`exit ${code}`; + expect(true).toBe(false); + } catch (error) { + expect(error.code).toBe(code); + expect(error.exitCode).toBe(code); + expect(error.code).toBe(error.exitCode); + } + } + }); + + test('should handle file system errors with both properties', async () => { + try { + await $`ls /nonexistent/directory/path/that/should/not/exist`; + } catch (error) { + // Both properties should exist for file system errors + expect(error.code).toBeDefined(); + expect(error.exitCode).toBeDefined(); + expect(error.code).toBe(error.exitCode); + expect(typeof error.code).toBe('number'); + expect(typeof error.exitCode).toBe('number'); + } + }); +}); \ No newline at end of file