diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index 15f0da1eca..72d90ce14d 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -66,8 +66,7 @@ import { getLogsFilePath, writeLogToFile, type LogLevel } from 'src/logging'; import { getMainWindow } from 'src/main-window'; import { popupMenu, setupMenu } from 'src/menu'; import { editSiteViaCli, EditSiteOptions } from 'src/modules/cli/lib/cli-site-editor'; -import { isStudioCliInstalled } from 'src/modules/cli/lib/ipc-handlers'; -import { STABLE_BIN_DIR_PATH } from 'src/modules/cli/lib/windows-installation-manager'; +import { WindowsCliInstallationManager } from 'src/modules/cli/lib/windows-installation-manager'; import { shouldExcludeFromSync, shouldLimitDepth } from 'src/modules/sync/lib/tree-utils'; import { supportedEditorConfig, SupportedEditor } from 'src/modules/user-settings/lib/editor'; import { getUserTerminal } from 'src/modules/user-settings/lib/ipc-handlers'; @@ -982,17 +981,24 @@ export async function openTerminalAtPath( _event: IpcMainInvokeEvent, targetPath // Ensure the Studio CLI bin directory is in the PATH for the spawned terminal. // Child processes inherit the environment from the Electron process, which may have // been started before the CLI was installed or PATH was updated in the registry. - const isCliInstalled = await isStudioCliInstalled(); let env: NodeJS.ProcessEnv | undefined; - if ( isCliInstalled ) { - const currentPath = process.env.PATH || ''; - const pathEntries = currentPath.split( ';' ).map( ( p ) => p.toLowerCase() ); - if ( ! pathEntries.includes( STABLE_BIN_DIR_PATH.toLowerCase() ) ) { - env = { ...process.env }; - delete env.PATH; - delete env.Path; - env.PATH = `${ STABLE_BIN_DIR_PATH };${ currentPath }`; + try { + const installationManager = new WindowsCliInstallationManager(); + const isCliInstalled = await installationManager.isCliInstalled(); + + if ( isCliInstalled ) { + const currentPath = process.env.PATH || ''; + const pathEntries = currentPath.split( ';' ).map( ( p ) => p.toLowerCase() ); + const STABLE_BIN_DIR_PATH = installationManager.getStableBinDirPath(); + if ( ! pathEntries.includes( STABLE_BIN_DIR_PATH.toLowerCase() ) ) { + env = { ...process.env }; + delete env.PATH; + delete env.Path; + env.PATH = `${ STABLE_BIN_DIR_PATH };${ currentPath }`; + } } + } catch { + // Handle error gracefully } return promiseExec( `start "Command Prompt" ${ defaultShell }`, { diff --git a/apps/studio/src/modules/cli/lib/windows-installation-manager.ts b/apps/studio/src/modules/cli/lib/windows-installation-manager.ts index 919b837aa2..591c0e68cf 100644 --- a/apps/studio/src/modules/cli/lib/windows-installation-manager.ts +++ b/apps/studio/src/modules/cli/lib/windows-installation-manager.ts @@ -8,10 +8,7 @@ import Registry from 'winreg'; // don't update winreg to 1.2.5 - https://github. import { getMainWindow } from 'src/main-window'; import { StudioCliInstallationManager } from 'src/modules/cli/lib/ipc-handlers'; -// `STABLE_BIN_DIR_PATH` resolves to C:\Users\\AppData\Local\studio\bin -export const STABLE_BIN_DIR_PATH = path.resolve( path.dirname( app.getPath( 'exe' ) ), '../bin' ); -const PATH_KEY = 'Path'; - +const REGISTRY_PATH_KEY = 'Path'; const currentUserRegistry = new Registry( { hive: Registry.HKCU, key: '\\Environment', @@ -24,13 +21,22 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag } } + getStableBinDirPath() { + if ( ! process.env.LOCALAPPDATA ) { + throw new Error( 'LOCALAPPDATA environment variable is not set' ); + } + + // Resolves to C:\Users\\AppData\Local\studio\bin + return path.join( process.env.LOCALAPPDATA, 'studio', 'bin' ); + } + /** * Check if the stable bin directory has been created and if it's contained in the registry PATH. */ async isCliInstalled(): Promise< boolean > { try { const isStudioCliDirInPath = await this.isStudioCliDirInPath(); - return isStudioCliDirInPath && existsSync( STABLE_BIN_DIR_PATH ); + return isStudioCliDirInPath && existsSync( this.getStableBinDirPath() ); } catch ( error ) { console.error( 'Failed to check installation status of CLI', error ); return false; @@ -105,7 +111,7 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag private getPathFromRegistry(): Promise< string > { return new Promise( ( resolve, reject ) => { - currentUserRegistry.get( PATH_KEY, ( error, item ) => { + currentUserRegistry.get( REGISTRY_PATH_KEY, ( error, item ) => { if ( error ) { return reject( error ); } @@ -117,18 +123,23 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag private setPathInRegistry( updatedPath: string ): Promise< void > { return new Promise( ( resolve, reject ) => { - currentUserRegistry.set( PATH_KEY, Registry.REG_EXPAND_SZ, updatedPath, ( error ) => { - if ( error ) { - return reject( error ); + currentUserRegistry.set( + REGISTRY_PATH_KEY, + Registry.REG_EXPAND_SZ, + updatedPath, + ( error ) => { + if ( error ) { + return reject( error ); + } + + resolve(); } - - resolve(); - } ); + ); } ); } private async isStudioCliDirInPath(): Promise< boolean > { - let studioCliDir = STABLE_BIN_DIR_PATH; + let studioCliDir = this.getStableBinDirPath(); // Return true if we are running the development version of the app and the production CLI is installed if ( process.env.NODE_ENV !== 'production' && process.env.LOCALAPPDATA ) { @@ -153,13 +164,13 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag .split( ';' ) .map( ( p ) => p.trim() ) .filter( Boolean ) - .concat( STABLE_BIN_DIR_PATH ) + .concat( this.getStableBinDirPath() ) .join( ';' ); await this.setPathInRegistry( updatedPath ); } catch ( error ) { - Sentry.captureException( error ); console.error( 'Failed to install CLI path', error ); + Sentry.captureException( error ); } } @@ -172,20 +183,18 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag */ private async installProxyBatFile(): Promise< void > { try { - await mkdir( STABLE_BIN_DIR_PATH, { recursive: true } ); + await mkdir( this.getStableBinDirPath(), { recursive: true } ); const versionedCliPath = path.join( path.dirname( app.getPath( 'exe' ) ), 'resources/bin/studio-cli.bat' ); - const relativeVersionedCliPath = path.relative( STABLE_BIN_DIR_PATH, versionedCliPath ); + const content = `@echo off\n"${ versionedCliPath }" %*`; - const content = `@echo off\n"%~dp0\\${ relativeVersionedCliPath }" %*`; - - await writeFile( path.join( STABLE_BIN_DIR_PATH, 'studio.bat' ), content ); + await writeFile( path.join( this.getStableBinDirPath(), 'studio.bat' ), content ); } catch ( error ) { + console.error( 'Failed to install CLI proxy .bat file', error ); Sentry.captureException( error ); - console.error( 'Failed to install CLI: Proxy Bat file', error ); } } @@ -195,15 +204,23 @@ export class WindowsCliInstallationManager implements StudioCliInstallationManag } private async uninstallCli(): Promise< void > { - const currentPath = await this.getPathFromRegistry(); - const newPath = currentPath - .split( ';' ) - .filter( ( item ) => item.trim().toLowerCase() !== STABLE_BIN_DIR_PATH.toLowerCase() ) - .join( ';' ); + try { + const currentPath = await this.getPathFromRegistry(); + const newPath = currentPath + .split( ';' ) + .filter( + ( item ) => item.trim().toLowerCase() !== this.getStableBinDirPath().toLowerCase() + ) + .join( ';' ); + + await this.setPathInRegistry( newPath ); - await this.setPathInRegistry( newPath ); - if ( process.env.NODE_ENV === 'production' ) { - await rm( STABLE_BIN_DIR_PATH, { recursive: true, force: true } ); + if ( process.env.NODE_ENV === 'production' ) { + await rm( this.getStableBinDirPath(), { recursive: true, force: true } ); + } + } catch ( error ) { + console.error( 'Failed to uninstall CLI proxy .bat file', error ); + Sentry.captureException( error ); } } }