diff --git a/packages/core/src/ide/detect-ide.test.ts b/packages/core/src/ide/detect-ide.test.ts index 1f717cec56..0b27b27560 100644 --- a/packages/core/src/ide/detect-ide.test.ts +++ b/packages/core/src/ide/detect-ide.test.ts @@ -28,6 +28,7 @@ describe('detectIde', () => { vi.stubEnv('TERM_PRODUCT', ''); vi.stubEnv('MONOSPACE_ENV', ''); vi.stubEnv('REPLIT_USER', ''); + vi.stubEnv('POSITRON', ''); vi.stubEnv('__COG_BASHRC_SOURCED', ''); vi.stubEnv('TERMINAL_EMULATOR', ''); }); @@ -100,6 +101,7 @@ describe('detectIde', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', ''); vi.stubEnv('CURSOR_TRACE_ID', ''); + vi.stubEnv('POSITRON', ''); expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.vscode); }); @@ -107,11 +109,21 @@ describe('detectIde', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', ''); vi.stubEnv('CURSOR_TRACE_ID', ''); + vi.stubEnv('POSITRON', ''); expect(detectIde(ideProcessInfoNoCode)).toBe(IDE_DEFINITIONS.vscodefork); }); + it('should detect positron when POSITRON is set', () => { + vi.stubEnv('TERM_PROGRAM', 'vscode'); + vi.stubEnv('MONOSPACE_ENV', ''); + vi.stubEnv('CURSOR_TRACE_ID', ''); + vi.stubEnv('POSITRON', '1'); + expect(detectIde(ideProcessInfoNoCode)).toBe(IDE_DEFINITIONS.positron); + }); + it('should detect AntiGravity', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); + vi.stubEnv('POSITRON', ''); vi.stubEnv('ANTIGRAVITY_CLI_ALIAS', 'agy'); expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.antigravity); }); @@ -196,6 +208,7 @@ describe('detectIde with ideInfoFromFile', () => { vi.stubEnv('TERM_PRODUCT', ''); vi.stubEnv('MONOSPACE_ENV', ''); vi.stubEnv('REPLIT_USER', ''); + vi.stubEnv('POSITRON', ''); vi.stubEnv('__COG_BASHRC_SOURCED', ''); vi.stubEnv('TERMINAL_EMULATOR', ''); }); @@ -212,6 +225,7 @@ describe('detectIde with ideInfoFromFile', () => { const ideInfoFromFile = { displayName: 'Custom IDE' }; vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('CURSOR_TRACE_ID', ''); + vi.stubEnv('POSITRON', ''); expect(detectIde(ideProcessInfo, ideInfoFromFile)).toBe( IDE_DEFINITIONS.vscode, ); @@ -221,6 +235,7 @@ describe('detectIde with ideInfoFromFile', () => { const ideInfoFromFile = { name: 'custom-ide' }; vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('CURSOR_TRACE_ID', ''); + vi.stubEnv('POSITRON', ''); expect(detectIde(ideProcessInfo, ideInfoFromFile)).toBe( IDE_DEFINITIONS.vscode, ); diff --git a/packages/core/src/ide/detect-ide.ts b/packages/core/src/ide/detect-ide.ts index 6c1f0b458b..40aae11b7f 100644 --- a/packages/core/src/ide/detect-ide.ts +++ b/packages/core/src/ide/detect-ide.ts @@ -14,6 +14,7 @@ export const IDE_DEFINITIONS = { trae: { name: 'trae', displayName: 'Trae' }, vscode: { name: 'vscode', displayName: 'VS Code' }, vscodefork: { name: 'vscodefork', displayName: 'IDE' }, + positron: { name: 'positron', displayName: 'Positron' }, antigravity: { name: 'antigravity', displayName: 'Antigravity' }, sublimetext: { name: 'sublimetext', displayName: 'Sublime Text' }, jetbrains: { name: 'jetbrains', displayName: 'JetBrains IDE' }, @@ -68,6 +69,9 @@ export function detectIdeFromEnv(): IdeInfo { if (process.env['MONOSPACE_ENV']) { return IDE_DEFINITIONS.firebasestudio; } + if (process.env['POSITRON'] === '1') { + return IDE_DEFINITIONS.positron; + } if (process.env['TERM_PROGRAM'] === 'sublime') { return IDE_DEFINITIONS.sublimetext; } diff --git a/packages/core/src/ide/ide-installer.test.ts b/packages/core/src/ide/ide-installer.test.ts index 5f0ab9abb4..e35cb3280f 100644 --- a/packages/core/src/ide/ide-installer.test.ts +++ b/packages/core/src/ide/ide-installer.test.ts @@ -202,6 +202,53 @@ describe('ide-installer', () => { ); }); }); + + describe('PositronInstaller', () => { + function setup({ + execSync = () => '', + platform = 'linux' as NodeJS.Platform, + existsResult = false, + }: { + execSync?: () => string; + platform?: NodeJS.Platform; + existsResult?: boolean; + } = {}) { + vi.spyOn(child_process, 'execSync').mockImplementation(execSync); + vi.spyOn(fs, 'existsSync').mockReturnValue(existsResult); + const installer = getIdeInstaller(IDE_DEFINITIONS.positron, platform)!; + + return { installer }; + } + + it('installs the extension', async () => { + vi.stubEnv('POSITRON', '1'); + const { installer } = setup({}); + const result = await installer.install(); + + expect(result.success).toBe(true); + expect(child_process.spawnSync).toHaveBeenCalledWith( + 'positron', + [ + '--install-extension', + 'google.gemini-cli-vscode-ide-companion', + '--force', + ], + { stdio: 'pipe', shell: false }, + ); + }); + + it('returns a failure message if the cli is not found', async () => { + const { installer } = setup({ + execSync: () => { + throw new Error('Command not found'); + }, + }); + const result = await installer.install(); + + expect(result.success).toBe(false); + expect(result.message).toContain('Positron CLI not found'); + }); + }); }); describe('AntigravityInstaller', () => { diff --git a/packages/core/src/ide/ide-installer.ts b/packages/core/src/ide/ide-installer.ts index 903e831268..886670d4f8 100644 --- a/packages/core/src/ide/ide-installer.ts +++ b/packages/core/src/ide/ide-installer.ts @@ -51,39 +51,88 @@ async function findCommand( const locations: string[] = []; const homeDir = homedir(); + 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; + if (command === 'code' || command === 'code.cmd') { + appname = 'code'; + } else if (command === 'positron' || command === 'positron.cmd') { + appname = 'positron'; + } + + if (appname) { if (platform === 'darwin') { // macOS - locations.push( - '/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code', - path.join(homeDir, 'Library/Application Support/Code/bin/code'), - ); + 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}`, + ), + ); + } } else if (platform === 'linux') { // Linux - locations.push( - '/usr/share/code/bin/code', - '/snap/bin/code', - path.join(homeDir, '.local/share/code/bin/code'), - ); + 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}`, + ), + ); + } } else if (platform === 'win32') { // Windows - locations.push( - path.join( - process.env['ProgramFiles'] || 'C:\\Program Files', - 'Microsoft VS Code', - 'bin', - 'code.cmd', - ), - path.join( - homeDir, - 'AppData', - 'Local', - 'Programs', - 'Microsoft VS Code', - 'bin', - 'code.cmd', - ), - ); + 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, + ), + ); + } } } @@ -146,6 +195,56 @@ class VsCodeInstaller implements IdeInstaller { } } +class PositronInstaller implements IdeInstaller { + private vsCodeCommand: Promise; + + constructor( + readonly ideInfo: IdeInfo, + readonly platform = process.platform, + ) { + const command = platform === 'win32' ? 'positron.cmd' : 'positron'; + this.vsCodeCommand = findCommand(command, platform); + } + + async install(): Promise { + 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.`, + }; + } + } +} + class AntigravityInstaller implements IdeInstaller { constructor( readonly ideInfo: IdeInfo, @@ -207,6 +306,8 @@ export function getIdeInstaller( case IDE_DEFINITIONS.vscode.name: case IDE_DEFINITIONS.firebasestudio.name: return new VsCodeInstaller(ide, platform); + case IDE_DEFINITIONS.positron.name: + return new PositronInstaller(ide, platform); case IDE_DEFINITIONS.antigravity.name: return new AntigravityInstaller(ide, platform); default: diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts index 8af85e88d4..fa7dd705c6 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts @@ -462,9 +462,20 @@ describe('ClearcutLogger', () => { TERM_PROGRAM: 'vscode', GITHUB_SHA: undefined, MONOSPACE_ENV: '', + POSITRON: '', }, expected: 'vscode', }, + { + name: 'Positron via TERM_PROGRAM', + env: { + TERM_PROGRAM: 'vscode', + GITHUB_SHA: undefined, + MONOSPACE_ENV: '', + POSITRON: '1', + }, + expected: 'positron', + }, { name: 'SURFACE env var', env: { SURFACE: 'ide-1234' },