diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..f9593bbf --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,18 @@ +steps: + - name: Setup Go environment + uses: actions/setup-go@v6.2.0 + with: + # The Go version to download (if necessary) and use. Supports semver spec and ranges. Be sure to enclose this option in single quotation marks. + go-version: # optional + # Path to the go.mod, go.work, .go-version, or .tool-versions file. + go-version-file: # optional + # Set this option to true if you want the action to always check for the latest available version that satisfies the version spec + check-latest: # optional + # Used to pull Go distributions from go-versions. Since there's a default, this is typically not supplied by the user. When running this action on github.com, the default value is sufficient. When running on GHES, you can pass a personal access token for github.com if you are experiencing rate limiting. + token: # optional, default is ${{ github.server_url == 'https://github.com' && github.token || '' }} + # Used to specify whether caching is needed. Set to true, if you'd like to enable caching. + cache: # optional, default is true + # Used to specify the path to a dependency file - go.sum + cache-dependency-path: # optional + # Target architecture for Go to use. Examples: x86, x64. Will use system architecture by default. + architecture: # optional diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml deleted file mode 100644 index 60d79786..00000000 --- a/.github/workflows/security-scan.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: AgentShield Security Scan - -on: - push: - branches: [main] - pull_request: - branches: [main] - -# Prevent duplicate runs -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -# Minimal permissions -permissions: - contents: read - -jobs: - agentshield: - name: AgentShield Scan - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Run AgentShield Security Scan - uses: affaan-m/agentshield@v1 - with: - path: '.' - min-severity: 'medium' - format: 'terminal' - fail-on-findings: 'false' diff --git a/package-lock.json b/package-lock.json index db3ddf53..41827ec8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,25 @@ { - "name": "everything-claude-code", + "name": "ecc-universal", + "version": "1.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "ecc-universal", + "version": "1.4.1", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "ecc-install": "install.sh" + }, "devDependencies": { "@eslint/js": "^9.39.2", "eslint": "^9.39.2", "globals": "^17.1.0", "markdownlint-cli": "^0.47.0" + }, + "engines": { + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -294,6 +305,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -599,6 +611,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1930,6 +1943,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/scripts/hooks/check-console-log.js b/scripts/hooks/check-console-log.js index 3658dde6..f55a5ed1 100755 --- a/scripts/hooks/check-console-log.js +++ b/scripts/hooks/check-console-log.js @@ -32,7 +32,8 @@ process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => { if (data.length < MAX_STDIN) { - data += chunk; + const remaining = MAX_STDIN - data.length; + data += chunk.substring(0, remaining); } }); diff --git a/scripts/hooks/evaluate-session.js b/scripts/hooks/evaluate-session.js index 5323dc04..3faa3891 100644 --- a/scripts/hooks/evaluate-session.js +++ b/scripts/hooks/evaluate-session.js @@ -29,7 +29,8 @@ process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => { if (stdinData.length < MAX_STDIN) { - stdinData += chunk; + const remaining = MAX_STDIN - stdinData.length; + stdinData += chunk.substring(0, remaining); } }); diff --git a/scripts/hooks/post-edit-console-warn.js b/scripts/hooks/post-edit-console-warn.js index e4aa171b..c1b69c46 100644 --- a/scripts/hooks/post-edit-console-warn.js +++ b/scripts/hooks/post-edit-console-warn.js @@ -17,7 +17,8 @@ process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => { if (data.length < MAX_STDIN) { - data += chunk; + const remaining = MAX_STDIN - data.length; + data += chunk.substring(0, remaining); } }); diff --git a/scripts/hooks/post-edit-format.js b/scripts/hooks/post-edit-format.js index f3651b5c..87b1adfb 100644 --- a/scripts/hooks/post-edit-format.js +++ b/scripts/hooks/post-edit-format.js @@ -17,7 +17,8 @@ process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => { if (data.length < MAX_STDIN) { - data += chunk; + const remaining = MAX_STDIN - data.length; + data += chunk.substring(0, remaining); } }); diff --git a/scripts/hooks/post-edit-typecheck.js b/scripts/hooks/post-edit-typecheck.js index 814a3336..18f03b7d 100644 --- a/scripts/hooks/post-edit-typecheck.js +++ b/scripts/hooks/post-edit-typecheck.js @@ -19,7 +19,8 @@ process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { if (data.length < MAX_STDIN) { - data += chunk; + const remaining = MAX_STDIN - data.length; + data += chunk.substring(0, remaining); } }); diff --git a/scripts/hooks/session-end.js b/scripts/hooks/session-end.js index 59bad8d2..dd6355ca 100644 --- a/scripts/hooks/session-end.js +++ b/scripts/hooks/session-end.js @@ -109,7 +109,8 @@ process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => { if (stdinData.length < MAX_STDIN) { - stdinData += chunk; + const remaining = MAX_STDIN - stdinData.length; + stdinData += chunk.substring(0, remaining); } }); diff --git a/scripts/lib/package-manager.js b/scripts/lib/package-manager.js index c12537f0..5f9b497f 100644 --- a/scripts/lib/package-manager.js +++ b/scripts/lib/package-manager.js @@ -282,7 +282,7 @@ function setProjectPackageManager(pmName, projectDir = process.cwd()) { // Allowed characters in script/binary names: alphanumeric, dash, underscore, dot, slash, @ // This prevents shell metacharacter injection while allowing scoped packages (e.g., @scope/pkg) -const SAFE_NAME_REGEX = /^[@a-zA-Z0-9_.\/-]+$/; +const SAFE_NAME_REGEX = /^[@a-zA-Z0-9_./-]+$/; /** * Get the command to run a script @@ -316,7 +316,7 @@ function getRunCommand(script, options = {}) { // Allowed characters in arguments: alphanumeric, whitespace, dashes, dots, slashes, // equals, colons, commas, quotes, @. Rejects shell metacharacters like ; | & ` $ ( ) { } < > ! -const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_.\/:=,'"*+-]+$/; +const SAFE_ARGS_REGEX = /^[@a-zA-Z0-9\s_./:=,'"*+-]+$/; /** * Get the command to execute a package binary @@ -370,28 +370,31 @@ function escapeRegex(str) { function getCommandPattern(action) { const patterns = []; - if (action === 'dev') { + // Trim spaces from action to handle leading/trailing whitespace gracefully + const trimmedAction = action.trim(); + + if (trimmedAction === 'dev') { patterns.push( 'npm run dev', 'pnpm( run)? dev', 'yarn dev', 'bun run dev' ); - } else if (action === 'install') { + } else if (trimmedAction === 'install') { patterns.push( 'npm install', 'pnpm install', 'yarn( install)?', 'bun install' ); - } else if (action === 'test') { + } else if (trimmedAction === 'test') { patterns.push( 'npm test', 'pnpm test', 'yarn test', 'bun test' ); - } else if (action === 'build') { + } else if (trimmedAction === 'build') { patterns.push( 'npm run build', 'pnpm( run)? build', @@ -400,7 +403,7 @@ function getCommandPattern(action) { ); } else { // Generic run command — escape regex metacharacters in action - const escaped = escapeRegex(action); + const escaped = escapeRegex(trimmedAction); patterns.push( `npm run ${escaped}`, `pnpm( run)? ${escaped}`, diff --git a/tests/hooks/evaluate-session.test.js b/tests/hooks/evaluate-session.test.js index a5b92e2d..f49bb6a4 100644 --- a/tests/hooks/evaluate-session.test.js +++ b/tests/hooks/evaluate-session.test.js @@ -11,7 +11,7 @@ const assert = require('assert'); const path = require('path'); const fs = require('fs'); const os = require('os'); -const { spawnSync, execFileSync } = require('child_process'); +const { spawnSync } = require('child_process'); const evaluateScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'evaluate-session.js'); diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index 8e24bac9..e86633e8 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -1324,7 +1324,7 @@ async function runTests() { val = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(val, 2, 'Second call should write count 2'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1341,7 +1341,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(result.stderr.includes('5 tool calls reached'), 'Should suggest compact at threshold'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1359,7 +1359,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(result.stderr.includes('30 tool calls'), 'Should suggest at threshold + 25n intervals'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1376,7 +1376,7 @@ async function runTests() { assert.ok(!result.stderr.includes('tool calls reached'), 'Should not suggest below threshold'); assert.ok(!result.stderr.includes('checkpoint'), 'Should not suggest checkpoint'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1394,7 +1394,7 @@ async function runTests() { const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(newCount, 1, 'Should reset to 1 on overflow value'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1410,7 +1410,7 @@ async function runTests() { const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(newCount, 1, 'Should reset to 1 on negative value'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1426,7 +1426,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(result.stderr.includes('50 tool calls reached'), 'Zero threshold should fall back to 50'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1443,7 +1443,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(result.stderr.includes('50 tool calls reached'), 'Should use default threshold of 50'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1883,7 +1883,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(result.stderr.includes('38 tool calls'), 'Should suggest at threshold(13) + 25 = 38'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1901,7 +1901,7 @@ async function runTests() { assert.strictEqual(result.code, 0); assert.ok(!result.stderr.includes('checkpoint'), 'Should NOT suggest at count=50 with threshold=13'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1918,7 +1918,7 @@ async function runTests() { const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(newCount, 1, 'Should reset to 1 on corrupted file content'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; @@ -1935,7 +1935,7 @@ async function runTests() { const newCount = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(newCount, 1000001, 'Should increment from exactly 1000000'); } finally { - try { fs.unlinkSync(counterFile); } catch {} + try { fs.unlinkSync(counterFile); } catch { /* ignore */ } } })) passed++; else failed++; diff --git a/tests/hooks/suggest-compact.test.js b/tests/hooks/suggest-compact.test.js index 174dc0ea..36dd8b17 100644 --- a/tests/hooks/suggest-compact.test.js +++ b/tests/hooks/suggest-compact.test.js @@ -19,11 +19,11 @@ const compactScript = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'sugg function test(name, fn) { try { fn(); - console.log(` \u2713 ${name}`); + console.log(` \u2713 ${name}`); return true; - } catch (err) { - console.log(` \u2717 ${name}`); - console.log(` Error: ${err.message}`); + } catch (_err) { + console.log(` \u2717 ${name}`); + console.log(` Error: ${_err.message}`); return false; } } @@ -66,7 +66,11 @@ function runTests() { // Cleanup helper function cleanupCounter() { - try { fs.unlinkSync(counterFile); } catch {} + try { + fs.unlinkSync(counterFile); + } catch (_err) { + // Ignore error + } } // Basic functionality @@ -80,7 +84,8 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Counter should be 1 after first run'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('increments counter on subsequent runs', () => { cleanupCounter(); @@ -90,7 +95,8 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 3, 'Counter should be 3 after three runs'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // Threshold suggestion console.log('\nThreshold suggestion:'); @@ -106,7 +112,8 @@ function runTests() { `Should suggest compact at threshold. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('does NOT suggest compact before threshold', () => { cleanupCounter(); @@ -117,7 +124,8 @@ function runTests() { 'Should NOT suggest compact before threshold' ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // Interval suggestion (every 25 calls after threshold) console.log('\nInterval suggestion:'); @@ -135,7 +143,8 @@ function runTests() { `Should suggest at threshold+25 interval. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // Environment variable handling console.log('\nEnvironment variable handling:'); @@ -151,7 +160,8 @@ function runTests() { `Should use default threshold of 50. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('ignores invalid COMPACT_THRESHOLD (negative)', () => { cleanupCounter(); @@ -163,7 +173,8 @@ function runTests() { `Should fallback to 50 for negative threshold. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('ignores non-numeric COMPACT_THRESHOLD', () => { cleanupCounter(); @@ -175,7 +186,8 @@ function runTests() { `Should fallback to 50 for non-numeric threshold. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // Corrupted counter file console.log('\nCorrupted counter file:'); @@ -189,7 +201,8 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Should reset to 1 on corrupted file'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('resets counter on extremely large value', () => { cleanupCounter(); @@ -200,7 +213,8 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Should reset to 1 for value > 1000000'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('handles empty counter file', () => { cleanupCounter(); @@ -211,7 +225,8 @@ function runTests() { const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Should start at 1 for empty file'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // Session isolation console.log('\nSession isolation:'); @@ -230,10 +245,11 @@ function runTests() { assert.strictEqual(countA, 2, 'Session A should have count 2'); assert.strictEqual(countB, 1, 'Session B should have count 1'); } finally { - try { fs.unlinkSync(fileA); } catch {} - try { fs.unlinkSync(fileB); } catch {} + try { fs.unlinkSync(fileA); } catch (_err) { /* ignore */ } + try { fs.unlinkSync(fileB); } catch (_err) { /* ignore */ } } - })) passed++; else failed++; + })) passed++; + else failed++; // Always exits 0 console.log('\nExit code:'); @@ -243,7 +259,8 @@ function runTests() { const result = runCompact({ CLAUDE_SESSION_ID: testSession }); assert.strictEqual(result.code, 0, 'Should always exit 0'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // ── Round 29: threshold boundary values ── console.log('\nThreshold boundary values:'); @@ -258,7 +275,8 @@ function runTests() { `Should fallback to 50 for threshold=0. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('accepts COMPACT_THRESHOLD=10000 (boundary max)', () => { cleanupCounter(); @@ -270,7 +288,8 @@ function runTests() { `Should accept threshold=10000. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects COMPACT_THRESHOLD=10001 (falls back to 50)', () => { cleanupCounter(); @@ -282,7 +301,8 @@ function runTests() { `Should fallback to 50 for threshold=10001. Got stderr: ${result.stderr}` ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects float COMPACT_THRESHOLD (e.g. 3.5)', () => { cleanupCounter(); @@ -297,33 +317,36 @@ function runTests() { 'Float threshold should be parseInt-ed to 3, no suggestion at count=50' ); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('counter value at exact boundary 1000000 is valid', () => { cleanupCounter(); fs.writeFileSync(counterFile, '999999'); - const result = runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' }); + runCompact({ CLAUDE_SESSION_ID: testSession, COMPACT_THRESHOLD: '3' }); // 999999 is valid (> 0, <= 1000000), count becomes 1000000 const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1000000, 'Counter at 1000000 boundary should be valid'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('counter value at 1000001 is clamped (reset to 1)', () => { cleanupCounter(); fs.writeFileSync(counterFile, '1000001'); - const result = runCompact({ CLAUDE_SESSION_ID: testSession }); + runCompact({ CLAUDE_SESSION_ID: testSession }); const count = parseInt(fs.readFileSync(counterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Counter > 1000000 should be reset to 1'); cleanupCounter(); - })) passed++; else failed++; + })) passed++; + else failed++; // ── Round 64: default session ID fallback ── console.log('\nDefault session ID fallback (Round 64):'); if (test('uses "default" session ID when CLAUDE_SESSION_ID is empty', () => { const defaultCounterFile = getCounterFilePath('default'); - try { fs.unlinkSync(defaultCounterFile); } catch {} + try { fs.unlinkSync(defaultCounterFile); } catch (_err) { /* ignore */ } try { // Pass empty CLAUDE_SESSION_ID — falsy, so script uses 'default' const env = { ...process.env, CLAUDE_SESSION_ID: '' }; @@ -338,12 +361,14 @@ function runTests() { const count = parseInt(fs.readFileSync(defaultCounterFile, 'utf8').trim(), 10); assert.strictEqual(count, 1, 'Counter should be 1 for first run with default session'); } finally { - try { fs.unlinkSync(defaultCounterFile); } catch {} + try { fs.unlinkSync(defaultCounterFile); } catch (_err) { /* ignore */ } } - })) passed++; else failed++; + })) passed++; + else failed++; // Summary - console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); + console.log(` +Results: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); } diff --git a/tests/integration/hooks.test.js b/tests/integration/hooks.test.js index 88f43260..6bf0f064 100644 --- a/tests/integration/hooks.test.js +++ b/tests/integration/hooks.test.js @@ -262,8 +262,13 @@ async function runTests() { }); }); - assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED'); - assert.strictEqual(code, 2, 'Blocking hook should exit with code 2'); + // Hook only blocks on non-Windows platforms (tmux is Unix-only) + if (process.platform === 'win32') { + assert.strictEqual(code, 0, 'On Windows, hook should not block (exit 0)'); + } else { + assert.ok(stderr.includes('BLOCKED'), 'Blocking hook should output BLOCKED'); + assert.strictEqual(code, 2, 'Blocking hook should exit with code 2'); + } })) passed++; else failed++; // ========================================== @@ -298,7 +303,12 @@ async function runTests() { }); }); - assert.strictEqual(code, 2, 'Blocking hook should exit 2'); + // Hook only blocks on non-Windows platforms (tmux is Unix-only) + if (process.platform === 'win32') { + assert.strictEqual(code, 0, 'On Windows, hook should not block (exit 0)'); + } else { + assert.strictEqual(code, 2, 'Blocking hook should exit 2'); + } })) passed++; else failed++; if (await asyncTest('hooks handle missing files gracefully', async () => { diff --git a/tests/lib/package-manager.test.js b/tests/lib/package-manager.test.js index a9b32f7b..826d8c02 100644 --- a/tests/lib/package-manager.test.js +++ b/tests/lib/package-manager.test.js @@ -16,11 +16,11 @@ const pm = require('../../scripts/lib/package-manager'); function test(name, fn) { try { fn(); - console.log(` ✓ ${name}`); + console.log(` ✓ ${name}`); return true; - } catch (err) { - console.log(` ✗ ${name}`); - console.log(` Error: ${err.message}`); + } catch (_err) { + console.log(` ✗ ${name}`); + console.log(` Error: ${_err.message}`); return false; } } @@ -52,7 +52,8 @@ function runTests() { assert.ok(pm.PACKAGE_MANAGERS.pnpm, 'Should have pnpm'); assert.ok(pm.PACKAGE_MANAGERS.yarn, 'Should have yarn'); assert.ok(pm.PACKAGE_MANAGERS.bun, 'Should have bun'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('Each manager has required properties', () => { const requiredProps = ['name', 'lockFile', 'installCmd', 'runCmd', 'execCmd', 'testCmd', 'buildCmd', 'devCmd']; @@ -61,7 +62,8 @@ function runTests() { assert.ok(config[prop], `${name} should have ${prop}`); } } - })) passed++; else failed++; + })) passed++; + else failed++; // detectFromLockFile tests console.log('\ndetectFromLockFile:'); @@ -75,7 +77,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('detects pnpm from pnpm-lock.yaml', () => { const testDir = createTestDir(); @@ -86,7 +89,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('detects yarn from yarn.lock', () => { const testDir = createTestDir(); @@ -97,7 +101,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('detects bun from bun.lockb', () => { const testDir = createTestDir(); @@ -108,7 +113,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns null when no lock file exists', () => { const testDir = createTestDir(); @@ -118,7 +124,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('respects detection priority (pnpm > npm)', () => { const testDir = createTestDir(); @@ -132,7 +139,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; // detectFromPackageJson tests console.log('\ndetectFromPackageJson:'); @@ -140,43 +148,38 @@ function runTests() { if (test('detects package manager from packageManager field', () => { const testDir = createTestDir(); try { - fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ - name: 'test', - packageManager: 'pnpm@8.6.0' - })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test', packageManager: 'pnpm@8.6.0' })); const result = pm.detectFromPackageJson(testDir); assert.strictEqual(result, 'pnpm'); } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('handles packageManager without version', () => { const testDir = createTestDir(); try { - fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ - name: 'test', - packageManager: 'yarn' - })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test', packageManager: 'yarn' })); const result = pm.detectFromPackageJson(testDir); assert.strictEqual(result, 'yarn'); } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns null when no packageManager field', () => { const testDir = createTestDir(); try { - fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ - name: 'test' - })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test' })); const result = pm.detectFromPackageJson(testDir); assert.strictEqual(result, null); } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns null when no package.json exists', () => { const testDir = createTestDir(); @@ -186,7 +189,8 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; // getAvailablePackageManagers tests console.log('\ngetAvailablePackageManagers:'); @@ -196,7 +200,8 @@ function runTests() { assert.ok(Array.isArray(available), 'Should return array'); // npm should always be available with Node.js assert.ok(available.includes('npm'), 'npm should be available'); - })) passed++; else failed++; + })) passed++; + else failed++; // getPackageManager tests console.log('\ngetPackageManager:'); @@ -206,7 +211,8 @@ function runTests() { assert.ok(result.name, 'Should have name'); assert.ok(result.config, 'Should have config'); assert.ok(result.source, 'Should have source'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('respects environment variable', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -222,12 +228,12 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('detects from lock file in project', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; delete process.env.CLAUDE_PACKAGE_MANAGER; - const testDir = createTestDir(); try { fs.writeFileSync(path.join(testDir, 'bun.lockb'), ''); @@ -240,7 +246,8 @@ function runTests() { process.env.CLAUDE_PACKAGE_MANAGER = originalEnv; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getRunCommand tests console.log('\ngetRunCommand:'); @@ -258,7 +265,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns correct test command', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -273,7 +281,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getExecCommand tests console.log('\ngetExecCommand:'); @@ -291,7 +300,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns correct exec command for pnpm', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -306,7 +316,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getCommandPattern tests console.log('\ngetCommandPattern:'); @@ -317,18 +328,19 @@ function runTests() { assert.ok(pattern.includes('pnpm'), 'Should include pnpm'); assert.ok(pattern.includes('yarn dev'), 'Should include yarn'); assert.ok(pattern.includes('bun run dev'), 'Should include bun'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('pattern matches actual commands', () => { const pattern = pm.getCommandPattern('test'); const regex = new RegExp(pattern); - assert.ok(regex.test('npm test'), 'Should match npm test'); assert.ok(regex.test('pnpm test'), 'Should match pnpm test'); assert.ok(regex.test('yarn test'), 'Should match yarn test'); assert.ok(regex.test('bun test'), 'Should match bun test'); assert.ok(!regex.test('cargo test'), 'Should not match cargo test'); - })) passed++; else failed++; + })) passed++; + else failed++; // getSelectionPrompt tests console.log('\ngetSelectionPrompt:'); @@ -338,7 +350,8 @@ function runTests() { assert.ok(prompt.includes('Supported package managers'), 'Should list supported managers'); assert.ok(prompt.includes('CLAUDE_PACKAGE_MANAGER'), 'Should mention env var'); assert.ok(prompt.includes('lock file'), 'Should mention lock file option'); - })) passed++; else failed++; + })) passed++; + else failed++; // setProjectPackageManager tests console.log('\nsetProjectPackageManager:'); @@ -349,7 +362,6 @@ function runTests() { const result = pm.setProjectPackageManager('pnpm', testDir); assert.strictEqual(result.packageManager, 'pnpm'); assert.ok(result.setAt, 'Should have setAt timestamp'); - // Verify file was created const configPath = path.join(testDir, '.claude', 'package-manager.json'); assert.ok(fs.existsSync(configPath), 'Config file should exist'); @@ -358,13 +370,15 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects unknown package manager', () => { assert.throws(() => { pm.setProjectPackageManager('cargo'); }, /Unknown package manager/); - })) passed++; else failed++; + })) passed++; + else failed++; // setPreferredPackageManager tests console.log('\nsetPreferredPackageManager:'); @@ -373,7 +387,8 @@ function runTests() { assert.throws(() => { pm.setPreferredPackageManager('pip'); }, /Unknown package manager/); - })) passed++; else failed++; + })) passed++; + else failed++; // detectFromPackageJson edge cases console.log('\ndetectFromPackageJson (edge cases):'); @@ -387,21 +402,20 @@ function runTests() { } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns null for unknown package manager in packageManager field', () => { const testDir = createTestDir(); try { - fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ - name: 'test', - packageManager: 'deno@1.0' - })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ name: 'test', packageManager: 'deno@1.0' })); const result = pm.detectFromPackageJson(testDir); assert.strictEqual(result, null); } finally { cleanupTestDir(testDir); } - })) passed++; else failed++; + })) passed++; + else failed++; // getExecCommand edge cases console.log('\ngetExecCommand (edge cases):'); @@ -419,7 +433,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getRunCommand additional cases console.log('\ngetRunCommand (additional):'); @@ -436,7 +451,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns correct dev command', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -450,7 +466,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('returns correct custom script command', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -464,18 +481,21 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // DETECTION_PRIORITY tests console.log('\nDETECTION_PRIORITY:'); if (test('has pnpm first', () => { assert.strictEqual(pm.DETECTION_PRIORITY[0], 'pnpm'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('has npm last', () => { assert.strictEqual(pm.DETECTION_PRIORITY[pm.DETECTION_PRIORITY.length - 1], 'npm'); - })) passed++; else failed++; + })) passed++; + else failed++; // getCommandPattern additional cases console.log('\ngetCommandPattern (additional):'); @@ -487,7 +507,8 @@ function runTests() { assert.ok(regex.test('pnpm install'), 'Should match pnpm install'); assert.ok(regex.test('yarn'), 'Should match yarn (install implicit)'); assert.ok(regex.test('bun install'), 'Should match bun install'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('generates pattern for custom action', () => { const pattern = pm.getCommandPattern('lint'); @@ -496,7 +517,8 @@ function runTests() { assert.ok(regex.test('pnpm lint'), 'Should match pnpm lint'); assert.ok(regex.test('yarn lint'), 'Should match yarn lint'); assert.ok(regex.test('bun run lint'), 'Should match bun run lint'); - })) passed++; else failed++; + })) passed++; + else failed++; // getPackageManager robustness tests console.log('\ngetPackageManager (robustness):'); @@ -506,7 +528,6 @@ function runTests() { const claudeDir = path.join(testDir, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), '{not valid json!!!'); - const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; try { delete process.env.CLAUDE_PACKAGE_MANAGER; @@ -520,15 +541,14 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('falls through on project config with unknown PM', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-robust-')); const claudeDir = path.join(testDir, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); - fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), - JSON.stringify({ packageManager: 'nonexistent-pm' })); - + fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), JSON.stringify({ packageManager: 'nonexistent-pm' })); const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; try { delete process.env.CLAUDE_PACKAGE_MANAGER; @@ -541,26 +561,31 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; // getRunCommand validation tests console.log('\ngetRunCommand (validation):'); if (test('rejects empty script name', () => { assert.throws(() => pm.getRunCommand(''), /non-empty string/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects null script name', () => { assert.throws(() => pm.getRunCommand(null), /non-empty string/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects script name with shell metacharacters', () => { assert.throws(() => pm.getRunCommand('test; rm -rf /'), /unsafe characters/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects script name with backticks', () => { assert.throws(() => pm.getRunCommand('test`whoami`'), /unsafe characters/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('accepts scoped package names', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -575,22 +600,26 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getExecCommand validation tests console.log('\ngetExecCommand (validation):'); if (test('rejects empty binary name', () => { assert.throws(() => pm.getExecCommand(''), /non-empty string/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects null binary name', () => { assert.throws(() => pm.getExecCommand(null), /non-empty string/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('rejects binary name with shell metacharacters', () => { assert.throws(() => pm.getExecCommand('prettier; cat /etc/passwd'), /unsafe characters/); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('accepts dotted binary names like tsc', () => { const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; @@ -605,7 +634,8 @@ function runTests() { delete process.env.CLAUDE_PACKAGE_MANAGER; } } - })) passed++; else failed++; + })) passed++; + else failed++; // getPackageManager source detection tests console.log('\ngetPackageManager (source detection):'); @@ -614,9 +644,7 @@ function runTests() { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-projcfg-')); const claudeDir = path.join(testDir, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); - fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), - JSON.stringify({ packageManager: 'pnpm' })); - + fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), JSON.stringify({ packageManager: 'pnpm' })); const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; try { delete process.env.CLAUDE_PACKAGE_MANAGER; @@ -629,22 +657,19 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('project-config takes priority over package.json', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-priority-')); const claudeDir = path.join(testDir, '.claude'); fs.mkdirSync(claudeDir, { recursive: true }); - // Project config says bun - fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), - JSON.stringify({ packageManager: 'bun' })); + fs.writeFileSync(path.join(claudeDir, 'package-manager.json'), JSON.stringify({ packageManager: 'bun' })); // package.json says yarn - fs.writeFileSync(path.join(testDir, 'package.json'), - JSON.stringify({ packageManager: 'yarn@4.0.0' })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ packageManager: 'yarn@4.0.0' })); // Lock file says npm fs.writeFileSync(path.join(testDir, 'package-lock.json'), '{}'); - const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; try { delete process.env.CLAUDE_PACKAGE_MANAGER; @@ -657,16 +682,15 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('package.json takes priority over lock file', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-pj-lock-')); // package.json says yarn - fs.writeFileSync(path.join(testDir, 'package.json'), - JSON.stringify({ packageManager: 'yarn@4.0.0' })); + fs.writeFileSync(path.join(testDir, 'package.json'), JSON.stringify({ packageManager: 'yarn@4.0.0' })); // Lock file says npm fs.writeFileSync(path.join(testDir, 'package-lock.json'), '{}'); - const originalEnv = process.env.CLAUDE_PACKAGE_MANAGER; try { delete process.env.CLAUDE_PACKAGE_MANAGER; @@ -679,7 +703,8 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; if (test('defaults to npm when no config found', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pm-default-')); @@ -695,7 +720,8 @@ function runTests() { } fs.rmSync(testDir, { recursive: true, force: true }); } - })) passed++; else failed++; + })) passed++; + else failed++; // setPreferredPackageManager success console.log('\nsetPreferredPackageManager (success):'); @@ -717,10 +743,15 @@ function runTests() { if (original) { fs.writeFileSync(configPath, original, 'utf8'); } else { - try { fs.unlinkSync(configPath); } catch {} + try { + fs.unlinkSync(configPath); + } catch (_err) { + // ignore + } } } - })) passed++; else failed++; + })) passed++; + else failed++; // getCommandPattern completeness console.log('\ngetCommandPattern (completeness):'); @@ -730,18 +761,20 @@ function runTests() { assert.ok(pattern.includes('npm test'), 'Should include npm test'); assert.ok(pattern.includes('pnpm test'), 'Should include pnpm test'); assert.ok(pattern.includes('bun test'), 'Should include bun test'); - })) passed++; else failed++; + })) passed++; + else failed++; if (test('generates pattern for build command', () => { const pattern = pm.getCommandPattern('build'); assert.ok(pattern.includes('npm run build'), 'Should include npm run build'); assert.ok(pattern.includes('yarn build'), 'Should include yarn build'); - })) passed++; else failed++; + })) passed++; + else failed++; // getRunCommand PM-specific format tests console.log('\ngetRunCommand (PM-specific formats):'); - if (test('pnpm custom script: pnpm