mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat(platform): add executable permission validation to node-finder
- Add isExecutable() helper to verify files have execute permission - On Unix: uses fs.constants.X_OK to check execute permission - On Windows: only checks file existence (X_OK not meaningful) - Replace fs.existsSync with isExecutable for all node path checks - Add JSDoc comment documenting version sorting limitations - Add test to verify found node binary is executable 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,9 +44,32 @@ export interface NodeFinderOptions {
|
||||
logger?: (message: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists and is executable
|
||||
* On Windows, only checks existence (X_OK is not meaningful)
|
||||
*/
|
||||
function isExecutable(filePath: string): boolean {
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// On Windows, fs.constants.X_OK is not meaningful - just check existence
|
||||
fs.accessSync(filePath, fs.constants.F_OK);
|
||||
} else {
|
||||
// On Unix-like systems, check for execute permission
|
||||
fs.accessSync(filePath, fs.constants.X_OK);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Node.js executable from version manager directories (NVM, fnm)
|
||||
* Uses semantic version sorting to prefer the latest version
|
||||
*
|
||||
* Note: Version sorting uses localeCompare with numeric:true which handles most cases
|
||||
* correctly (e.g., v18.17.0 > v18.9.0) but may not perfectly sort pre-release versions
|
||||
* (e.g., v20.0.0-beta vs v19.9.9). This is acceptable as we prefer the latest stable.
|
||||
*/
|
||||
function findNodeFromVersionManager(
|
||||
basePath: string,
|
||||
@@ -63,7 +86,7 @@ function findNodeFromVersionManager(
|
||||
|
||||
for (const version of versions) {
|
||||
const nodePath = path.join(basePath, version, binSubpath);
|
||||
if (fs.existsSync(nodePath)) {
|
||||
if (isExecutable(nodePath)) {
|
||||
return nodePath;
|
||||
}
|
||||
}
|
||||
@@ -87,13 +110,13 @@ function findNodeMacOS(homeDir: string): NodeFinderResult | null {
|
||||
];
|
||||
|
||||
for (const nodePath of homebrewPaths) {
|
||||
if (fs.existsSync(nodePath)) {
|
||||
if (isExecutable(nodePath)) {
|
||||
return { nodePath, source: 'homebrew' };
|
||||
}
|
||||
}
|
||||
|
||||
// System Node
|
||||
if (fs.existsSync('/usr/bin/node')) {
|
||||
if (isExecutable('/usr/bin/node')) {
|
||||
return { nodePath: '/usr/bin/node', source: 'system' };
|
||||
}
|
||||
|
||||
@@ -133,7 +156,7 @@ function findNodeLinux(homeDir: string): NodeFinderResult | null {
|
||||
];
|
||||
|
||||
for (const nodePath of systemPaths) {
|
||||
if (fs.existsSync(nodePath)) {
|
||||
if (isExecutable(nodePath)) {
|
||||
return { nodePath, source: 'system' };
|
||||
}
|
||||
}
|
||||
@@ -172,7 +195,7 @@ function findNodeWindows(homeDir: string): NodeFinderResult | null {
|
||||
];
|
||||
|
||||
for (const nodePath of programFilesPaths) {
|
||||
if (fs.existsSync(nodePath)) {
|
||||
if (isExecutable(nodePath)) {
|
||||
return { nodePath, source: 'program-files' };
|
||||
}
|
||||
}
|
||||
@@ -206,7 +229,7 @@ function findNodeWindows(homeDir: string): NodeFinderResult | null {
|
||||
|
||||
// Scoop installation
|
||||
const scoopPath = path.join(homeDir, 'scoop', 'apps', 'nodejs', 'current', 'node.exe');
|
||||
if (fs.existsSync(scoopPath)) {
|
||||
if (isExecutable(scoopPath)) {
|
||||
return { nodePath: scoopPath, source: 'scoop' };
|
||||
}
|
||||
|
||||
@@ -216,7 +239,7 @@ function findNodeWindows(homeDir: string): NodeFinderResult | null {
|
||||
'bin',
|
||||
'node.exe'
|
||||
);
|
||||
if (fs.existsSync(chocoPath)) {
|
||||
if (isExecutable(chocoPath)) {
|
||||
return { nodePath: chocoPath, source: 'chocolatey' };
|
||||
}
|
||||
|
||||
@@ -240,8 +263,8 @@ function findNodeViaShell(
|
||||
// 'where' on Windows can return multiple lines, take the first
|
||||
const nodePath = result.split(/\r?\n/)[0];
|
||||
|
||||
// Validate path: check for null bytes (security) and existence
|
||||
if (nodePath && !nodePath.includes('\x00') && fs.existsSync(nodePath)) {
|
||||
// Validate path: check for null bytes (security) and executable permission
|
||||
if (nodePath && !nodePath.includes('\x00') && isExecutable(nodePath)) {
|
||||
return {
|
||||
nodePath,
|
||||
source: platform === 'win32' ? 'where' : 'which',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { findNodeExecutable, buildEnhancedPath } from '../src/node-finder.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
describe('node-finder', () => {
|
||||
describe('findNodeExecutable', () => {
|
||||
@@ -51,6 +52,25 @@ describe('node-finder', () => {
|
||||
];
|
||||
expect(validSources).toContain(result.source);
|
||||
});
|
||||
|
||||
it('should find an executable node binary', () => {
|
||||
const result = findNodeExecutable();
|
||||
|
||||
// Skip this test if fallback is used (node not found via path search)
|
||||
if (result.source === 'fallback') {
|
||||
expect(result.nodePath).toBe('node');
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify the found path is actually executable
|
||||
if (process.platform === 'win32') {
|
||||
// On Windows, just check file exists (X_OK is not meaningful)
|
||||
expect(() => fs.accessSync(result.nodePath, fs.constants.F_OK)).not.toThrow();
|
||||
} else {
|
||||
// On Unix-like systems, verify execute permission
|
||||
expect(() => fs.accessSync(result.nodePath, fs.constants.X_OK)).not.toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildEnhancedPath', () => {
|
||||
|
||||
Reference in New Issue
Block a user