diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..443ffe6b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +# Normalize all text files to LF on commit and checkout. +# This prevents CRLF shebang lines in bundled scripts from breaking +# the MCP server on macOS/Linux when built on Windows. Fixes #1342. +* text=auto eol=lf + +# Compiled plugin scripts must always be LF — CRLF in the shebang +# causes "env: node\r: No such file or directory" on non-Windows hosts. +plugin/scripts/*.cjs eol=lf +plugin/scripts/*.js eol=lf + +# Explicitly mark binary assets so git never modifies them. +*.png binary +*.jpg binary +*.jpeg binary +*.ico binary +*.gif binary +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary +*.otf binary diff --git a/tests/plugin-scripts-line-endings.test.ts b/tests/plugin-scripts-line-endings.test.ts new file mode 100644 index 00000000..d3a63872 --- /dev/null +++ b/tests/plugin-scripts-line-endings.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect } from 'bun:test'; +import { readFileSync, existsSync } from 'fs'; +import { join } from 'path'; + +/** + * Regression tests for issue #1342. + * + * Bundled plugin scripts use a shebang line (#!/usr/bin/env node or #!/usr/bin/env bun). + * If those files are committed with Windows CRLF line endings, the shebang becomes + * "#!/usr/bin/env node\r" which fails with: + * env: node\r: No such file or directory + * on macOS and Linux, breaking the MCP server and all hook scripts. + * + * These tests guard against CRLF line endings being re-introduced into the + * committed plugin scripts (e.g. by a Windows contributor without .gitattributes). + */ + +const SCRIPTS_DIR = join(import.meta.dir, '..', 'plugin', 'scripts'); + +const SHEBANG_SCRIPTS = [ + 'mcp-server.cjs', + 'worker-service.cjs', + 'bun-runner.js', + 'smart-install.js', + 'worker-cli.js', +]; + +describe('plugin/scripts line endings (#1342)', () => { + for (const filename of SHEBANG_SCRIPTS) { + const filePath = join(SCRIPTS_DIR, filename); + + it(`${filename} shebang line must not contain CRLF`, () => { + if (!existsSync(filePath)) { + // Skip if not yet built (CI may not have run the build step) + return; + } + const content = readFileSync(filePath, 'binary'); + const firstLine = content.split('\n')[0]; + // CRLF would leave a trailing \r on the shebang line + expect(firstLine.endsWith('\r')).toBe(false); + }); + + it(`${filename} must not contain any CRLF sequences`, () => { + if (!existsSync(filePath)) { + return; + } + const content = readFileSync(filePath, 'binary'); + expect(content.includes('\r\n')).toBe(false); + }); + } +});