From db5b49b2ca1cc6f4b3befa0a13fed1d49af002a6 Mon Sep 17 00:00:00 2001 From: Shreya Keshive Date: Thu, 18 Sep 2025 15:23:24 -0400 Subject: [PATCH] refactor(ide): replace DetectedIde enum with IDE_DEFINITIONS object (#8698) --- packages/cli/src/ui/AppContainer.tsx | 4 +- packages/cli/src/ui/IdeIntegrationNudge.tsx | 7 +- .../cli/src/ui/commands/ideCommand.test.ts | 31 +++--- packages/cli/src/ui/commands/ideCommand.ts | 2 +- .../cli/src/ui/contexts/UIStateContext.tsx | 4 +- packages/core/index.ts | 2 +- packages/core/src/ide/detect-ide.test.ts | 72 ++----------- packages/core/src/ide/detect-ide.ts | 101 +++++------------- packages/core/src/ide/ide-client.test.ts | 12 +-- packages/core/src/ide/ide-client.ts | 22 ++-- packages/core/src/ide/ide-installer.test.ts | 53 +++++---- packages/core/src/ide/ide-installer.ts | 14 ++- packages/core/src/index.ts | 3 +- .../clearcut-logger/clearcut-logger.ts | 4 +- .../src/extension.test.ts | 10 +- .../vscode-ide-companion/src/extension.ts | 17 +-- 16 files changed, 129 insertions(+), 229 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 6bb73b2530..d637952934 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -31,7 +31,7 @@ import { MessageType, StreamingState } from './types.js'; import { type EditorType, type Config, - type DetectedIde, + type IdeInfo, type IdeContext, type UserTierId, DEFAULT_GEMINI_FLASH_MODEL, @@ -717,7 +717,7 @@ Logging in with Google... Please restart Gemini CLI to continue. ]); const [idePromptAnswered, setIdePromptAnswered] = useState(false); - const [currentIDE, setCurrentIDE] = useState(null); + const [currentIDE, setCurrentIDE] = useState(null); useEffect(() => { const getIde = async () => { diff --git a/packages/cli/src/ui/IdeIntegrationNudge.tsx b/packages/cli/src/ui/IdeIntegrationNudge.tsx index 7584e35a8f..5ed6237e85 100644 --- a/packages/cli/src/ui/IdeIntegrationNudge.tsx +++ b/packages/cli/src/ui/IdeIntegrationNudge.tsx @@ -4,8 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import type { DetectedIde } from '@google/gemini-cli-core'; -import { getIdeInfo } from '@google/gemini-cli-core'; +import type { IdeInfo } from '@google/gemini-cli-core'; import { Box, Text } from 'ink'; import type { RadioSelectItem } from './components/shared/RadioButtonSelect.js'; import { RadioButtonSelect } from './components/shared/RadioButtonSelect.js'; @@ -18,7 +17,7 @@ export type IdeIntegrationNudgeResult = { }; interface IdeIntegrationNudgeProps { - ide: DetectedIde; + ide: IdeInfo; onComplete: (result: IdeIntegrationNudgeResult) => void; } @@ -38,7 +37,7 @@ export function IdeIntegrationNudge({ { isActive: true }, ); - const { displayName: ideName } = getIdeInfo(ide); + const { displayName: ideName } = ide; // Assume extension is already installed if the env variables are set. const isExtensionPreInstalled = !!process.env['GEMINI_CLI_IDE_SERVER_PORT'] && diff --git a/packages/cli/src/ui/commands/ideCommand.test.ts b/packages/cli/src/ui/commands/ideCommand.test.ts index 90a7480358..d63d81851c 100644 --- a/packages/cli/src/ui/commands/ideCommand.test.ts +++ b/packages/cli/src/ui/commands/ideCommand.test.ts @@ -8,7 +8,7 @@ import type { MockInstance } from 'vitest'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { ideCommand } from './ideCommand.js'; import { type CommandContext } from './types.js'; -import { DetectedIde } from '@google/gemini-cli-core'; +import { IDE_DEFINITIONS } from '@google/gemini-cli-core'; import * as core from '@google/gemini-cli-core'; vi.mock('@google/gemini-cli-core', async (importOriginal) => { @@ -36,11 +36,14 @@ describe('ideCommand', () => { disconnect: vi.fn(), connect: vi.fn(), getCurrentIde: vi.fn(), - getDetectedIdeDisplayName: vi.fn(), getConnectionStatus: vi.fn(), + getDetectedIdeDisplayName: vi.fn(), } as unknown as core.IdeClient; vi.mocked(core.IdeClient.getInstance).mockResolvedValue(mockIdeClient); + vi.mocked(mockIdeClient.getDetectedIdeDisplayName).mockReturnValue( + 'VS Code', + ); mockContext = { ui: { @@ -66,9 +69,8 @@ describe('ideCommand', () => { }); it('should return the ide command', async () => { - vi.mocked(mockIdeClient.getCurrentIde).mockReturnValue(DetectedIde.VSCode); - vi.mocked(mockIdeClient.getDetectedIdeDisplayName).mockReturnValue( - 'VS Code', + vi.mocked(mockIdeClient.getCurrentIde).mockReturnValue( + IDE_DEFINITIONS.vscode, ); vi.mocked(mockIdeClient.getConnectionStatus).mockReturnValue({ status: core.IDEConnectionStatus.Disconnected, @@ -83,9 +85,8 @@ describe('ideCommand', () => { }); it('should show disable command when connected', async () => { - vi.mocked(mockIdeClient.getCurrentIde).mockReturnValue(DetectedIde.VSCode); - vi.mocked(mockIdeClient.getDetectedIdeDisplayName).mockReturnValue( - 'VS Code', + vi.mocked(mockIdeClient.getCurrentIde).mockReturnValue( + IDE_DEFINITIONS.vscode, ); vi.mocked(mockIdeClient.getConnectionStatus).mockReturnValue({ status: core.IDEConnectionStatus.Connected, @@ -100,10 +101,7 @@ describe('ideCommand', () => { describe('status subcommand', () => { beforeEach(() => { vi.mocked(mockIdeClient.getCurrentIde).mockReturnValue( - DetectedIde.VSCode, - ); - vi.mocked(mockIdeClient.getDetectedIdeDisplayName).mockReturnValue( - 'VS Code', + IDE_DEFINITIONS.vscode, ); }); @@ -177,10 +175,7 @@ describe('ideCommand', () => { const mockInstall = vi.fn(); beforeEach(() => { vi.mocked(mockIdeClient.getCurrentIde).mockReturnValue( - DetectedIde.VSCode, - ); - vi.mocked(mockIdeClient.getDetectedIdeDisplayName).mockReturnValue( - 'VS Code', + IDE_DEFINITIONS.vscode, ); vi.mocked(mockIdeClient.getConnectionStatus).mockReturnValue({ status: core.IDEConnectionStatus.Disconnected, @@ -211,7 +206,7 @@ describe('ideCommand', () => { await vi.runAllTimersAsync(); await actionPromise; - expect(core.getIdeInstaller).toHaveBeenCalledWith('vscode'); + expect(core.getIdeInstaller).toHaveBeenCalledWith(IDE_DEFINITIONS.vscode); expect(mockInstall).toHaveBeenCalled(); expect(mockContext.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ @@ -249,7 +244,7 @@ describe('ideCommand', () => { '', ); - expect(core.getIdeInstaller).toHaveBeenCalledWith('vscode'); + expect(core.getIdeInstaller).toHaveBeenCalledWith(IDE_DEFINITIONS.vscode); expect(mockInstall).toHaveBeenCalled(); expect(mockContext.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/cli/src/ui/commands/ideCommand.ts b/packages/cli/src/ui/commands/ideCommand.ts index b5f002b641..ff1faa2207 100644 --- a/packages/cli/src/ui/commands/ideCommand.ts +++ b/packages/cli/src/ui/commands/ideCommand.ts @@ -135,7 +135,7 @@ async function setIdeModeAndSyncConnection( export const ideCommand = async (): Promise => { const ideClient = await IdeClient.getInstance(); const currentIDE = ideClient.getCurrentIde(); - if (!currentIDE || !ideClient.getDetectedIdeDisplayName()) { + if (!currentIDE) { return { name: 'ide', description: 'manage IDE integration', diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index ae57f2e587..71ad7c5b28 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -21,7 +21,7 @@ import type { IdeContext, ApprovalMode, UserTierId, - DetectedIde, + IdeInfo, FallbackIntent, } from '@google/gemini-cli-core'; import type { DOMElement } from 'ink'; @@ -104,7 +104,7 @@ export interface UIState { terminalWidth: number; terminalHeight: number; mainControlsRef: React.MutableRefObject; - currentIDE: DetectedIde | null; + currentIDE: IdeInfo | null; updateInfo: UpdateObject | null; showIdeRestartPrompt: boolean; isRestarting: boolean; diff --git a/packages/core/index.ts b/packages/core/index.ts index daa3771150..83334558e7 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -23,7 +23,7 @@ export { DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, } from './src/config/config.js'; -export { detectIdeFromEnv, getIdeInfo } from './src/ide/detect-ide.js'; +export { detectIdeFromEnv } from './src/ide/detect-ide.js'; export { logIdeConnection } from './src/telemetry/loggers.js'; export { diff --git a/packages/core/src/ide/detect-ide.test.ts b/packages/core/src/ide/detect-ide.test.ts index e50e4a23f3..0aef991a49 100644 --- a/packages/core/src/ide/detect-ide.test.ts +++ b/packages/core/src/ide/detect-ide.test.ts @@ -5,7 +5,7 @@ */ import { describe, it, expect, vi, afterEach } from 'vitest'; -import { detectIde, DetectedIde, getIdeInfo } from './detect-ide.js'; +import { detectIde, IDE_DEFINITIONS } from './detect-ide.js'; describe('detectIde', () => { const ideProcessInfo = { pid: 123, command: 'some/path/to/code' }; @@ -23,110 +23,60 @@ describe('detectIde', () => { it('should detect Devin', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('__COG_BASHRC_SOURCED', '1'); - expect(detectIde(ideProcessInfo)).toBe(DetectedIde.Devin); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.devin); }); it('should detect Replit', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('REPLIT_USER', 'testuser'); - expect(detectIde(ideProcessInfo)).toBe(DetectedIde.Replit); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.replit); }); it('should detect Cursor', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('CURSOR_TRACE_ID', 'some-id'); - expect(detectIde(ideProcessInfo)).toBe(DetectedIde.Cursor); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cursor); }); it('should detect Codespaces', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('CODESPACES', 'true'); - expect(detectIde(ideProcessInfo)).toBe(DetectedIde.Codespaces); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.codespaces); }); it('should detect Cloud Shell via EDITOR_IN_CLOUD_SHELL', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('EDITOR_IN_CLOUD_SHELL', 'true'); - expect(detectIde(ideProcessInfo)).toBe(DetectedIde.CloudShell); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cloudshell); }); it('should detect Cloud Shell via CLOUD_SHELL', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('CLOUD_SHELL', 'true'); - expect(detectIde(ideProcessInfo)).toBe(DetectedIde.CloudShell); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cloudshell); }); it('should detect Trae', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('TERM_PRODUCT', 'Trae'); - expect(detectIde(ideProcessInfo)).toBe(DetectedIde.Trae); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.trae); }); it('should detect Firebase Studio via MONOSPACE_ENV', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', 'true'); - expect(detectIde(ideProcessInfo)).toBe(DetectedIde.FirebaseStudio); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.firebasestudio); }); it('should detect VSCode when no other IDE is detected and command includes "code"', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', ''); - expect(detectIde(ideProcessInfo)).toBe(DetectedIde.VSCode); + expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.vscode); }); it('should detect VSCodeFork when no other IDE is detected and command does not include "code"', () => { vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('MONOSPACE_ENV', ''); - expect(detectIde(ideProcessInfoNoCode)).toBe(DetectedIde.VSCodeFork); - }); - - it('should prioritize other IDEs over VSCode detection', () => { - vi.stubEnv('TERM_PROGRAM', 'vscode'); - vi.stubEnv('REPLIT_USER', 'testuser'); - expect(detectIde(ideProcessInfo)).toBe(DetectedIde.Replit); - }); -}); - -describe('getIdeInfo', () => { - it('should return correct info for Devin', () => { - expect(getIdeInfo(DetectedIde.Devin)).toEqual({ displayName: 'Devin' }); - }); - - it('should return correct info for Replit', () => { - expect(getIdeInfo(DetectedIde.Replit)).toEqual({ displayName: 'Replit' }); - }); - - it('should return correct info for Cursor', () => { - expect(getIdeInfo(DetectedIde.Cursor)).toEqual({ displayName: 'Cursor' }); - }); - - it('should return correct info for CloudShell', () => { - expect(getIdeInfo(DetectedIde.CloudShell)).toEqual({ - displayName: 'Cloud Shell', - }); - }); - - it('should return correct info for Codespaces', () => { - expect(getIdeInfo(DetectedIde.Codespaces)).toEqual({ - displayName: 'GitHub Codespaces', - }); - }); - - it('should return correct info for FirebaseStudio', () => { - expect(getIdeInfo(DetectedIde.FirebaseStudio)).toEqual({ - displayName: 'Firebase Studio', - }); - }); - - it('should return correct info for Trae', () => { - expect(getIdeInfo(DetectedIde.Trae)).toEqual({ displayName: 'Trae' }); - }); - - it('should return correct info for VSCode', () => { - expect(getIdeInfo(DetectedIde.VSCode)).toEqual({ displayName: 'VS Code' }); - }); - - it('should return correct info for VSCodeFork', () => { - expect(getIdeInfo(DetectedIde.VSCodeFork)).toEqual({ displayName: 'IDE' }); + expect(detectIde(ideProcessInfoNoCode)).toBe(IDE_DEFINITIONS.vscodefork); }); }); diff --git a/packages/core/src/ide/detect-ide.ts b/packages/core/src/ide/detect-ide.ts index 931105e762..8589e379f3 100644 --- a/packages/core/src/ide/detect-ide.ts +++ b/packages/core/src/ide/detect-ide.ts @@ -4,113 +4,70 @@ * SPDX-License-Identifier: Apache-2.0 */ -export enum DetectedIde { - Devin = 'devin', - Replit = 'replit', - Cursor = 'cursor', - CloudShell = 'cloudshell', - Codespaces = 'codespaces', - FirebaseStudio = 'firebasestudio', - Trae = 'trae', - VSCode = 'vscode', - VSCodeFork = 'vscodefork', -} +export const IDE_DEFINITIONS = { + devin: { name: 'devin', displayName: 'Devin' }, + replit: { name: 'replit', displayName: 'Replit' }, + cursor: { name: 'cursor', displayName: 'Cursor' }, + cloudshell: { name: 'cloudshell', displayName: 'Cloud Shell' }, + codespaces: { name: 'codespaces', displayName: 'GitHub Codespaces' }, + firebasestudio: { name: 'firebasestudio', displayName: 'Firebase Studio' }, + trae: { name: 'trae', displayName: 'Trae' }, + vscode: { name: 'vscode', displayName: 'VS Code' }, + vscodefork: { name: 'vscodefork', displayName: 'IDE' }, +} as const; + +export type IdeName = keyof typeof IDE_DEFINITIONS; export interface IdeInfo { + name: IdeName; displayName: string; } -export function getIdeInfo(ide: DetectedIde): IdeInfo { - switch (ide) { - case DetectedIde.Devin: - return { - displayName: 'Devin', - }; - case DetectedIde.Replit: - return { - displayName: 'Replit', - }; - case DetectedIde.Cursor: - return { - displayName: 'Cursor', - }; - case DetectedIde.CloudShell: - return { - displayName: 'Cloud Shell', - }; - case DetectedIde.Codespaces: - return { - displayName: 'GitHub Codespaces', - }; - case DetectedIde.FirebaseStudio: - return { - displayName: 'Firebase Studio', - }; - case DetectedIde.Trae: - return { - displayName: 'Trae', - }; - case DetectedIde.VSCode: - return { - displayName: 'VS Code', - }; - case DetectedIde.VSCodeFork: - return { - displayName: 'IDE', - }; - default: { - // This ensures that if a new IDE is added to the enum, we get a compile-time error. - const exhaustiveCheck: never = ide; - return exhaustiveCheck; - } - } -} - -export function detectIdeFromEnv(): DetectedIde { +export function detectIdeFromEnv(): IdeInfo { if (process.env['__COG_BASHRC_SOURCED']) { - return DetectedIde.Devin; + return IDE_DEFINITIONS.devin; } if (process.env['REPLIT_USER']) { - return DetectedIde.Replit; + return IDE_DEFINITIONS.replit; } if (process.env['CURSOR_TRACE_ID']) { - return DetectedIde.Cursor; + return IDE_DEFINITIONS.cursor; } if (process.env['CODESPACES']) { - return DetectedIde.Codespaces; + return IDE_DEFINITIONS.codespaces; } if (process.env['EDITOR_IN_CLOUD_SHELL'] || process.env['CLOUD_SHELL']) { - return DetectedIde.CloudShell; + return IDE_DEFINITIONS.cloudshell; } if (process.env['TERM_PRODUCT'] === 'Trae') { - return DetectedIde.Trae; + return IDE_DEFINITIONS.trae; } if (process.env['MONOSPACE_ENV']) { - return DetectedIde.FirebaseStudio; + return IDE_DEFINITIONS.firebasestudio; } - return DetectedIde.VSCode; + return IDE_DEFINITIONS.vscode; } function verifyVSCode( - ide: DetectedIde, + ide: IdeInfo, ideProcessInfo: { pid: number; command: string; }, -): DetectedIde { - if (ide !== DetectedIde.VSCode) { +): IdeInfo { + if (ide.name !== IDE_DEFINITIONS.vscode.name) { return ide; } if (ideProcessInfo.command.toLowerCase().includes('code')) { - return DetectedIde.VSCode; + return IDE_DEFINITIONS.vscode; } - return DetectedIde.VSCodeFork; + return IDE_DEFINITIONS.vscodefork; } export function detectIde(ideProcessInfo: { pid: number; command: string; -}): DetectedIde | undefined { +}): IdeInfo | undefined { // Only VSCode-based integrations are currently supported. if (process.env['TERM_PROGRAM'] !== 'vscode') { return undefined; diff --git a/packages/core/src/ide/ide-client.test.ts b/packages/core/src/ide/ide-client.test.ts index 7dff81bda6..fe460abb16 100644 --- a/packages/core/src/ide/ide-client.test.ts +++ b/packages/core/src/ide/ide-client.test.ts @@ -20,12 +20,7 @@ import { getIdeProcessInfo } from './process-utils.js'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; -import { - detectIde, - DetectedIde, - getIdeInfo, - type IdeInfo, -} from './detect-ide.js'; +import { detectIde, IDE_DEFINITIONS } from './detect-ide.js'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -67,10 +62,7 @@ describe('IdeClient', () => { // Mock dependencies vi.spyOn(process, 'cwd').mockReturnValue('/test/workspace/sub-dir'); - vi.mocked(detectIde).mockReturnValue(DetectedIde.VSCode); - vi.mocked(getIdeInfo).mockReturnValue({ - displayName: 'VS Code', - } as IdeInfo); + vi.mocked(detectIde).mockReturnValue(IDE_DEFINITIONS.vscode); vi.mocked(getIdeProcessInfo).mockResolvedValue({ pid: 12345, command: 'test-ide', diff --git a/packages/core/src/ide/ide-client.ts b/packages/core/src/ide/ide-client.ts index 09e259c024..e31d8936f4 100644 --- a/packages/core/src/ide/ide-client.ts +++ b/packages/core/src/ide/ide-client.ts @@ -6,7 +6,7 @@ import * as fs from 'node:fs'; import { isSubpath } from '../utils/paths.js'; -import { detectIde, type DetectedIde, getIdeInfo } from '../ide/detect-ide.js'; +import { detectIde, type IdeInfo } from '../ide/detect-ide.js'; import { ideContextStore } from './ideContext.js'; import { IdeContextNotificationSchema, @@ -85,8 +85,7 @@ export class IdeClient { details: 'IDE integration is currently disabled. To enable it, run /ide enable.', }; - private currentIde: DetectedIde | undefined; - private currentIdeDisplayName: string | undefined; + private currentIde: IdeInfo | undefined; private ideProcessInfo: { pid: number; command: string } | undefined; private authToken: string | undefined; private diffResponses = new Map void>(); @@ -108,11 +107,6 @@ export class IdeClient { const client = new IdeClient(); client.ideProcessInfo = await getIdeProcessInfo(); client.currentIde = detectIde(client.ideProcessInfo); - if (client.currentIde) { - client.currentIdeDisplayName = getIdeInfo( - client.currentIde, - ).displayName; - } return client; })(); } @@ -136,7 +130,7 @@ export class IdeClient { } async connect(): Promise { - if (!this.currentIde || !this.currentIdeDisplayName) { + if (!this.currentIde) { this.setState( IDEConnectionStatus.Disconnected, `IDE integration is not supported in your current environment. To use this feature, run Gemini CLI in one of these supported IDEs: VS Code or VS Code forks`, @@ -157,7 +151,7 @@ export class IdeClient { const { isValid, error } = IdeClient.validateWorkspacePath( workspacePath, - this.currentIdeDisplayName, + this.currentIde.displayName, process.cwd(), ); @@ -203,7 +197,7 @@ export class IdeClient { this.setState( IDEConnectionStatus.Disconnected, - `Failed to connect to IDE companion extension in ${this.currentIdeDisplayName}. Please ensure the extension is running. To install the extension, run /ide install.`, + `Failed to connect to IDE companion extension in ${this.currentIde.displayName}. Please ensure the extension is running. To install the extension, run /ide install.`, true, ); } @@ -407,7 +401,7 @@ export class IdeClient { this.client?.close(); } - getCurrentIde(): DetectedIde | undefined { + getCurrentIde(): IdeInfo | undefined { return this.currentIde; } @@ -416,7 +410,7 @@ export class IdeClient { } getDetectedIdeDisplayName(): string | undefined { - return this.currentIdeDisplayName; + return this.currentIde?.displayName; } isDiffingEnabled(): boolean { @@ -636,7 +630,7 @@ export class IdeClient { } const { isValid } = IdeClient.validateWorkspacePath( content.workspacePath, - this.currentIdeDisplayName, + this.currentIde?.displayName, process.cwd(), ); return isValid; diff --git a/packages/core/src/ide/ide-installer.test.ts b/packages/core/src/ide/ide-installer.test.ts index 4225bd391a..dd29bb85e2 100644 --- a/packages/core/src/ide/ide-installer.test.ts +++ b/packages/core/src/ide/ide-installer.test.ts @@ -4,16 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { getIdeInstaller } from './ide-installer.js'; -import * as child_process from 'node:child_process'; -import * as fs from 'node:fs'; -import * as os from 'node:os'; -import * as path from 'node:path'; -import { DetectedIde } from './detect-ide.js'; +import { vi } from 'vitest'; vi.mock('node:child_process', async (importOriginal) => { - const actual = (await importOriginal()) as typeof child_process; + const actual = + (await importOriginal()) as typeof import('node:child_process'); return { ...actual, execSync: vi.fn(), @@ -23,6 +18,14 @@ vi.mock('node:child_process', async (importOriginal) => { vi.mock('fs'); vi.mock('os'); +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { getIdeInstaller } from './ide-installer.js'; +import * as child_process from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { IDE_DEFINITIONS, type IdeInfo } from './detect-ide.js'; + describe('ide-installer', () => { const HOME_DIR = '/home/user'; @@ -35,23 +38,28 @@ describe('ide-installer', () => { }); describe('getIdeInstaller', () => { - it.each([{ ide: DetectedIde.VSCode }, { ide: DetectedIde.FirebaseStudio }])( - 'returns a VsCodeInstaller for "$ide"', - ({ ide }) => { - const installer = getIdeInstaller(ide); + it.each([ + { ide: IDE_DEFINITIONS.vscode }, + { ide: IDE_DEFINITIONS.firebasestudio }, + ])('returns a VsCodeInstaller for "$ide.name"', ({ ide }) => { + const installer = getIdeInstaller(ide); - expect(installer).not.toBeNull(); - expect(installer?.install).toEqual(expect.any(Function)); - }, - ); + expect(installer).not.toBeNull(); + expect(installer?.install).toEqual(expect.any(Function)); + }); }); describe('VsCodeInstaller', () => { function setup({ - ide = DetectedIde.VSCode, + ide = IDE_DEFINITIONS.vscode, existsResult = false, execSync = () => '', platform = 'linux' as NodeJS.Platform, + }: { + ide?: IdeInfo; + existsResult?: boolean; + execSync?: () => string; + platform?: NodeJS.Platform; } = {}) { vi.spyOn(child_process, 'execSync').mockImplementation(execSync); vi.spyOn(fs, 'existsSync').mockReturnValue(existsResult); @@ -117,12 +125,12 @@ describe('ide-installer', () => { it.each([ { - ide: DetectedIde.VSCode, + ide: IDE_DEFINITIONS.vscode, expectedMessage: 'VS Code companion extension was installed successfully', }, { - ide: DetectedIde.FirebaseStudio, + ide: IDE_DEFINITIONS.firebasestudio, expectedMessage: 'Firebase Studio companion extension was installed successfully', }, @@ -137,9 +145,12 @@ describe('ide-installer', () => { ); it.each([ - { ide: DetectedIde.VSCode, expectedErr: 'VS Code CLI not found' }, { - ide: DetectedIde.FirebaseStudio, + ide: IDE_DEFINITIONS.vscode, + expectedErr: 'VS Code CLI not found', + }, + { + ide: IDE_DEFINITIONS.firebasestudio, expectedErr: 'Firebase Studio CLI not found', }, ])( diff --git a/packages/core/src/ide/ide-installer.ts b/packages/core/src/ide/ide-installer.ts index 720b0f3c06..554b87a987 100644 --- a/packages/core/src/ide/ide-installer.ts +++ b/packages/core/src/ide/ide-installer.ts @@ -9,7 +9,7 @@ import * as process from 'node:process'; import * as path from 'node:path'; import * as fs from 'node:fs'; import * as os from 'node:os'; -import { DetectedIde, getIdeInfo, type IdeInfo } from './detect-ide.js'; +import { IDE_DEFINITIONS, type IdeInfo } from './detect-ide.js'; import { GEMINI_CLI_COMPANION_EXTENSION_NAME } from './constants.js'; function getVsCodeCommand(platform: NodeJS.Platform = process.platform) { @@ -100,14 +100,12 @@ async function findVsCodeCommand( class VsCodeInstaller implements IdeInstaller { private vsCodeCommand: Promise; - private readonly ideInfo: IdeInfo; constructor( - readonly ide: DetectedIde, + readonly ideInfo: IdeInfo, readonly platform = process.platform, ) { this.vsCodeCommand = findVsCodeCommand(platform); - this.ideInfo = getIdeInfo(ide); } async install(): Promise { @@ -150,12 +148,12 @@ class VsCodeInstaller implements IdeInstaller { } export function getIdeInstaller( - ide: DetectedIde, + ide: IdeInfo, platform = process.platform, ): IdeInstaller | null { - switch (ide) { - case DetectedIde.VSCode: - case DetectedIde.FirebaseStudio: + switch (ide.name) { + case IDE_DEFINITIONS.vscode.name: + case IDE_DEFINITIONS.firebasestudio.name: return new VsCodeInstaller(ide, platform); default: return null; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e076d090c9..4a640f3f2d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -64,8 +64,7 @@ export * from './services/fileSystemService.js'; export * from './ide/ide-client.js'; export * from './ide/ideContext.js'; export * from './ide/ide-installer.js'; -export { getIdeInfo, DetectedIde } from './ide/detect-ide.js'; -export { type IdeInfo } from './ide/detect-ide.js'; +export { IDE_DEFINITIONS, type IdeInfo } from './ide/detect-ide.js'; export * from './ide/constants.js'; export * from './ide/types.js'; diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 57ce1323e3..c4d3caa3ac 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -37,7 +37,7 @@ import { UserAccountManager } from '../../utils/userAccountManager.js'; import { safeJsonStringify } from '../../utils/safeJsonStringify.js'; import { FixedDeque } from 'mnemonist'; import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js'; -import { DetectedIde, detectIdeFromEnv } from '../../ide/detect-ide.js'; +import { IDE_DEFINITIONS, detectIdeFromEnv } from '../../ide/detect-ide.js'; export enum EventNames { START_SESSION = 'start_session', @@ -113,7 +113,7 @@ function determineSurface(): string { } else if (process.env['GITHUB_SHA']) { return 'GitHub'; } else if (process.env['TERM_PROGRAM'] === 'vscode') { - return detectIdeFromEnv() || DetectedIde.VSCode; + return detectIdeFromEnv().name || IDE_DEFINITIONS.vscode.name; } else { return 'SURFACE_NOT_SET'; } diff --git a/packages/vscode-ide-companion/src/extension.test.ts b/packages/vscode-ide-companion/src/extension.test.ts index 8e28adc17a..f7acb2956e 100644 --- a/packages/vscode-ide-companion/src/extension.test.ts +++ b/packages/vscode-ide-companion/src/extension.test.ts @@ -7,13 +7,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as vscode from 'vscode'; import { activate } from './extension.js'; -import { DetectedIde, detectIdeFromEnv } from '@google/gemini-cli-core'; +import { IDE_DEFINITIONS, detectIdeFromEnv } from '@google/gemini-cli-core'; vi.mock('@google/gemini-cli-core', async () => { const actual = await vi.importActual('@google/gemini-cli-core'); return { ...actual, - detectIdeFromEnv: vi.fn(() => DetectedIde.VSCode), + detectIdeFromEnv: vi.fn(() => IDE_DEFINITIONS.vscode), }; }); @@ -198,10 +198,10 @@ describe('activate', () => { it.each([ { - ide: DetectedIde.CloudShell, + ide: IDE_DEFINITIONS.cloudshell, }, - { ide: DetectedIde.FirebaseStudio }, - ])('does not show the notification for $ide', async ({ ide }) => { + { ide: IDE_DEFINITIONS.firebasestudio }, + ])('does not show the notification for $ide.name', async ({ ide }) => { vi.mocked(detectIdeFromEnv).mockReturnValue(ide); vi.mocked(context.globalState.get).mockReturnValue(undefined); const showInformationMessageMock = vi.mocked( diff --git a/packages/vscode-ide-companion/src/extension.ts b/packages/vscode-ide-companion/src/extension.ts index b7dcb97bfc..e75158d660 100644 --- a/packages/vscode-ide-companion/src/extension.ts +++ b/packages/vscode-ide-companion/src/extension.ts @@ -9,7 +9,11 @@ import { IDEServer } from './ide-server.js'; import semver from 'semver'; import { DiffContentProvider, DiffManager } from './diff-manager.js'; import { createLogger } from './utils/logger.js'; -import { detectIdeFromEnv, DetectedIde } from '@google/gemini-cli-core'; +import { + detectIdeFromEnv, + IDE_DEFINITIONS, + type IdeInfo, +} from '@google/gemini-cli-core'; const CLI_IDE_COMPANION_IDENTIFIER = 'Google.gemini-cli-vscode-ide-companion'; const INFO_MESSAGE_SHOWN_KEY = 'geminiCliInfoMessageShown'; @@ -20,9 +24,9 @@ export const DIFF_SCHEME = 'gemini-diff'; * environments we either are pre-installed and the installation message is * confusing or we just want to be quiet. */ -const HIDE_INSTALLATION_GREETING_IDES: ReadonlySet = new Set([ - DetectedIde.FirebaseStudio, - DetectedIde.CloudShell, +const HIDE_INSTALLATION_GREETING_IDES: ReadonlySet = new Set([ + IDE_DEFINITIONS.firebasestudio.name, + IDE_DEFINITIONS.cloudshell.name, ]); let ideServer: IDEServer; @@ -144,8 +148,9 @@ export async function activate(context: vscode.ExtensionContext) { log(`Failed to start IDE server: ${message}`); } - const infoMessageEnabled = - !HIDE_INSTALLATION_GREETING_IDES.has(detectIdeFromEnv()); + const infoMessageEnabled = !HIDE_INSTALLATION_GREETING_IDES.has( + detectIdeFromEnv().name, + ); if (!context.globalState.get(INFO_MESSAGE_SHOWN_KEY) && infoMessageEnabled) { void vscode.window.showInformationMessage(