diff --git a/bin/install.js b/bin/install.js index 23de28aeb..aeb55f987 100755 --- a/bin/install.js +++ b/bin/install.js @@ -6812,7 +6812,7 @@ function installSdkIfNeeded() { ? path.join(npmPrefix, 'node_modules') : path.join(npmPrefix, 'lib', 'node_modules'); const cliPath = path.join(globalModulesDir, sdkName, 'dist', 'cli.js'); - try { fs.chmodSync(cliPath, 0o755); } catch (e) { /* Windows / path not found */ } + try { fs.chmodSync(cliPath, 0o755); } catch (e) { if (process.platform !== 'win32') throw e; } } } catch (e) { /* Non-fatal: PATH verification in step 4 will catch any real failure */ } diff --git a/tests/bug-2525-sdk-executable-bit.test.cjs b/tests/bug-2525-sdk-executable-bit.test.cjs new file mode 100644 index 000000000..5dcd9c5b3 --- /dev/null +++ b/tests/bug-2525-sdk-executable-bit.test.cjs @@ -0,0 +1,82 @@ +/** + * Regression test for bug #2525 + * + * After running the GSD installer on macOS with a Homebrew npm prefix, + * `gsd-sdk` is installed but `command -v gsd-sdk` returns nothing because + * `dist/cli.js` is installed with mode 644 (no executable bit). tsc emits + * .js files as 644, and `npm install -g .` creates the bin symlink without + * chmod-ing the target. The kernel then refuses to exec the file. + * + * Fix: between the `npm run build` step and `npm install -g .`, chmod + * dist/cli.js to 0o755. This mirrors the pattern already used for hook + * files at lines 5838, 5846, 5959, and 5965. + */ + +'use strict'; + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js'); + +describe('bug #2525: dist/cli.js chmod 0o755 after tsc build', () => { + test('install.js exists', () => { + assert.ok(fs.existsSync(INSTALL_PATH), 'bin/install.js should exist'); + }); + + test('chmodSync is called for dist/cli.js after the build step', () => { + const content = fs.readFileSync(INSTALL_PATH, 'utf-8'); + // Find the installSdkIfNeeded function body + const fnStart = content.indexOf('function installSdkIfNeeded()'); + assert.ok(fnStart !== -1, 'installSdkIfNeeded function must exist in bin/install.js'); + + // Find the closing brace of the function (next top-level function definition) + const nextFnIdx = content.indexOf('\nfunction ', fnStart + 1); + const fnEnd = nextFnIdx === -1 ? content.length : nextFnIdx; + const fnBody = content.slice(fnStart, fnEnd); + + // Locate the build step + const buildStep = fnBody.indexOf("'run', 'build'"); + assert.ok(buildStep !== -1, "installSdkIfNeeded must contain the 'run', 'build' spawn call"); + + // Locate the global install step + const globalStep = fnBody.indexOf("'install', '-g', '.'"); + assert.ok(globalStep !== -1, "installSdkIfNeeded must contain the 'install', '-g', '.' spawn call"); + + // Locate chmodSync for dist/cli.js + const chmodIdx = fnBody.indexOf("chmodSync"); + assert.ok(chmodIdx !== -1, "installSdkIfNeeded must call chmodSync to set the executable bit on dist/cli.js"); + + // The path may be assembled via path.join(sdkDir, 'dist', 'cli.js') so check + // for the component strings rather than the joined slash form. + const cliPathIdx = fnBody.indexOf("'cli.js'"); + assert.ok(cliPathIdx !== -1, "installSdkIfNeeded must reference 'cli.js' (via path.join or literal) for the chmod call"); + + // chmodSync must appear AFTER the build step + assert.ok( + chmodIdx > buildStep, + 'chmodSync for dist/cli.js must appear AFTER the npm run build step' + ); + + // chmodSync must appear AFTER the global install step + assert.ok( + chmodIdx > globalStep, + 'chmodSync for dist/cli.js must appear AFTER the npm install -g . step' + ); + }); + + test('chmod mode is 0o755', () => { + const content = fs.readFileSync(INSTALL_PATH, 'utf-8'); + const fnStart = content.indexOf('function installSdkIfNeeded()'); + const nextFnIdx = content.indexOf('\nfunction ', fnStart + 1); + const fnEnd = nextFnIdx === -1 ? content.length : nextFnIdx; + const fnBody = content.slice(fnStart, fnEnd); + + assert.ok( + fnBody.includes('0o755'), + "chmodSync call in installSdkIfNeeded must use mode 0o755 (not 0o644 or a bare number)" + ); + }); +});