From df70cf0488f4ba65a121cd8307f01b02b8ffc098 Mon Sep 17 00:00:00 2001 From: bcotrim Date: Mon, 22 Dec 2025 10:30:22 +0000 Subject: [PATCH 01/16] add bundled node --- .gitignore | 4 ++ forge.config.ts | 9 +++-- scripts/download-node-binary.sh | 55 ++++++++++++++++++++++++++ src/modules/cli/lib/execute-command.ts | 9 ++--- src/storage/paths.ts | 12 ++++++ 5 files changed, 80 insertions(+), 9 deletions(-) create mode 100755 scripts/download-node-binary.sh diff --git a/.gitignore b/.gitignore index b088b6d4bd..698c3381b8 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,7 @@ artifacts .cursor/ .claude/ CLAUDE.md + +# Bundled Node binary (downloaded during build) +bin/node +bin/node.exe diff --git a/forge.config.ts b/forge.config.ts index c1238a4aff..8c6f85f556 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -114,9 +114,10 @@ const config: ForgeConfig = { ], plugins: [ new AutoUnpackNativesPlugin( {} ) ], hooks: { - prePackage: async () => { - console.log( "Ensuring latest WordPress zip isn't included in production build ..." ); + prePackage: async ( _forgeConfig, platform, arch ) => { + const execAsync = promisify( exec ); + console.log( "Ensuring latest WordPress zip isn't included in production build ..." ); const zipPath = path.join( __dirname, 'wp-files', 'latest.zip' ); try { fs.unlinkSync( zipPath ); @@ -125,8 +126,10 @@ const config: ForgeConfig = { } console.log( 'Building CLI ...' ); - const execAsync = promisify( exec ); await execAsync( 'npm run cli:build' ); + + console.log( `Downloading Node.js binary for ${ platform }-${ arch }...` ); + await execAsync( `./scripts/download-node-binary.sh ${ platform } ${ arch }` ); }, }, }; diff --git a/scripts/download-node-binary.sh b/scripts/download-node-binary.sh new file mode 100755 index 0000000000..6648bb760e --- /dev/null +++ b/scripts/download-node-binary.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -e + +# Download Node.js binary for bundling with Studio +# Usage: ./scripts/download-node-binary.sh +# Example: ./scripts/download-node-binary.sh darwin arm64 + +NODE_VERSION="v22.12.0" # LTS version +PLATFORM="${1:-darwin}" +ARCH="${2:-arm64}" + +# Map architecture names +case "$ARCH" in + arm64) NODE_ARCH="arm64" ;; + x64) NODE_ARCH="x64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; +esac + +# Map platform names +case "$PLATFORM" in + darwin) NODE_PLATFORM="darwin" ;; + win32) NODE_PLATFORM="win" ;; + linux) NODE_PLATFORM="linux" ;; + *) echo "Unsupported platform: $PLATFORM"; exit 1 ;; +esac + +BIN_DIR="$(dirname "$0")/../bin" +mkdir -p "$BIN_DIR" + +if [ "$NODE_PLATFORM" = "win" ]; then + FILENAME="node-${NODE_VERSION}-${NODE_PLATFORM}-${NODE_ARCH}.zip" + URL="https://nodejs.org/dist/${NODE_VERSION}/${FILENAME}" + + echo "Downloading Node.js ${NODE_VERSION} for ${NODE_PLATFORM}-${NODE_ARCH}..." + curl -L "$URL" -o "/tmp/${FILENAME}" + + echo "Extracting node.exe..." + unzip -j "/tmp/${FILENAME}" "node-${NODE_VERSION}-${NODE_PLATFORM}-${NODE_ARCH}/node.exe" -d "$BIN_DIR" + rm "/tmp/${FILENAME}" +else + FILENAME="node-${NODE_VERSION}-${NODE_PLATFORM}-${NODE_ARCH}.tar.gz" + URL="https://nodejs.org/dist/${NODE_VERSION}/${FILENAME}" + + echo "Downloading Node.js ${NODE_VERSION} for ${NODE_PLATFORM}-${NODE_ARCH}..." + curl -L "$URL" -o "/tmp/${FILENAME}" + + echo "Extracting node binary..." + tar -xzf "/tmp/${FILENAME}" -C /tmp + cp "/tmp/node-${NODE_VERSION}-${NODE_PLATFORM}-${NODE_ARCH}/bin/node" "$BIN_DIR/node" + chmod +x "$BIN_DIR/node" + rm -rf "/tmp/${FILENAME}" "/tmp/node-${NODE_VERSION}-${NODE_PLATFORM}-${NODE_ARCH}" +fi + +echo "Node.js binary installed to $BIN_DIR" +ls -la "$BIN_DIR" diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 0cdb0d01df..83bc8e20cf 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -1,7 +1,7 @@ import { fork, ChildProcess, StdioOptions } from 'node:child_process'; import EventEmitter from 'node:events'; import * as Sentry from '@sentry/electron/main'; -import { getCliPath } from 'src/storage/paths'; +import { getBundledNodeBinaryPath, getCliPath } from 'src/storage/paths'; export interface CliCommandResult { stdout: string; @@ -55,13 +55,10 @@ export function executeCliCommand( stdio = [ 'ignore', 'ignore', 'ignore', 'ipc' ]; } - // Using Electron's utilityProcess.fork API gave us issues with the child process never exiting const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { stdio, - env: { - ...process.env, - ELECTRON_RUN_AS_NODE: '1', - }, + execPath: getBundledNodeBinaryPath(), + env: process.env, } ); const eventEmitter = new CliCommandEventEmitter(); diff --git a/src/storage/paths.ts b/src/storage/paths.ts index e1fe93e0a3..0633b735b1 100644 --- a/src/storage/paths.ts +++ b/src/storage/paths.ts @@ -66,6 +66,18 @@ export function getCliPath(): string { : path.join( getResourcesPath(), 'cli', 'main.js' ); } +export function getBundledNodeBinaryPath(): string { + const nodeBinaryName = process.platform === 'win32' ? 'node.exe' : 'node'; + + if ( process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' ) { + // In development, use system Node + const { execSync } = require( 'child_process' ); + return execSync( 'which node', { encoding: 'utf-8' } ).trim(); + } + + return path.join( getResourcesPath(), 'bin', nodeBinaryName ); +} + function getAppDataPath(): string { if ( inChildProcess() ) { if ( ! process.env.STUDIO_APP_DATA_PATH ) { From dd6bce14110454d5245602d6b7d3ecdfa624add8 Mon Sep 17 00:00:00 2001 From: bcotrim Date: Mon, 22 Dec 2025 16:33:33 +0000 Subject: [PATCH 02/16] change node download script --- forge.config.ts | 2 +- scripts/download-node-binary.js | 176 ++++++++++++++++++++++++++++++++ scripts/download-node-binary.sh | 55 ---------- vite.cli.config.ts | 5 +- 4 files changed, 180 insertions(+), 58 deletions(-) create mode 100644 scripts/download-node-binary.js delete mode 100755 scripts/download-node-binary.sh diff --git a/forge.config.ts b/forge.config.ts index 8c6f85f556..9770831291 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -129,7 +129,7 @@ const config: ForgeConfig = { await execAsync( 'npm run cli:build' ); console.log( `Downloading Node.js binary for ${ platform }-${ arch }...` ); - await execAsync( `./scripts/download-node-binary.sh ${ platform } ${ arch }` ); + await execAsync( `node ./scripts/download-node-binary.js ${ platform } ${ arch }` ); }, }, }; diff --git a/scripts/download-node-binary.js b/scripts/download-node-binary.js new file mode 100644 index 0000000000..d449e86e99 --- /dev/null +++ b/scripts/download-node-binary.js @@ -0,0 +1,176 @@ +#!/usr/bin/env node +/** + * Download Node.js binary for bundling with Studio + * Usage: node scripts/download-node-binary.js + * Example: node scripts/download-node-binary.js darwin arm64 + */ + +const https = require( 'https' ); +const fs = require( 'fs' ); +const path = require( 'path' ); +const { execSync } = require( 'child_process' ); +const os = require( 'os' ); + +const NODE_VERSION = 'v22.12.0'; // LTS version + +const platform = process.argv[ 2 ] || process.platform; +const arch = process.argv[ 3 ] || process.arch; + +// Map platform names to Node.js download naming +const platformMap = { + darwin: 'darwin', + win32: 'win', + linux: 'linux', +}; + +// Map architecture names +const archMap = { + arm64: 'arm64', + x64: 'x64', +}; + +const nodePlatform = platformMap[ platform ]; +const nodeArch = archMap[ arch ]; + +if ( ! nodePlatform ) { + console.error( `Unsupported platform: ${ platform }` ); + process.exit( 1 ); +} + +if ( ! nodeArch ) { + console.error( `Unsupported architecture: ${ arch }` ); + process.exit( 1 ); +} + +const binDir = path.join( __dirname, '..', 'bin' ); +const tmpDir = os.tmpdir(); + +// Ensure bin directory exists +if ( ! fs.existsSync( binDir ) ) { + fs.mkdirSync( binDir, { recursive: true } ); +} + +const isWindows = nodePlatform === 'win'; +const ext = isWindows ? 'zip' : 'tar.gz'; +const filename = `node-${ NODE_VERSION }-${ nodePlatform }-${ nodeArch }.${ ext }`; +const url = `https://nodejs.org/dist/${ NODE_VERSION }/${ filename }`; +const downloadPath = path.join( tmpDir, filename ); + +function download( downloadUrl, dest ) { + return new Promise( ( resolve, reject ) => { + console.log( `Downloading Node.js ${ NODE_VERSION } for ${ nodePlatform }-${ nodeArch }...` ); + console.log( `URL: ${ downloadUrl }` ); + + const file = fs.createWriteStream( dest ); + + https + .get( downloadUrl, ( response ) => { + // Handle redirects + if ( response.statusCode === 302 || response.statusCode === 301 ) { + file.close(); + fs.unlinkSync( dest ); + return download( response.headers.location, dest ).then( resolve ).catch( reject ); + } + + if ( response.statusCode !== 200 ) { + reject( new Error( `Failed to download: HTTP ${ response.statusCode }` ) ); + return; + } + + const totalSize = parseInt( response.headers[ 'content-length' ], 10 ); + let downloadedSize = 0; + + response.on( 'data', ( chunk ) => { + downloadedSize += chunk.length; + const percent = ( ( downloadedSize / totalSize ) * 100 ).toFixed( 1 ); + process.stdout.write( `\rDownloading... ${ percent }%` ); + } ); + + response.pipe( file ); + + file.on( 'finish', () => { + file.close(); + console.log( '\nDownload complete.' ); + resolve(); + } ); + } ) + .on( 'error', ( err ) => { + fs.unlink( dest, () => {} ); + reject( err ); + } ); + } ); +} + +function extractTarGz( archivePath, destDir, binaryName ) { + console.log( 'Extracting node binary...' ); + + const extractDir = path.join( tmpDir, `node-${ NODE_VERSION }-${ nodePlatform }-${ nodeArch }` ); + + // Use tar command (available on macOS and Linux) + execSync( `tar -xzf "${ archivePath }" -C "${ tmpDir }"` ); + + const sourcePath = path.join( extractDir, 'bin', 'node' ); + const destPath = path.join( destDir, binaryName ); + + fs.copyFileSync( sourcePath, destPath ); + fs.chmodSync( destPath, 0o755 ); + + // Cleanup + fs.unlinkSync( archivePath ); + fs.rmSync( extractDir, { recursive: true } ); +} + +function extractZip( archivePath, destDir, binaryName ) { + console.log( 'Extracting node.exe...' ); + + const extractDir = path.join( tmpDir, `node-${ NODE_VERSION }-${ nodePlatform }-${ nodeArch }` ); + + // Use PowerShell on Windows, unzip on others + if ( process.platform === 'win32' ) { + execSync( + `powershell -Command "Expand-Archive -Path '${ archivePath }' -DestinationPath '${ tmpDir }' -Force"` + ); + } else { + execSync( `unzip -q "${ archivePath }" -d "${ tmpDir }"` ); + } + + const sourcePath = path.join( extractDir, 'node.exe' ); + const destPath = path.join( destDir, binaryName ); + + fs.copyFileSync( sourcePath, destPath ); + + // Cleanup + fs.unlinkSync( archivePath ); + fs.rmSync( extractDir, { recursive: true } ); +} + +async function main() { + try { + await download( url, downloadPath ); + + const binaryName = isWindows ? 'node.exe' : 'node'; + + if ( isWindows ) { + extractZip( downloadPath, binDir, binaryName ); + } else { + extractTarGz( downloadPath, binDir, binaryName ); + } + + console.log( `\nNode.js binary installed to ${ binDir }` ); + + // List the bin directory contents + const files = fs.readdirSync( binDir ); + console.log( '\nBin directory contents:' ); + for ( const file of files ) { + const filePath = path.join( binDir, file ); + const stats = fs.statSync( filePath ); + const size = ( stats.size / 1024 / 1024 ).toFixed( 2 ); + console.log( ` ${ file } (${ size } MB)` ); + } + } catch ( error ) { + console.error( 'Error:', error.message ); + process.exit( 1 ); + } +} + +main(); diff --git a/scripts/download-node-binary.sh b/scripts/download-node-binary.sh deleted file mode 100755 index 6648bb760e..0000000000 --- a/scripts/download-node-binary.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -set -e - -# Download Node.js binary for bundling with Studio -# Usage: ./scripts/download-node-binary.sh -# Example: ./scripts/download-node-binary.sh darwin arm64 - -NODE_VERSION="v22.12.0" # LTS version -PLATFORM="${1:-darwin}" -ARCH="${2:-arm64}" - -# Map architecture names -case "$ARCH" in - arm64) NODE_ARCH="arm64" ;; - x64) NODE_ARCH="x64" ;; - *) echo "Unsupported architecture: $ARCH"; exit 1 ;; -esac - -# Map platform names -case "$PLATFORM" in - darwin) NODE_PLATFORM="darwin" ;; - win32) NODE_PLATFORM="win" ;; - linux) NODE_PLATFORM="linux" ;; - *) echo "Unsupported platform: $PLATFORM"; exit 1 ;; -esac - -BIN_DIR="$(dirname "$0")/../bin" -mkdir -p "$BIN_DIR" - -if [ "$NODE_PLATFORM" = "win" ]; then - FILENAME="node-${NODE_VERSION}-${NODE_PLATFORM}-${NODE_ARCH}.zip" - URL="https://nodejs.org/dist/${NODE_VERSION}/${FILENAME}" - - echo "Downloading Node.js ${NODE_VERSION} for ${NODE_PLATFORM}-${NODE_ARCH}..." - curl -L "$URL" -o "/tmp/${FILENAME}" - - echo "Extracting node.exe..." - unzip -j "/tmp/${FILENAME}" "node-${NODE_VERSION}-${NODE_PLATFORM}-${NODE_ARCH}/node.exe" -d "$BIN_DIR" - rm "/tmp/${FILENAME}" -else - FILENAME="node-${NODE_VERSION}-${NODE_PLATFORM}-${NODE_ARCH}.tar.gz" - URL="https://nodejs.org/dist/${NODE_VERSION}/${FILENAME}" - - echo "Downloading Node.js ${NODE_VERSION} for ${NODE_PLATFORM}-${NODE_ARCH}..." - curl -L "$URL" -o "/tmp/${FILENAME}" - - echo "Extracting node binary..." - tar -xzf "/tmp/${FILENAME}" -C /tmp - cp "/tmp/node-${NODE_VERSION}-${NODE_PLATFORM}-${NODE_ARCH}/bin/node" "$BIN_DIR/node" - chmod +x "$BIN_DIR/node" - rm -rf "/tmp/${FILENAME}" "/tmp/node-${NODE_VERSION}-${NODE_PLATFORM}-${NODE_ARCH}" -fi - -echo "Node.js binary installed to $BIN_DIR" -ls -la "$BIN_DIR" diff --git a/vite.cli.config.ts b/vite.cli.config.ts index 412c9e8f6c..d18852eed6 100644 --- a/vite.cli.config.ts +++ b/vite.cli.config.ts @@ -20,13 +20,14 @@ export default defineConfig( { } ), ] : [] ), + // Copy cli/node_modules but exclude .bin directory (symlinks cause issues on Windows) ...( existsSync( cliNodeModulesPath ) ? [ viteStaticCopy( { targets: [ { - src: 'cli/node_modules', - dest: '.', + src: 'cli/node_modules/!(*.bin|.bin)', + dest: 'node_modules', }, ], } ), From d1af8fed93289b7c10bcdece2472d2962bb8656a Mon Sep 17 00:00:00 2001 From: bcotrim Date: Mon, 22 Dec 2025 18:06:14 +0000 Subject: [PATCH 03/16] Fix cli/node_modules copy pattern for Windows compatibility --- vite.cli.config.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vite.cli.config.ts b/vite.cli.config.ts index d18852eed6..412c9e8f6c 100644 --- a/vite.cli.config.ts +++ b/vite.cli.config.ts @@ -20,14 +20,13 @@ export default defineConfig( { } ), ] : [] ), - // Copy cli/node_modules but exclude .bin directory (symlinks cause issues on Windows) ...( existsSync( cliNodeModulesPath ) ? [ viteStaticCopy( { targets: [ { - src: 'cli/node_modules/!(*.bin|.bin)', - dest: 'node_modules', + src: 'cli/node_modules', + dest: '.', }, ], } ), From 25b53bdaa0fbf72805ef9dec4f95e4bb1173a781 Mon Sep 17 00:00:00 2001 From: bcotrim Date: Tue, 23 Dec 2025 09:48:42 +0000 Subject: [PATCH 04/16] fix node command on Windows --- src/storage/paths.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/storage/paths.ts b/src/storage/paths.ts index 0633b735b1..8ffbd0b868 100644 --- a/src/storage/paths.ts +++ b/src/storage/paths.ts @@ -72,7 +72,9 @@ export function getBundledNodeBinaryPath(): string { if ( process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' ) { // In development, use system Node const { execSync } = require( 'child_process' ); - return execSync( 'which node', { encoding: 'utf-8' } ).trim(); + const command = process.platform === 'win32' ? 'where node' : 'which node'; + // `where` on Windows can return multiple paths, take the first one + return execSync( command, { encoding: 'utf-8' } ).trim().split( /\r?\n/ )[ 0 ]; } return path.join( getResourcesPath(), 'bin', nodeBinaryName ); From 53af43f66ba067b6beddfa09b19e57bba4f91521 Mon Sep 17 00:00:00 2001 From: bcotrim Date: Tue, 23 Dec 2025 11:33:03 +0000 Subject: [PATCH 05/16] hide windows on child processes --- cli/lib/pm2-manager.ts | 1 + src/modules/cli/lib/execute-command.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/cli/lib/pm2-manager.ts b/cli/lib/pm2-manager.ts index 7d80f3d317..d7ed3bf806 100644 --- a/cli/lib/pm2-manager.ts +++ b/cli/lib/pm2-manager.ts @@ -165,6 +165,7 @@ export async function startProcess( script: scriptPath, exec_mode: 'fork', autorestart: false, + windowsHide: true, // Merge process.env with custom env to ensure child processes inherit // necessary environment variables (PATH, HOME, E2E vars, etc.) env: { ...process.env, ...env } as Record< string, string >, diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 83bc8e20cf..83386ccb8a 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -59,6 +59,7 @@ export function executeCliCommand( stdio, execPath: getBundledNodeBinaryPath(), env: process.env, + windowsHide: true, } ); const eventEmitter = new CliCommandEventEmitter(); From 7ac84b7d72bf88a305a1a7b49e45c7c7b72e2970 Mon Sep 17 00:00:00 2001 From: bcotrim Date: Tue, 23 Dec 2025 12:26:13 +0000 Subject: [PATCH 06/16] ensure nvrm version is used --- scripts/download-node-binary.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/scripts/download-node-binary.js b/scripts/download-node-binary.js index d449e86e99..74571081c2 100644 --- a/scripts/download-node-binary.js +++ b/scripts/download-node-binary.js @@ -11,7 +11,20 @@ const path = require( 'path' ); const { execSync } = require( 'child_process' ); const os = require( 'os' ); -const NODE_VERSION = 'v22.12.0'; // LTS version +const LTS_FALLBACK = 'v22.12.0'; + +function getNodeVersion() { + const nvmrcPath = path.join( __dirname, '..', '.nvmrc' ); + if ( fs.existsSync( nvmrcPath ) ) { + const version = fs.readFileSync( nvmrcPath, 'utf-8' ).trim(); + // Ensure version starts with 'v' + return version.startsWith( 'v' ) ? version : `v${ version }`; + } + console.log( `.nvmrc not found, using fallback version ${ LTS_FALLBACK }` ); + return LTS_FALLBACK; +} + +const NODE_VERSION = getNodeVersion(); const platform = process.argv[ 2 ] || process.platform; const arch = process.argv[ 3 ] || process.arch; From 09ab0f4eef5ad42cef6d9f315240ea373ba1973b Mon Sep 17 00:00:00 2001 From: bcotrim Date: Tue, 23 Dec 2025 16:23:39 +0000 Subject: [PATCH 07/16] logs --- cli/lib/wordpress-server-manager.ts | 3 +++ cli/wordpress-server-child.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index 3443e68d0e..61db0291e7 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -228,6 +228,7 @@ async function sendMessage( const validationResult = childMessagePm2Schema.safeParse( packet ); if ( ! validationResult.success ) { // Don't reject on validation errors - other processes may send messages we don't handle + console.error( `[sendMessage] Received invalid packet:`, JSON.stringify( packet ).slice( 0, 200 ) ); return; } @@ -237,6 +238,8 @@ async function sendMessage( return; } + console.error( `[sendMessage] Received message topic: ${validPacket.raw.topic}, pmId: ${pmId}` ); + if ( validPacket.raw.topic === 'activity' ) { lastActivityTimestamp = Date.now(); } else if ( validPacket.raw.topic === 'console-message' ) { diff --git a/cli/wordpress-server-child.ts b/cli/wordpress-server-child.ts index 5fd143ffe3..132253d107 100644 --- a/cli/wordpress-server-child.ts +++ b/cli/wordpress-server-child.ts @@ -407,7 +407,9 @@ async function ipcMessageHandler( packet: unknown ) { topic: 'result', result, }; + logToConsole( `Sending result message for ${validMessage.topic}, messageId: ${validMessage.messageId}` ); process.send!( response ); + logToConsole( `Result message sent successfully` ); } catch ( error ) { errorToConsole( `Error handling message ${ validMessage.topic }:`, error ); sendErrorMessage( validMessage.messageId, error ); From c2016e4407a33a313a373792f6ec1bec30d03fe3 Mon Sep 17 00:00:00 2001 From: bcotrim Date: Tue, 23 Dec 2025 18:28:27 +0000 Subject: [PATCH 08/16] fix lint and tests --- cli/lib/pm2-manager.ts | 2 +- cli/lib/wordpress-server-manager.ts | 3 --- cli/wordpress-server-child.ts | 2 -- src/modules/cli/lib/execute-command.ts | 7 ++++--- src/storage/paths.ts | 9 ++++----- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/cli/lib/pm2-manager.ts b/cli/lib/pm2-manager.ts index d7ed3bf806..a34e586979 100644 --- a/cli/lib/pm2-manager.ts +++ b/cli/lib/pm2-manager.ts @@ -159,7 +159,7 @@ export async function startProcess( env: Record< string, string > = {} ): Promise< ProcessDescription > { return new Promise( ( resolve, reject ) => { - const processConfig: StartOptions = { + const processConfig: StartOptions & { windowsHide?: boolean } = { name: processName, interpreter: process.execPath, script: scriptPath, diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts index 61db0291e7..3443e68d0e 100644 --- a/cli/lib/wordpress-server-manager.ts +++ b/cli/lib/wordpress-server-manager.ts @@ -228,7 +228,6 @@ async function sendMessage( const validationResult = childMessagePm2Schema.safeParse( packet ); if ( ! validationResult.success ) { // Don't reject on validation errors - other processes may send messages we don't handle - console.error( `[sendMessage] Received invalid packet:`, JSON.stringify( packet ).slice( 0, 200 ) ); return; } @@ -238,8 +237,6 @@ async function sendMessage( return; } - console.error( `[sendMessage] Received message topic: ${validPacket.raw.topic}, pmId: ${pmId}` ); - if ( validPacket.raw.topic === 'activity' ) { lastActivityTimestamp = Date.now(); } else if ( validPacket.raw.topic === 'console-message' ) { diff --git a/cli/wordpress-server-child.ts b/cli/wordpress-server-child.ts index 132253d107..5fd143ffe3 100644 --- a/cli/wordpress-server-child.ts +++ b/cli/wordpress-server-child.ts @@ -407,9 +407,7 @@ async function ipcMessageHandler( packet: unknown ) { topic: 'result', result, }; - logToConsole( `Sending result message for ${validMessage.topic}, messageId: ${validMessage.messageId}` ); process.send!( response ); - logToConsole( `Result message sent successfully` ); } catch ( error ) { errorToConsole( `Error handling message ${ validMessage.topic }:`, error ); sendErrorMessage( validMessage.messageId, error ); diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 83386ccb8a..3a0ac77042 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -1,4 +1,4 @@ -import { fork, ChildProcess, StdioOptions } from 'node:child_process'; +import { fork, ChildProcess, StdioOptions, ForkOptions } from 'node:child_process'; import EventEmitter from 'node:events'; import * as Sentry from '@sentry/electron/main'; import { getBundledNodeBinaryPath, getCliPath } from 'src/storage/paths'; @@ -55,12 +55,13 @@ export function executeCliCommand( stdio = [ 'ignore', 'ignore', 'ignore', 'ipc' ]; } - const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { + const forkOptions: ForkOptions & { windowsHide?: boolean } = { stdio, execPath: getBundledNodeBinaryPath(), env: process.env, windowsHide: true, - } ); + }; + const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], forkOptions ); const eventEmitter = new CliCommandEventEmitter(); let stdout = ''; diff --git a/src/storage/paths.ts b/src/storage/paths.ts index 8ffbd0b868..25e9809bf4 100644 --- a/src/storage/paths.ts +++ b/src/storage/paths.ts @@ -70,11 +70,10 @@ export function getBundledNodeBinaryPath(): string { const nodeBinaryName = process.platform === 'win32' ? 'node.exe' : 'node'; if ( process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' ) { - // In development, use system Node - const { execSync } = require( 'child_process' ); - const command = process.platform === 'win32' ? 'where node' : 'which node'; - // `where` on Windows can return multiple paths, take the first one - return execSync( command, { encoding: 'utf-8' } ).trim().split( /\r?\n/ )[ 0 ]; + // In development/test, use the current Node process executable. + // This is more reliable than trying to find 'node' in PATH, especially + // when tests mock process.platform to a different OS. + return process.execPath; } return path.join( getResourcesPath(), 'bin', nodeBinaryName ); From 216e89061884e9c8757dd86c42bdae8acdc9973a Mon Sep 17 00:00:00 2001 From: bcotrim Date: Wed, 24 Dec 2025 11:14:32 +0000 Subject: [PATCH 09/16] attempt to fix windows hide --- cli/lib/pm2-manager.ts | 3 +-- cli/patches/ps-man+1.1.8.patch | 22 ++++++++++++++++++++++ src/modules/cli/lib/execute-command.ts | 3 +-- 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 cli/patches/ps-man+1.1.8.patch diff --git a/cli/lib/pm2-manager.ts b/cli/lib/pm2-manager.ts index a34e586979..7d80f3d317 100644 --- a/cli/lib/pm2-manager.ts +++ b/cli/lib/pm2-manager.ts @@ -159,13 +159,12 @@ export async function startProcess( env: Record< string, string > = {} ): Promise< ProcessDescription > { return new Promise( ( resolve, reject ) => { - const processConfig: StartOptions & { windowsHide?: boolean } = { + const processConfig: StartOptions = { name: processName, interpreter: process.execPath, script: scriptPath, exec_mode: 'fork', autorestart: false, - windowsHide: true, // Merge process.env with custom env to ensure child processes inherit // necessary environment variables (PATH, HOME, E2E vars, etc.) env: { ...process.env, ...env } as Record< string, string >, diff --git a/cli/patches/ps-man+1.1.8.patch b/cli/patches/ps-man+1.1.8.patch new file mode 100644 index 0000000000..d9a4335080 --- /dev/null +++ b/cli/patches/ps-man+1.1.8.patch @@ -0,0 +1,22 @@ +diff --git a/node_modules/ps-man/lib/index.js b/node_modules/ps-man/lib/index.js +index 1234567..abcdefg 100644 +--- a/node_modules/ps-man/lib/index.js ++++ b/node_modules/ps-man/lib/index.js +@@ -42,7 +42,7 @@ var listProcesses = function(options, done) { + options = options || {}; + } + +- var ps = spawn(operations.ps.command, operations.ps.arguments); ++ var ps = spawn(operations.ps.command, operations.ps.arguments, { windowsHide: true }); + var processList = ''; + var processErr = ''; + var options = { +@@ -137,7 +137,7 @@ var killProcesses = function(options, done) { + }); + } + +- var kill = spawn(operations.kill.command, killArguments); ++ var kill = spawn(operations.kill.command, killArguments, { windowsHide: true }); + var processErr = ''; + + kill.on('error', done); diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 3a0ac77042..2317ca4ad3 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -55,11 +55,10 @@ export function executeCliCommand( stdio = [ 'ignore', 'ignore', 'ignore', 'ipc' ]; } - const forkOptions: ForkOptions & { windowsHide?: boolean } = { + const forkOptions: ForkOptions = { stdio, execPath: getBundledNodeBinaryPath(), env: process.env, - windowsHide: true, }; const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], forkOptions ); const eventEmitter = new CliCommandEventEmitter(); From 6fd7336911a8b3316f5c8b41e454394d2328413a Mon Sep 17 00:00:00 2001 From: bcotrim Date: Wed, 24 Dec 2025 11:55:45 +0000 Subject: [PATCH 10/16] trigger tests From 17e2eadfad13b37a48a404b80f8f6a3412cedc85 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 2 Jan 2026 09:20:49 +0100 Subject: [PATCH 11/16] Refine download-node-binary script - ESM instead of CommonJS - Use npm modules instead of shell commands to extract archives - Linux isn't supported - fetch API instead of https module --- forge.config.ts | 2 +- scripts/download-node-binary.js | 189 ------------------------------- scripts/download-node-binary.mjs | 166 +++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 190 deletions(-) delete mode 100644 scripts/download-node-binary.js create mode 100644 scripts/download-node-binary.mjs diff --git a/forge.config.ts b/forge.config.ts index 9770831291..451941a87e 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -129,7 +129,7 @@ const config: ForgeConfig = { await execAsync( 'npm run cli:build' ); console.log( `Downloading Node.js binary for ${ platform }-${ arch }...` ); - await execAsync( `node ./scripts/download-node-binary.js ${ platform } ${ arch }` ); + await execAsync( `node ./scripts/download-node-binary.mjs ${ platform } ${ arch }` ); }, }, }; diff --git a/scripts/download-node-binary.js b/scripts/download-node-binary.js deleted file mode 100644 index 74571081c2..0000000000 --- a/scripts/download-node-binary.js +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/env node -/** - * Download Node.js binary for bundling with Studio - * Usage: node scripts/download-node-binary.js - * Example: node scripts/download-node-binary.js darwin arm64 - */ - -const https = require( 'https' ); -const fs = require( 'fs' ); -const path = require( 'path' ); -const { execSync } = require( 'child_process' ); -const os = require( 'os' ); - -const LTS_FALLBACK = 'v22.12.0'; - -function getNodeVersion() { - const nvmrcPath = path.join( __dirname, '..', '.nvmrc' ); - if ( fs.existsSync( nvmrcPath ) ) { - const version = fs.readFileSync( nvmrcPath, 'utf-8' ).trim(); - // Ensure version starts with 'v' - return version.startsWith( 'v' ) ? version : `v${ version }`; - } - console.log( `.nvmrc not found, using fallback version ${ LTS_FALLBACK }` ); - return LTS_FALLBACK; -} - -const NODE_VERSION = getNodeVersion(); - -const platform = process.argv[ 2 ] || process.platform; -const arch = process.argv[ 3 ] || process.arch; - -// Map platform names to Node.js download naming -const platformMap = { - darwin: 'darwin', - win32: 'win', - linux: 'linux', -}; - -// Map architecture names -const archMap = { - arm64: 'arm64', - x64: 'x64', -}; - -const nodePlatform = platformMap[ platform ]; -const nodeArch = archMap[ arch ]; - -if ( ! nodePlatform ) { - console.error( `Unsupported platform: ${ platform }` ); - process.exit( 1 ); -} - -if ( ! nodeArch ) { - console.error( `Unsupported architecture: ${ arch }` ); - process.exit( 1 ); -} - -const binDir = path.join( __dirname, '..', 'bin' ); -const tmpDir = os.tmpdir(); - -// Ensure bin directory exists -if ( ! fs.existsSync( binDir ) ) { - fs.mkdirSync( binDir, { recursive: true } ); -} - -const isWindows = nodePlatform === 'win'; -const ext = isWindows ? 'zip' : 'tar.gz'; -const filename = `node-${ NODE_VERSION }-${ nodePlatform }-${ nodeArch }.${ ext }`; -const url = `https://nodejs.org/dist/${ NODE_VERSION }/${ filename }`; -const downloadPath = path.join( tmpDir, filename ); - -function download( downloadUrl, dest ) { - return new Promise( ( resolve, reject ) => { - console.log( `Downloading Node.js ${ NODE_VERSION } for ${ nodePlatform }-${ nodeArch }...` ); - console.log( `URL: ${ downloadUrl }` ); - - const file = fs.createWriteStream( dest ); - - https - .get( downloadUrl, ( response ) => { - // Handle redirects - if ( response.statusCode === 302 || response.statusCode === 301 ) { - file.close(); - fs.unlinkSync( dest ); - return download( response.headers.location, dest ).then( resolve ).catch( reject ); - } - - if ( response.statusCode !== 200 ) { - reject( new Error( `Failed to download: HTTP ${ response.statusCode }` ) ); - return; - } - - const totalSize = parseInt( response.headers[ 'content-length' ], 10 ); - let downloadedSize = 0; - - response.on( 'data', ( chunk ) => { - downloadedSize += chunk.length; - const percent = ( ( downloadedSize / totalSize ) * 100 ).toFixed( 1 ); - process.stdout.write( `\rDownloading... ${ percent }%` ); - } ); - - response.pipe( file ); - - file.on( 'finish', () => { - file.close(); - console.log( '\nDownload complete.' ); - resolve(); - } ); - } ) - .on( 'error', ( err ) => { - fs.unlink( dest, () => {} ); - reject( err ); - } ); - } ); -} - -function extractTarGz( archivePath, destDir, binaryName ) { - console.log( 'Extracting node binary...' ); - - const extractDir = path.join( tmpDir, `node-${ NODE_VERSION }-${ nodePlatform }-${ nodeArch }` ); - - // Use tar command (available on macOS and Linux) - execSync( `tar -xzf "${ archivePath }" -C "${ tmpDir }"` ); - - const sourcePath = path.join( extractDir, 'bin', 'node' ); - const destPath = path.join( destDir, binaryName ); - - fs.copyFileSync( sourcePath, destPath ); - fs.chmodSync( destPath, 0o755 ); - - // Cleanup - fs.unlinkSync( archivePath ); - fs.rmSync( extractDir, { recursive: true } ); -} - -function extractZip( archivePath, destDir, binaryName ) { - console.log( 'Extracting node.exe...' ); - - const extractDir = path.join( tmpDir, `node-${ NODE_VERSION }-${ nodePlatform }-${ nodeArch }` ); - - // Use PowerShell on Windows, unzip on others - if ( process.platform === 'win32' ) { - execSync( - `powershell -Command "Expand-Archive -Path '${ archivePath }' -DestinationPath '${ tmpDir }' -Force"` - ); - } else { - execSync( `unzip -q "${ archivePath }" -d "${ tmpDir }"` ); - } - - const sourcePath = path.join( extractDir, 'node.exe' ); - const destPath = path.join( destDir, binaryName ); - - fs.copyFileSync( sourcePath, destPath ); - - // Cleanup - fs.unlinkSync( archivePath ); - fs.rmSync( extractDir, { recursive: true } ); -} - -async function main() { - try { - await download( url, downloadPath ); - - const binaryName = isWindows ? 'node.exe' : 'node'; - - if ( isWindows ) { - extractZip( downloadPath, binDir, binaryName ); - } else { - extractTarGz( downloadPath, binDir, binaryName ); - } - - console.log( `\nNode.js binary installed to ${ binDir }` ); - - // List the bin directory contents - const files = fs.readdirSync( binDir ); - console.log( '\nBin directory contents:' ); - for ( const file of files ) { - const filePath = path.join( binDir, file ); - const stats = fs.statSync( filePath ); - const size = ( stats.size / 1024 / 1024 ).toFixed( 2 ); - console.log( ` ${ file } (${ size } MB)` ); - } - } catch ( error ) { - console.error( 'Error:', error.message ); - process.exit( 1 ); - } -} - -main(); diff --git a/scripts/download-node-binary.mjs b/scripts/download-node-binary.mjs new file mode 100644 index 0000000000..5f1b40a21d --- /dev/null +++ b/scripts/download-node-binary.mjs @@ -0,0 +1,166 @@ +#!/usr/bin/env node +/** + * Download Node.js binary for bundling with Studio + * Usage: node scripts/download-node-binary.js + * Example: node scripts/download-node-binary.js darwin arm64 + */ + +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { extract } from 'tar'; +import unzipper from 'unzipper'; + +const LTS_FALLBACK = 'v22.12.0'; + +function getNodeVersion() { + const nvmrcPath = path.join( import.meta.dirname, '..', '.nvmrc' ); + if ( fs.existsSync( nvmrcPath ) ) { + const version = fs.readFileSync( nvmrcPath, 'utf-8' ).trim(); + return version.startsWith( 'v' ) ? version : `v${ version }`; + } + console.log( `.nvmrc not found, using fallback version ${ LTS_FALLBACK }` ); + return LTS_FALLBACK; +} + +const NODE_VERSION = getNodeVersion(); + +const platform = process.argv[ 2 ] || process.platform; +const arch = process.argv[ 3 ] || process.arch; + +// Map platform names to nodejs.org download naming +const platformMap = { + darwin: 'darwin', + win32: 'win', +}; + +// Map architecture names to nodejs.org download naming +const archMap = { + arm64: 'arm64', + x64: 'x64', +}; + +const nodePlatform = platformMap[ platform ]; +const nodeArch = archMap[ arch ]; + +if ( ! nodePlatform ) { + console.error( `Unsupported platform: ${ platform }` ); + process.exit( 1 ); +} + +if ( ! nodeArch ) { + console.error( `Unsupported architecture: ${ arch }` ); + process.exit( 1 ); +} + +const binDir = path.join( import.meta.dirname, '..', 'bin' ); +const tmpDir = os.tmpdir(); + +if ( ! fs.existsSync( binDir ) ) { + fs.mkdirSync( binDir, { recursive: true } ); +} + +const isWindows = nodePlatform === 'win'; +// nodejs.org provides different archive formats depending on the target platform +const ext = isWindows ? 'zip' : 'tar.gz'; +const filename = `node-${ NODE_VERSION }-${ nodePlatform }-${ nodeArch }.${ ext }`; +const url = `https://nodejs.org/dist/${ NODE_VERSION }/${ filename }`; +const downloadPath = path.join( tmpDir, filename ); + +async function download( downloadUrl, dest ) { + console.log( `Downloading Node.js ${ NODE_VERSION } for ${ nodePlatform }-${ nodeArch }...` ); + + const response = await fetch( downloadUrl ); + + if ( ! response.ok ) { + throw new Error( `Failed to download: HTTP ${ response.status }` ); + } + + const file = fs.createWriteStream( dest ); + const reader = response.body.getReader(); + + try { + while ( true ) { + const { done, value } = await reader.read(); + if ( done ) { + break; + } + file.write( value ); + } + } finally { + reader.releaseLock(); + } + + await new Promise( ( resolve, reject ) => { + file.on( 'finish', resolve ); + file.on( 'error', reject ); + file.end(); + } ); + + console.log( 'Download complete.' ); +} + +async function extractTarGz( archivePath, destDir, binaryName ) { + console.log( 'Extracting node binary...' ); + + const extractDir = path.join( tmpDir, `node-${ NODE_VERSION }-${ nodePlatform }-${ nodeArch }` ); + + await extract( { + file: archivePath, + cwd: tmpDir, + } ).then( () => { + // Do nothing. We just need the `.then` chaining to be able to await the promise + } ); + + const sourcePath = path.join( extractDir, 'bin', 'node' ); + const destPath = path.join( destDir, binaryName ); + + fs.copyFileSync( sourcePath, destPath ); + fs.chmodSync( destPath, 0o755 ); + fs.rmSync( extractDir, { recursive: true } ); +} + +async function extractZip( archivePath, destDir, binaryName ) { + console.log( 'Extracting node.exe...' ); + + const extractDir = path.join( tmpDir, `node-${ NODE_VERSION }-${ nodePlatform }-${ nodeArch }` ); + + await fs + .createReadStream( archivePath ) + .pipe( unzipper.Extract( { path: tmpDir } ) ) + .promise(); + + const sourcePath = path.join( extractDir, 'node.exe' ); + const destPath = path.join( destDir, binaryName ); + + fs.copyFileSync( sourcePath, destPath ); + fs.rmSync( extractDir, { recursive: true } ); +} + +try { + await download( url, downloadPath ); + + const binaryName = isWindows ? 'node.exe' : 'node'; + + if ( isWindows ) { + await extractZip( downloadPath, binDir, binaryName ); + } else { + await extractTarGz( downloadPath, binDir, binaryName ); + } + + fs.unlinkSync( downloadPath ); + + console.log( `\nNode.js binary installed to ${ binDir }` ); + + const files = fs.readdirSync( binDir ); + console.log( '\nBin directory contents:' ); + for ( const file of files ) { + const filePath = path.join( binDir, file ); + const stats = fs.statSync( filePath ); + const size = ( stats.size / 1024 / 1024 ).toFixed( 2 ); + console.log( ` ${ file } (${ size } MB)` ); + } +} catch ( error ) { + console.error( 'Error:', error.message ); + process.exit( 1 ); +} From 4e77d331171a4bc572c9ed9a395959d6d0a64bd8 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 2 Jan 2026 09:37:03 +0100 Subject: [PATCH 12/16] Use bundled node binary in ./bin scripts --- bin/studio-cli.bat | 15 +++++++-------- bin/studio-cli.sh | 13 +++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bin/studio-cli.bat b/bin/studio-cli.bat index f06233969e..f2b92c9ece 100644 --- a/bin/studio-cli.bat +++ b/bin/studio-cli.bat @@ -8,20 +8,19 @@ set ORIGINAL_CP=%ORIGINAL_CP: =% rem Set code page to UTF-8 chcp 65001 >nul -set ELECTRON_EXECUTABLE=%~dp0..\..\Studio.exe +set BUNDLED_NODE_EXECUTABLE=%~dp0node.exe set CLI_SCRIPT=%~dp0..\cli\main.js rem Prevent node from printing warnings about NODE_OPTIONS being ignored set NODE_OPTIONS= -if exist "%ELECTRON_EXECUTABLE%" ( - set ELECTRON_RUN_AS_NODE=1 - call "%ELECTRON_EXECUTABLE%" "%CLI_SCRIPT%" %* +if exist "%BUNDLED_NODE_EXECUTABLE%" ( + call "%BUNDLED_NODE_EXECUTABLE%" "%CLI_SCRIPT%" %* ) else ( - if not exist "%CLI_SCRIPT%" ( - set CLI_SCRIPT=%~dp0..\dist\cli\main.js - ) - call node "%CLI_SCRIPT%" %* + if not exist "%CLI_SCRIPT%" ( + set CLI_SCRIPT=%~dp0..\dist\cli\main.js + ) + call node "%CLI_SCRIPT%" %* ) rem Restore original code page diff --git a/bin/studio-cli.sh b/bin/studio-cli.sh index 46c95749d3..3759575d05 100755 --- a/bin/studio-cli.sh +++ b/bin/studio-cli.sh @@ -1,19 +1,20 @@ #!/bin/sh -# The default assumption is that this script lives in `/Applications/Studio.app/Contents/Resources/bin/studio-cli.sh` -CONTENTS_DIR=$(dirname "$(dirname "$(dirname "$(realpath "$0")")")") -ELECTRON_EXECUTABLE="$CONTENTS_DIR/MacOS/Studio" +# The assumption is that this script lives in `/Applications/Studio.app/Contents/Resources/bin/studio-cli.sh` +BIN_DIR=$(dirname "$(realpath "$0")") +BUNDLED_NODE_EXECUTABLE="$BIN_DIR/node" +CONTENTS_DIR=$(dirname "$(dirname "$BIN_DIR")") CLI_SCRIPT="$CONTENTS_DIR/Resources/cli/main.js" -if [ -x "$ELECTRON_EXECUTABLE" ]; then +if [ -x "$BUNDLED_NODE_EXECUTABLE" ]; then # Prevent node from printing warnings about NODE_OPTIONS being ignored unset NODE_OPTIONS - ELECTRON_RUN_AS_NODE=1 exec "$ELECTRON_EXECUTABLE" "$CLI_SCRIPT" "$@" + exec "$BUNDLED_NODE_EXECUTABLE" "$CLI_SCRIPT" "$@" else # If the default script path is not found, assume that this script lives in the development directory # and look for the CLI JS bundle in the `./dist` directory if ! [ -f "$CLI_SCRIPT" ]; then - SCRIPT_DIR=$(dirname $(dirname "$(realpath "$0")")) + SCRIPT_DIR=$(dirname "$(dirname "$(realpath "$0")")") CLI_SCRIPT="$SCRIPT_DIR/dist/cli/main.js" fi From 0280050513aa4f64076e5c5dfd88a8a6347306f9 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 2 Jan 2026 09:40:15 +0100 Subject: [PATCH 13/16] Flip the logic for distinguishing node environments --- src/storage/paths.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/storage/paths.ts b/src/storage/paths.ts index 25e9809bf4..285da7a5eb 100644 --- a/src/storage/paths.ts +++ b/src/storage/paths.ts @@ -67,16 +67,15 @@ export function getCliPath(): string { } export function getBundledNodeBinaryPath(): string { - const nodeBinaryName = process.platform === 'win32' ? 'node.exe' : 'node'; - - if ( process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' ) { - // In development/test, use the current Node process executable. - // This is more reliable than trying to find 'node' in PATH, especially - // when tests mock process.platform to a different OS. - return process.execPath; + if ( process.env.NODE_ENV === 'production' ) { + const nodeBinaryName = process.platform === 'win32' ? 'node.exe' : 'node'; + return path.join( getResourcesPath(), 'bin', nodeBinaryName ); } - return path.join( getResourcesPath(), 'bin', nodeBinaryName ); + // In development/test, use the current Node process executable. + // This is more reliable than trying to find 'node' in PATH, especially + // when tests mock process.platform to a different OS. + return process.execPath; } function getAppDataPath(): string { From e284836e67c135c697a4b6a7ec71256b79d7eb98 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 2 Jan 2026 09:41:48 +0100 Subject: [PATCH 14/16] process.env is already the default --- src/modules/cli/lib/execute-command.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/modules/cli/lib/execute-command.ts b/src/modules/cli/lib/execute-command.ts index 2317ca4ad3..9097094081 100644 --- a/src/modules/cli/lib/execute-command.ts +++ b/src/modules/cli/lib/execute-command.ts @@ -1,4 +1,4 @@ -import { fork, ChildProcess, StdioOptions, ForkOptions } from 'node:child_process'; +import { fork, ChildProcess, StdioOptions } from 'node:child_process'; import EventEmitter from 'node:events'; import * as Sentry from '@sentry/electron/main'; import { getBundledNodeBinaryPath, getCliPath } from 'src/storage/paths'; @@ -55,12 +55,10 @@ export function executeCliCommand( stdio = [ 'ignore', 'ignore', 'ignore', 'ipc' ]; } - const forkOptions: ForkOptions = { + const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], { stdio, execPath: getBundledNodeBinaryPath(), - env: process.env, - }; - const child = fork( cliPath, [ ...args, '--avoid-telemetry' ], forkOptions ); + } ); const eventEmitter = new CliCommandEventEmitter(); let stdout = ''; From f0b987307df67ddd586aa38bce81018c27dce7b6 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 2 Jan 2026 09:55:54 +0100 Subject: [PATCH 15/16] Use system-level node binary in development --- src/storage/paths.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/storage/paths.ts b/src/storage/paths.ts index 285da7a5eb..14b94a8d0c 100644 --- a/src/storage/paths.ts +++ b/src/storage/paths.ts @@ -67,15 +67,15 @@ export function getCliPath(): string { } export function getBundledNodeBinaryPath(): string { + const nodeBinaryName = process.platform === 'win32' ? 'node.exe' : 'node'; + if ( process.env.NODE_ENV === 'production' ) { - const nodeBinaryName = process.platform === 'win32' ? 'node.exe' : 'node'; return path.join( getResourcesPath(), 'bin', nodeBinaryName ); } - // In development/test, use the current Node process executable. - // This is more reliable than trying to find 'node' in PATH, especially - // when tests mock process.platform to a different OS. - return process.execPath; + // In development and test environments, use the system-level node binary. The bundled node binary + // most likely isn't available, and the Electron binary is noticeably slower. + return nodeBinaryName; } function getAppDataPath(): string { From 16814d1eb206b7728993697722fda99113202de3 Mon Sep 17 00:00:00 2001 From: Fredrik Rombach Ekelund Date: Fri, 2 Jan 2026 11:09:15 +0100 Subject: [PATCH 16/16] Use Electron node binary in unit tests --- src/storage/paths.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/storage/paths.ts b/src/storage/paths.ts index 14b94a8d0c..b68a9d5cfa 100644 --- a/src/storage/paths.ts +++ b/src/storage/paths.ts @@ -73,8 +73,14 @@ export function getBundledNodeBinaryPath(): string { return path.join( getResourcesPath(), 'bin', nodeBinaryName ); } - // In development and test environments, use the system-level node binary. The bundled node binary - // most likely isn't available, and the Electron binary is noticeably slower. + // In test environment, use the Electron node binary. The system-level node binary is not reliable + // in this context. + if ( process.env.NODE_ENV === 'test' ) { + return process.execPath; + } + + // In development, use the system-level node binary. The bundled node binary most likely isn't + // available, and the Electron binary is noticeably slower. return nodeBinaryName; }