diff options
Diffstat (limited to 'packages/cli/src/utils')
| -rw-r--r-- | packages/cli/src/utils/handleAutoUpdate.test.ts | 153 | ||||
| -rw-r--r-- | packages/cli/src/utils/handleAutoUpdate.ts | 139 | ||||
| -rw-r--r-- | packages/cli/src/utils/installationInfo.test.ts | 313 | ||||
| -rw-r--r-- | packages/cli/src/utils/installationInfo.ts | 177 | ||||
| -rw-r--r-- | packages/cli/src/utils/updateEventEmitter.ts | 13 |
5 files changed, 795 insertions, 0 deletions
diff --git a/packages/cli/src/utils/handleAutoUpdate.test.ts b/packages/cli/src/utils/handleAutoUpdate.test.ts new file mode 100644 index 00000000..adaed932 --- /dev/null +++ b/packages/cli/src/utils/handleAutoUpdate.test.ts @@ -0,0 +1,153 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { ChildProcess, spawn } from 'node:child_process'; +import { handleAutoUpdate } from './handleAutoUpdate.js'; +import { getInstallationInfo, PackageManager } from './installationInfo.js'; +import { updateEventEmitter } from './updateEventEmitter.js'; +import { UpdateObject } from '../ui/utils/updateCheck.js'; +import { LoadedSettings } from '../config/settings.js'; + +// Mock dependencies +vi.mock('node:child_process', async () => { + const actual = await vi.importActual('node:child_process'); + return { + ...actual, + spawn: vi.fn(), + }; +}); + +vi.mock('./installationInfo.js', async () => { + const actual = await vi.importActual('./installationInfo.js'); + return { + ...actual, + getInstallationInfo: vi.fn(), + }; +}); + +vi.mock('./updateEventEmitter.js', async () => { + const actual = await vi.importActual('./updateEventEmitter.js'); + return { + ...actual, + updateEventEmitter: { + ...actual.updateEventEmitter, + emit: vi.fn(), + }, + }; +}); + +const mockSpawn = vi.mocked(spawn); +const mockGetInstallationInfo = vi.mocked(getInstallationInfo); +const mockUpdateEventEmitter = vi.mocked(updateEventEmitter); + +describe('handleAutoUpdate', () => { + let mockUpdateInfo: UpdateObject; + let mockSettings: LoadedSettings; + let mockChildProcess: { + stderr: { on: ReturnType<typeof vi.fn> }; + stdout: { on: ReturnType<typeof vi.fn> }; + on: ReturnType<typeof vi.fn>; + unref: ReturnType<typeof vi.fn>; + }; + + beforeEach(() => { + mockUpdateInfo = { + update: { + latest: '2.0.0', + current: '1.0.0', + type: 'major', + name: '@google/gemini-cli', + }, + message: 'An update is available!', + }; + + mockSettings = { + merged: { + disableAutoUpdate: false, + }, + } as LoadedSettings; + + mockChildProcess = { + stdout: { on: vi.fn() }, + stderr: { on: vi.fn() }, + on: vi.fn(), + unref: vi.fn(), + }; + mockSpawn.mockReturnValue(mockChildProcess as unknown as ChildProcess); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should do nothing if update info is null', () => { + handleAutoUpdate(null, mockSettings, '/root'); + expect(mockGetInstallationInfo).not.toHaveBeenCalled(); + expect(mockUpdateEventEmitter.emit).not.toHaveBeenCalled(); + expect(mockSpawn).not.toHaveBeenCalled(); + }); + + it('should emit "update-received" but not update if auto-updates are disabled', () => { + mockSettings.merged.disableAutoUpdate = true; + mockGetInstallationInfo.mockReturnValue({ + updateCommand: 'npm i -g @google/gemini-cli@latest', + updateMessage: 'Please update manually.', + isGlobal: true, + packageManager: PackageManager.NPM, + }); + + handleAutoUpdate(mockUpdateInfo, mockSettings, '/root'); + + expect(mockUpdateEventEmitter.emit).toHaveBeenCalledTimes(1); + expect(mockUpdateEventEmitter.emit).toHaveBeenCalledWith( + 'update-received', + { + message: 'An update is available!\nPlease update manually.', + }, + ); + expect(mockSpawn).not.toHaveBeenCalled(); + }); + + it('should emit "update-received" but not update if no update command is found', () => { + mockGetInstallationInfo.mockReturnValue({ + updateCommand: undefined, + updateMessage: 'Cannot determine update command.', + isGlobal: false, + packageManager: PackageManager.NPM, + }); + + handleAutoUpdate(mockUpdateInfo, mockSettings, '/root'); + + expect(mockUpdateEventEmitter.emit).toHaveBeenCalledTimes(1); + expect(mockUpdateEventEmitter.emit).toHaveBeenCalledWith( + 'update-received', + { + message: 'An update is available!\nCannot determine update command.', + }, + ); + expect(mockSpawn).not.toHaveBeenCalled(); + }); + + it('should combine update messages correctly', () => { + mockGetInstallationInfo.mockReturnValue({ + updateCommand: undefined, // No command to prevent spawn + updateMessage: 'This is an additional message.', + isGlobal: false, + packageManager: PackageManager.NPM, + }); + + handleAutoUpdate(mockUpdateInfo, mockSettings, '/root'); + + expect(mockUpdateEventEmitter.emit).toHaveBeenCalledTimes(1); + expect(mockUpdateEventEmitter.emit).toHaveBeenCalledWith( + 'update-received', + { + message: 'An update is available!\nThis is an additional message.', + }, + ); + }); +}); diff --git a/packages/cli/src/utils/handleAutoUpdate.ts b/packages/cli/src/utils/handleAutoUpdate.ts new file mode 100644 index 00000000..1ef2d475 --- /dev/null +++ b/packages/cli/src/utils/handleAutoUpdate.ts @@ -0,0 +1,139 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { spawn } from 'node:child_process'; +import { UpdateObject } from '../ui/utils/updateCheck.js'; +import { LoadedSettings } from '../config/settings.js'; +import { getInstallationInfo } from './installationInfo.js'; +import { updateEventEmitter } from './updateEventEmitter.js'; +import { HistoryItem, MessageType } from '../ui/types.js'; + +export function handleAutoUpdate( + info: UpdateObject | null, + settings: LoadedSettings, + projectRoot: string, +) { + if (!info) { + return; + } + + const installationInfo = getInstallationInfo( + projectRoot, + settings.merged.disableAutoUpdate ?? false, + ); + + let combinedMessage = info.message; + if (installationInfo.updateMessage) { + combinedMessage += `\n${installationInfo.updateMessage}`; + } + + updateEventEmitter.emit('update-received', { + message: combinedMessage, + }); + + if (!installationInfo.updateCommand || settings.merged.disableAutoUpdate) { + return; + } + + const updateCommand = installationInfo.updateCommand.replace( + '@latest', + `@${info.update.latest}`, + ); + + const updateProcess = spawn(updateCommand, { stdio: 'pipe', shell: true }); + let errorOutput = ''; + updateProcess.stderr.on('data', (data) => { + errorOutput += data.toString(); + }); + + updateProcess.on('close', (code) => { + if (code === 0) { + updateEventEmitter.emit('update-success', { + message: + 'Update successful! The new version will be used on your next run.', + }); + } else { + updateEventEmitter.emit('update-failed', { + message: `Automatic update failed. Please try updating manually. (command: ${updateCommand}, stderr: ${errorOutput.trim()})`, + }); + } + }); + + updateProcess.on('error', (err) => { + updateEventEmitter.emit('update-failed', { + message: `Automatic update failed. Please try updating manually. (error: ${err.message})`, + }); + }); + return updateProcess; +} + +export function setUpdateHandler( + addItem: (item: Omit<HistoryItem, 'id'>, timestamp: number) => void, + setUpdateInfo: (info: UpdateObject | null) => void, +) { + let successfullyInstalled = false; + const handleUpdateRecieved = (info: UpdateObject) => { + setUpdateInfo(info); + const savedMessage = info.message; + setTimeout(() => { + if (!successfullyInstalled) { + addItem( + { + type: MessageType.INFO, + text: savedMessage, + }, + Date.now(), + ); + } + setUpdateInfo(null); + }, 60000); + }; + + const handleUpdateFailed = () => { + setUpdateInfo(null); + addItem( + { + type: MessageType.ERROR, + text: `Automatic update failed. Please try updating manually`, + }, + Date.now(), + ); + }; + + const handleUpdateSuccess = () => { + successfullyInstalled = true; + setUpdateInfo(null); + addItem( + { + type: MessageType.INFO, + text: `Update successful! The new version will be used on your next run.`, + }, + Date.now(), + ); + }; + + const handleUpdateInfo = (data: { message: string }) => { + addItem( + { + type: MessageType.INFO, + text: data.message, + }, + Date.now(), + ); + }; + + updateEventEmitter.on('update-received', handleUpdateRecieved); + updateEventEmitter.on('update-failed', handleUpdateFailed); + updateEventEmitter.on('update-success', handleUpdateSuccess); + updateEventEmitter.on('update-info', handleUpdateInfo); + + return () => { + updateEventEmitter.off('update-received', handleUpdateRecieved); + updateEventEmitter.off('update-failed', handleUpdateFailed); + updateEventEmitter.off('update-success', handleUpdateSuccess); + updateEventEmitter.off('update-info', handleUpdateInfo); + }; +} diff --git a/packages/cli/src/utils/installationInfo.test.ts b/packages/cli/src/utils/installationInfo.test.ts new file mode 100644 index 00000000..c2bcf074 --- /dev/null +++ b/packages/cli/src/utils/installationInfo.test.ts @@ -0,0 +1,313 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { getInstallationInfo, PackageManager } from './installationInfo.js'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as childProcess from 'child_process'; +import { isGitRepository } from '@google/gemini-cli-core'; + +vi.mock('@google/gemini-cli-core', () => ({ + isGitRepository: vi.fn(), +})); + +vi.mock('fs', async (importOriginal) => { + const actualFs = await importOriginal<typeof fs>(); + return { + ...actualFs, + realpathSync: vi.fn(), + existsSync: vi.fn(), + }; +}); + +vi.mock('child_process', async (importOriginal) => { + const actual = await importOriginal<typeof import('child_process')>(); + return { + ...actual, + execSync: vi.fn(), + }; +}); + +const mockedIsGitRepository = vi.mocked(isGitRepository); +const mockedRealPathSync = vi.mocked(fs.realpathSync); +const mockedExistsSync = vi.mocked(fs.existsSync); +const mockedExecSync = vi.mocked(childProcess.execSync); + +describe('getInstallationInfo', () => { + const projectRoot = '/path/to/project'; + let originalArgv: string[]; + + beforeEach(() => { + vi.resetAllMocks(); + originalArgv = [...process.argv]; + // Mock process.cwd() for isGitRepository + vi.spyOn(process, 'cwd').mockReturnValue(projectRoot); + }); + + afterEach(() => { + process.argv = originalArgv; + }); + + it('should return UNKNOWN when cliPath is not available', () => { + process.argv[1] = ''; + const info = getInstallationInfo(projectRoot, false); + expect(info.packageManager).toBe(PackageManager.UNKNOWN); + }); + + it('should return UNKNOWN and log error if realpathSync fails', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + process.argv[1] = '/path/to/cli'; + const error = new Error('realpath failed'); + mockedRealPathSync.mockImplementation(() => { + throw error; + }); + + const info = getInstallationInfo(projectRoot, false); + + expect(info.packageManager).toBe(PackageManager.UNKNOWN); + expect(consoleSpy).toHaveBeenCalledWith(error); + consoleSpy.mockRestore(); + }); + + it('should detect running from a local git clone', () => { + process.argv[1] = `${projectRoot}/packages/cli/dist/index.js`; + mockedRealPathSync.mockReturnValue( + `${projectRoot}/packages/cli/dist/index.js`, + ); + mockedIsGitRepository.mockReturnValue(true); + + const info = getInstallationInfo(projectRoot, false); + + expect(info.packageManager).toBe(PackageManager.UNKNOWN); + expect(info.isGlobal).toBe(false); + expect(info.updateMessage).toBe( + 'Running from a local git clone. Please update with "git pull".', + ); + }); + + it('should detect running via npx', () => { + const npxPath = `/Users/test/.npm/_npx/12345/bin/gemini`; + process.argv[1] = npxPath; + mockedRealPathSync.mockReturnValue(npxPath); + + const info = getInstallationInfo(projectRoot, false); + + expect(info.packageManager).toBe(PackageManager.NPX); + expect(info.isGlobal).toBe(false); + expect(info.updateMessage).toBe('Running via npx, update not applicable.'); + }); + + it('should detect running via pnpx', () => { + const pnpxPath = `/Users/test/.pnpm/_pnpx/12345/bin/gemini`; + process.argv[1] = pnpxPath; + mockedRealPathSync.mockReturnValue(pnpxPath); + + const info = getInstallationInfo(projectRoot, false); + + expect(info.packageManager).toBe(PackageManager.PNPX); + expect(info.isGlobal).toBe(false); + expect(info.updateMessage).toBe('Running via pnpx, update not applicable.'); + }); + + it('should detect running via bunx', () => { + const bunxPath = `/Users/test/.bun/install/cache/12345/bin/gemini`; + process.argv[1] = bunxPath; + mockedRealPathSync.mockReturnValue(bunxPath); + mockedExecSync.mockImplementation(() => { + throw new Error('Command failed'); + }); + + const info = getInstallationInfo(projectRoot, false); + + expect(info.packageManager).toBe(PackageManager.BUNX); + expect(info.isGlobal).toBe(false); + expect(info.updateMessage).toBe('Running via bunx, update not applicable.'); + }); + + it('should detect Homebrew installation via execSync', () => { + Object.defineProperty(process, 'platform', { + value: 'darwin', + }); + const cliPath = '/usr/local/bin/gemini'; + process.argv[1] = cliPath; + mockedRealPathSync.mockReturnValue(cliPath); + mockedExecSync.mockReturnValue(Buffer.from('gemini-cli')); // Simulate successful command + + const info = getInstallationInfo(projectRoot, false); + + expect(mockedExecSync).toHaveBeenCalledWith( + 'brew list -1 | grep -q "^gemini-cli$"', + { stdio: 'ignore' }, + ); + expect(info.packageManager).toBe(PackageManager.HOMEBREW); + expect(info.isGlobal).toBe(true); + expect(info.updateMessage).toContain('brew upgrade'); + }); + + it('should fall through if brew command fails', () => { + Object.defineProperty(process, 'platform', { + value: 'darwin', + }); + const cliPath = '/usr/local/bin/gemini'; + process.argv[1] = cliPath; + mockedRealPathSync.mockReturnValue(cliPath); + mockedExecSync.mockImplementation(() => { + throw new Error('Command failed'); + }); + + const info = getInstallationInfo(projectRoot, false); + + expect(mockedExecSync).toHaveBeenCalledWith( + 'brew list -1 | grep -q "^gemini-cli$"', + { stdio: 'ignore' }, + ); + // Should fall back to default global npm + expect(info.packageManager).toBe(PackageManager.NPM); + expect(info.isGlobal).toBe(true); + }); + + it('should detect global pnpm installation', () => { + const pnpmPath = `/Users/test/.pnpm/global/5/node_modules/.pnpm/some-hash/node_modules/@google/gemini-cli/dist/index.js`; + process.argv[1] = pnpmPath; + mockedRealPathSync.mockReturnValue(pnpmPath); + mockedExecSync.mockImplementation(() => { + throw new Error('Command failed'); + }); + + const info = getInstallationInfo(projectRoot, false); + expect(info.packageManager).toBe(PackageManager.PNPM); + expect(info.isGlobal).toBe(true); + expect(info.updateCommand).toBe('pnpm add -g @google/gemini-cli@latest'); + expect(info.updateMessage).toContain('Attempting to automatically update'); + + const infoDisabled = getInstallationInfo(projectRoot, true); + expect(infoDisabled.updateMessage).toContain('Please run pnpm add'); + }); + + it('should detect global yarn installation', () => { + const yarnPath = `/Users/test/.yarn/global/node_modules/@google/gemini-cli/dist/index.js`; + process.argv[1] = yarnPath; + mockedRealPathSync.mockReturnValue(yarnPath); + mockedExecSync.mockImplementation(() => { + throw new Error('Command failed'); + }); + + const info = getInstallationInfo(projectRoot, false); + expect(info.packageManager).toBe(PackageManager.YARN); + expect(info.isGlobal).toBe(true); + expect(info.updateCommand).toBe( + 'yarn global add @google/gemini-cli@latest', + ); + expect(info.updateMessage).toContain('Attempting to automatically update'); + + const infoDisabled = getInstallationInfo(projectRoot, true); + expect(infoDisabled.updateMessage).toContain('Please run yarn global add'); + }); + + it('should detect global bun installation', () => { + const bunPath = `/Users/test/.bun/bin/gemini`; + process.argv[1] = bunPath; + mockedRealPathSync.mockReturnValue(bunPath); + mockedExecSync.mockImplementation(() => { + throw new Error('Command failed'); + }); + + const info = getInstallationInfo(projectRoot, false); + expect(info.packageManager).toBe(PackageManager.BUN); + expect(info.isGlobal).toBe(true); + expect(info.updateCommand).toBe('bun add -g @google/gemini-cli@latest'); + expect(info.updateMessage).toContain('Attempting to automatically update'); + + const infoDisabled = getInstallationInfo(projectRoot, true); + expect(infoDisabled.updateMessage).toContain('Please run bun add'); + }); + + it('should detect local installation and identify yarn from lockfile', () => { + const localPath = `${projectRoot}/node_modules/.bin/gemini`; + process.argv[1] = localPath; + mockedRealPathSync.mockReturnValue(localPath); + mockedExecSync.mockImplementation(() => { + throw new Error('Command failed'); + }); + mockedExistsSync.mockImplementation( + (p) => p === path.join(projectRoot, 'yarn.lock'), + ); + + const info = getInstallationInfo(projectRoot, false); + + expect(info.packageManager).toBe(PackageManager.YARN); + expect(info.isGlobal).toBe(false); + expect(info.updateMessage).toContain('Locally installed'); + }); + + it('should detect local installation and identify pnpm from lockfile', () => { + const localPath = `${projectRoot}/node_modules/.bin/gemini`; + process.argv[1] = localPath; + mockedRealPathSync.mockReturnValue(localPath); + mockedExecSync.mockImplementation(() => { + throw new Error('Command failed'); + }); + mockedExistsSync.mockImplementation( + (p) => p === path.join(projectRoot, 'pnpm-lock.yaml'), + ); + + const info = getInstallationInfo(projectRoot, false); + + expect(info.packageManager).toBe(PackageManager.PNPM); + expect(info.isGlobal).toBe(false); + }); + + it('should detect local installation and identify bun from lockfile', () => { + const localPath = `${projectRoot}/node_modules/.bin/gemini`; + process.argv[1] = localPath; + mockedRealPathSync.mockReturnValue(localPath); + mockedExecSync.mockImplementation(() => { + throw new Error('Command failed'); + }); + mockedExistsSync.mockImplementation( + (p) => p === path.join(projectRoot, 'bun.lockb'), + ); + + const info = getInstallationInfo(projectRoot, false); + + expect(info.packageManager).toBe(PackageManager.BUN); + expect(info.isGlobal).toBe(false); + }); + + it('should default to local npm installation if no lockfile is found', () => { + const localPath = `${projectRoot}/node_modules/.bin/gemini`; + process.argv[1] = localPath; + mockedRealPathSync.mockReturnValue(localPath); + mockedExecSync.mockImplementation(() => { + throw new Error('Command failed'); + }); + mockedExistsSync.mockReturnValue(false); // No lockfiles + + const info = getInstallationInfo(projectRoot, false); + + expect(info.packageManager).toBe(PackageManager.NPM); + expect(info.isGlobal).toBe(false); + }); + + it('should default to global npm installation for unrecognized paths', () => { + const globalPath = `/usr/local/bin/gemini`; + process.argv[1] = globalPath; + mockedRealPathSync.mockReturnValue(globalPath); + mockedExecSync.mockImplementation(() => { + throw new Error('Command failed'); + }); + + const info = getInstallationInfo(projectRoot, false); + expect(info.packageManager).toBe(PackageManager.NPM); + expect(info.isGlobal).toBe(true); + expect(info.updateCommand).toBe('npm install -g @google/gemini-cli@latest'); + expect(info.updateMessage).toContain('Attempting to automatically update'); + + const infoDisabled = getInstallationInfo(projectRoot, true); + expect(infoDisabled.updateMessage).toContain('Please run npm install'); + }); +}); diff --git a/packages/cli/src/utils/installationInfo.ts b/packages/cli/src/utils/installationInfo.ts new file mode 100644 index 00000000..ca5733d3 --- /dev/null +++ b/packages/cli/src/utils/installationInfo.ts @@ -0,0 +1,177 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { isGitRepository } from '@google/gemini-cli-core'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as childProcess from 'child_process'; + +export enum PackageManager { + NPM = 'npm', + YARN = 'yarn', + PNPM = 'pnpm', + PNPX = 'pnpx', + BUN = 'bun', + BUNX = 'bunx', + HOMEBREW = 'homebrew', + NPX = 'npx', + UNKNOWN = 'unknown', +} + +export interface InstallationInfo { + packageManager: PackageManager; + isGlobal: boolean; + updateCommand?: string; + updateMessage?: string; +} + +export function getInstallationInfo( + projectRoot: string, + isAutoUpdateDisabled: boolean, +): InstallationInfo { + const cliPath = process.argv[1]; + if (!cliPath) { + return { packageManager: PackageManager.UNKNOWN, isGlobal: false }; + } + + try { + // Normalize path separators to forward slashes for consistent matching. + const realPath = fs.realpathSync(cliPath).replace(/\\/g, '/'); + const normalizedProjectRoot = projectRoot?.replace(/\\/g, '/'); + const isGit = isGitRepository(process.cwd()); + + // Check for local git clone first + if ( + isGit && + normalizedProjectRoot && + realPath.startsWith(normalizedProjectRoot) && + !realPath.includes('/node_modules/') + ) { + return { + packageManager: PackageManager.UNKNOWN, // Not managed by a package manager in this sense + isGlobal: false, + updateMessage: + 'Running from a local git clone. Please update with "git pull".', + }; + } + + // Check for npx/pnpx + if (realPath.includes('/.npm/_npx') || realPath.includes('/npm/_npx')) { + return { + packageManager: PackageManager.NPX, + isGlobal: false, + updateMessage: 'Running via npx, update not applicable.', + }; + } + if (realPath.includes('/.pnpm/_pnpx')) { + return { + packageManager: PackageManager.PNPX, + isGlobal: false, + updateMessage: 'Running via pnpx, update not applicable.', + }; + } + + // Check for Homebrew + if (process.platform === 'darwin') { + try { + // The package name in homebrew is gemini-cli + childProcess.execSync('brew list -1 | grep -q "^gemini-cli$"', { + stdio: 'ignore', + }); + return { + packageManager: PackageManager.HOMEBREW, + isGlobal: true, + updateMessage: + 'Installed via Homebrew. Please update with "brew upgrade".', + }; + } catch (_error) { + // Brew is not installed or gemini-cli is not installed via brew. + // Continue to the next check. + } + } + + // Check for pnpm + if (realPath.includes('/.pnpm/global')) { + const updateCommand = 'pnpm add -g @google/gemini-cli@latest'; + return { + packageManager: PackageManager.PNPM, + isGlobal: true, + updateCommand, + updateMessage: isAutoUpdateDisabled + ? `Please run ${updateCommand} to update` + : 'Installed with pnpm. Attempting to automatically update now...', + }; + } + + // Check for yarn + if (realPath.includes('/.yarn/global')) { + const updateCommand = 'yarn global add @google/gemini-cli@latest'; + return { + packageManager: PackageManager.YARN, + isGlobal: true, + updateCommand, + updateMessage: isAutoUpdateDisabled + ? `Please run ${updateCommand} to update` + : 'Installed with yarn. Attempting to automatically update now...', + }; + } + + // Check for bun + if (realPath.includes('/.bun/install/cache')) { + return { + packageManager: PackageManager.BUNX, + isGlobal: false, + updateMessage: 'Running via bunx, update not applicable.', + }; + } + if (realPath.includes('/.bun/bin')) { + const updateCommand = 'bun add -g @google/gemini-cli@latest'; + return { + packageManager: PackageManager.BUN, + isGlobal: true, + updateCommand, + updateMessage: isAutoUpdateDisabled + ? `Please run ${updateCommand} to update` + : 'Installed with bun. Attempting to automatically update now...', + }; + } + + // Check for local install + if ( + normalizedProjectRoot && + realPath.startsWith(`${normalizedProjectRoot}/node_modules`) + ) { + let pm = PackageManager.NPM; + if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) { + pm = PackageManager.YARN; + } else if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) { + pm = PackageManager.PNPM; + } else if (fs.existsSync(path.join(projectRoot, 'bun.lockb'))) { + pm = PackageManager.BUN; + } + return { + packageManager: pm, + isGlobal: false, + updateMessage: + "Locally installed. Please update via your project's package.json.", + }; + } + + // Assume global npm + const updateCommand = 'npm install -g @google/gemini-cli@latest'; + return { + packageManager: PackageManager.NPM, + isGlobal: true, + updateCommand, + updateMessage: isAutoUpdateDisabled + ? `Please run ${updateCommand} to update` + : 'Installed with npm. Attempting to automatically update now...', + }; + } catch (error) { + console.log(error); + return { packageManager: PackageManager.UNKNOWN, isGlobal: false }; + } +} diff --git a/packages/cli/src/utils/updateEventEmitter.ts b/packages/cli/src/utils/updateEventEmitter.ts new file mode 100644 index 00000000..a60ef039 --- /dev/null +++ b/packages/cli/src/utils/updateEventEmitter.ts @@ -0,0 +1,13 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EventEmitter } from 'events'; + +/** + * A shared event emitter for application-wide communication + * between decoupled parts of the CLI. + */ +export const updateEventEmitter = new EventEmitter(); |
