2025-07-30 21:26:31 +00:00
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
2025-08-25 22:11:27 +02:00
import * as child_process from 'node:child_process' ;
import * as process from 'node:process' ;
import * as path from 'node:path' ;
import * as fs from 'node:fs' ;
2025-09-18 15:23:24 -04:00
import { IDE_DEFINITIONS , type IdeInfo } from './detect-ide.js' ;
2025-08-14 14:57:36 +00:00
import { GEMINI_CLI_COMPANION_EXTENSION_NAME } from './constants.js' ;
2026-01-06 20:09:39 -08:00
import { homedir } from '../utils/paths.js' ;
2025-07-30 21:26:31 +00:00
export interface IdeInstaller {
install ( ) : Promise < InstallResult > ;
}
export interface InstallResult {
success : boolean ;
message : string ;
}
2025-11-18 12:01:16 -05:00
async function findCommand (
command : string ,
2025-08-25 17:10:36 -04:00
platform : NodeJS.Platform = process . platform ,
) : Promise < string | null > {
2025-07-30 21:26:31 +00:00
// 1. Check PATH first.
try {
2025-08-25 17:10:36 -04:00
if ( platform === 'win32' ) {
2025-08-19 13:25:11 -07:00
const result = child_process
2025-11-18 12:01:16 -05:00
. execSync ( ` where.exe ${ command } ` )
2025-08-19 13:25:11 -07:00
. toString ( )
. trim ( ) ;
// `where.exe` can return multiple paths. Return the first one.
const firstPath = result . split ( /\r?\n/ ) [ 0 ] ;
if ( firstPath ) {
return firstPath ;
}
} else {
2025-11-18 12:01:16 -05:00
child_process . execSync ( ` command -v ${ command } ` , {
2025-08-19 13:25:11 -07:00
stdio : 'ignore' ,
} ) ;
2025-11-18 12:01:16 -05:00
return command ;
2025-08-19 13:25:11 -07:00
}
2025-07-30 21:26:31 +00:00
} catch {
// Not in PATH, continue to check common locations.
}
// 2. Check common installation locations.
const locations : string [ ] = [ ] ;
2026-01-06 20:09:39 -08:00
const homeDir = homedir ( ) ;
2025-07-30 21:26:31 +00:00
2026-01-28 16:08:03 +01:00
interface AppConfigEntry {
mac ? : { appName : string ; supportDirName : string } ;
win ? : { appName : string ; appBinary : string } ;
linux ? : { appBinary : string } ;
}
interface AppConfigs {
code : AppConfigEntry ;
positron : AppConfigEntry ;
}
const appConfigs : AppConfigs = {
code : {
mac : { appName : 'Visual Studio Code' , supportDirName : 'Code' } ,
win : { appName : 'Microsoft VS Code' , appBinary : 'code.cmd' } ,
linux : { appBinary : 'code' } ,
} ,
positron : {
mac : { appName : 'Positron' , supportDirName : 'Positron' } ,
win : { appName : 'Positron' , appBinary : 'positron.cmd' } ,
linux : { appBinary : 'positron' } ,
} ,
} ;
type AppName = keyof typeof appConfigs ;
let appname : AppName | undefined ;
2025-11-18 12:01:16 -05:00
if ( command === 'code' || command === 'code.cmd' ) {
2026-01-28 16:08:03 +01:00
appname = 'code' ;
} else if ( command === 'positron' || command === 'positron.cmd' ) {
appname = 'positron' ;
}
if ( appname ) {
2025-11-18 12:01:16 -05:00
if ( platform === 'darwin' ) {
// macOS
2026-01-28 16:08:03 +01:00
const macConfig = appConfigs [ appname ] . mac ;
if ( macConfig ) {
locations . push (
` /Applications/ ${ macConfig . appName } .app/Contents/Resources/app/bin/ ${ appname } ` ,
path . join (
homeDir ,
` Library/Application Support/ ${ macConfig . supportDirName } /bin/ ${ appname } ` ,
) ,
) ;
}
2025-11-18 12:01:16 -05:00
} else if ( platform === 'linux' ) {
// Linux
2026-01-28 16:08:03 +01:00
const linuxConfig = appConfigs [ appname ] ? . linux ;
if ( linuxConfig ) {
locations . push (
` /usr/share/ ${ linuxConfig . appBinary } /bin/ ${ linuxConfig . appBinary } ` ,
` /snap/bin/ ${ linuxConfig . appBinary } ` ,
path . join (
homeDir ,
` .local/share/ ${ linuxConfig . appBinary } /bin/ ${ linuxConfig . appBinary } ` ,
) ,
) ;
}
2025-11-18 12:01:16 -05:00
} else if ( platform === 'win32' ) {
// Windows
2026-01-28 16:08:03 +01:00
const winConfig = appConfigs [ appname ] . win ;
if ( winConfig ) {
const winAppName = winConfig . appName ;
locations . push (
path . join (
process . env [ 'ProgramFiles' ] || 'C:\\Program Files' ,
winAppName ,
'bin' ,
winConfig . appBinary ,
) ,
path . join (
homeDir ,
'AppData' ,
'Local' ,
'Programs' ,
winAppName ,
'bin' ,
winConfig . appBinary ,
) ,
) ;
}
2025-11-18 12:01:16 -05:00
}
2025-07-30 21:26:31 +00:00
}
for ( const location of locations ) {
if ( fs . existsSync ( location ) ) {
return location ;
}
}
return null ;
}
class VsCodeInstaller implements IdeInstaller {
private vsCodeCommand : Promise < string | null > ;
2025-08-25 17:10:36 -04:00
constructor (
2025-09-18 15:23:24 -04:00
readonly ideInfo : IdeInfo ,
2025-08-25 17:10:36 -04:00
readonly platform = process . platform ,
) {
2025-11-18 12:01:16 -05:00
const command = platform === 'win32' ? 'code.cmd' : 'code' ;
this . vsCodeCommand = findCommand ( command , platform ) ;
2025-07-30 21:26:31 +00:00
}
async install ( ) : Promise < InstallResult > {
const commandPath = await this . vsCodeCommand ;
if ( ! commandPath ) {
return {
success : false ,
2025-08-25 17:10:36 -04:00
message : ` ${ this . ideInfo . displayName } CLI not found. Please ensure 'code' is in your system's PATH. For help, see https://code.visualstudio.com/docs/configure/command-line#_code-is-not-recognized-as-an-internal-or-external-command. You can also install the ' ${ GEMINI_CLI_COMPANION_EXTENSION_NAME } ' extension manually from the VS Code marketplace. ` ,
2025-07-30 21:26:31 +00:00
} ;
}
try {
2025-09-16 12:03:17 -07:00
const result = child_process . spawnSync (
commandPath ,
[
'--install-extension' ,
'google.gemini-cli-vscode-ide-companion' ,
'--force' ,
] ,
2025-10-08 14:21:23 -07:00
{ stdio : 'pipe' , shell : this.platform === 'win32' } ,
2025-09-16 12:03:17 -07:00
) ;
if ( result . status !== 0 ) {
throw new Error (
` Failed to install extension: ${ result . stderr ? . toString ( ) } ` ,
) ;
}
2025-07-30 21:26:31 +00:00
return {
success : true ,
2025-08-25 17:10:36 -04:00
message : ` ${ this . ideInfo . displayName } companion extension was installed successfully. ` ,
2025-07-30 21:26:31 +00:00
} ;
} catch ( _error ) {
return {
success : false ,
2025-08-25 17:10:36 -04:00
message : ` Failed to install ${ this . ideInfo . displayName } companion extension. Please try installing ' ${ GEMINI_CLI_COMPANION_EXTENSION_NAME } ' manually from the ${ this . ideInfo . displayName } extension marketplace. ` ,
2025-07-30 21:26:31 +00:00
} ;
}
}
}
2026-01-28 16:08:03 +01:00
class PositronInstaller implements IdeInstaller {
private vsCodeCommand : Promise < string | null > ;
constructor (
readonly ideInfo : IdeInfo ,
readonly platform = process . platform ,
) {
const command = platform === 'win32' ? 'positron.cmd' : 'positron' ;
this . vsCodeCommand = findCommand ( command , platform ) ;
}
async install ( ) : Promise < InstallResult > {
const commandPath = await this . vsCodeCommand ;
if ( ! commandPath ) {
return {
success : false ,
message : ` ${ this . ideInfo . displayName } CLI not found. Please ensure 'positron' is in your system's PATH. For help, see https://positron.posit.co/add-to-path.html. You can also install the ' ${ GEMINI_CLI_COMPANION_EXTENSION_NAME } ' extension manually from the VS Code marketplace / Open VSX registry. ` ,
} ;
}
try {
const result = child_process . spawnSync (
commandPath ,
[
'--install-extension' ,
'google.gemini-cli-vscode-ide-companion' ,
'--force' ,
] ,
{ stdio : 'pipe' , shell : this.platform === 'win32' } ,
) ;
if ( result . status !== 0 ) {
throw new Error (
` Failed to install extension: ${ result . stderr ? . toString ( ) } ` ,
) ;
}
return {
success : true ,
message : ` ${ this . ideInfo . displayName } companion extension was installed successfully. ` ,
} ;
} catch ( _error ) {
return {
success : false ,
message : ` Failed to install ${ this . ideInfo . displayName } companion extension. Please try installing ' ${ GEMINI_CLI_COMPANION_EXTENSION_NAME } ' manually from the ${ this . ideInfo . displayName } extension marketplace. ` ,
} ;
}
}
}
2025-11-18 12:01:16 -05:00
class AntigravityInstaller implements IdeInstaller {
constructor (
readonly ideInfo : IdeInfo ,
readonly platform = process . platform ,
) { }
async install ( ) : Promise < InstallResult > {
const command = process . env [ 'ANTIGRAVITY_CLI_ALIAS' ] ;
if ( ! command ) {
return {
success : false ,
message : 'ANTIGRAVITY_CLI_ALIAS environment variable not set.' ,
} ;
}
const commandPath = await findCommand ( command , this . platform ) ;
if ( ! commandPath ) {
return {
success : false ,
message : ` ${ command } not found. Please ensure it is in your system's PATH. ` ,
} ;
}
try {
const result = child_process . spawnSync (
commandPath ,
[
'--install-extension' ,
'google.gemini-cli-vscode-ide-companion' ,
'--force' ,
] ,
{ stdio : 'pipe' , shell : this.platform === 'win32' } ,
) ;
if ( result . status !== 0 ) {
throw new Error (
` Failed to install extension: ${ result . stderr ? . toString ( ) } ` ,
) ;
}
return {
success : true ,
message : ` ${ this . ideInfo . displayName } companion extension was installed successfully. ` ,
} ;
} catch ( _error ) {
return {
success : false ,
message : ` Failed to install ${ this . ideInfo . displayName } companion extension. Please try installing ' ${ GEMINI_CLI_COMPANION_EXTENSION_NAME } ' manually from the ${ this . ideInfo . displayName } extension marketplace. ` ,
} ;
}
}
}
2025-08-25 17:10:36 -04:00
export function getIdeInstaller (
2025-09-18 15:23:24 -04:00
ide : IdeInfo ,
2025-08-25 17:10:36 -04:00
platform = process . platform ,
) : IdeInstaller | null {
2025-09-18 15:23:24 -04:00
switch ( ide . name ) {
case IDE_DEFINITIONS.vscode.name :
case IDE_DEFINITIONS.firebasestudio.name :
2025-08-25 17:10:36 -04:00
return new VsCodeInstaller ( ide , platform ) ;
2026-01-28 16:08:03 +01:00
case IDE_DEFINITIONS.positron.name :
return new PositronInstaller ( ide , platform ) ;
2025-11-18 12:01:16 -05:00
case IDE_DEFINITIONS.antigravity.name :
return new AntigravityInstaller ( ide , platform ) ;
2025-07-30 21:26:31 +00:00
default :
2025-08-12 20:08:47 +00:00
return null ;
2025-07-30 21:26:31 +00:00
}
}