From 112a4195bd0767f04878857a2710180e2ca54dc3 Mon Sep 17 00:00:00 2001 From: mkorwel Date: Fri, 13 Mar 2026 18:07:38 +0000 Subject: [PATCH] fix(cli): resolve build failures and consolidate type guards --- .../cli/src/commands/extensions/uninstall.ts | 2 ++ packages/cli/src/commands/hooks/migrate.ts | 5 +-- packages/cli/src/commands/mcp/add.ts | 10 +++++- .../cli/src/ui/commands/directoryCommand.tsx | 7 ++-- .../cli/src/ui/commands/extensionsCommand.ts | 7 ++-- .../ui/components/MultiFolderTrustDialog.tsx | 12 ++----- .../cli/src/ui/hooks/slashCommandProcessor.ts | 5 +-- .../src/ui/hooks/useHistoryManager.test.ts | 32 +++++++++---------- .../cli/src/ui/hooks/useHistoryManager.ts | 12 +++---- .../cli/src/ui/hooks/useIncludeDirsTrust.tsx | 7 ++-- packages/cli/src/utils/handleAutoUpdate.ts | 5 ++- packages/cli/src/utils/typeGuards.ts | 12 +++++++ 12 files changed, 59 insertions(+), 57 deletions(-) create mode 100644 packages/cli/src/utils/typeGuards.ts diff --git a/packages/cli/src/commands/extensions/uninstall.ts b/packages/cli/src/commands/extensions/uninstall.ts index 5ddcf50dfe..1ecc295d0b 100644 --- a/packages/cli/src/commands/extensions/uninstall.ts +++ b/packages/cli/src/commands/extensions/uninstall.ts @@ -95,6 +95,8 @@ export const uninstallCommand: CommandModule = { let namesArray: string[] = []; if (Array.isArray(names)) { namesArray = names.filter((n): n is string => typeof n === 'string'); + } else if (typeof names === 'string') { + namesArray = [names]; } await handleUninstall({ names: namesArray, diff --git a/packages/cli/src/commands/hooks/migrate.ts b/packages/cli/src/commands/hooks/migrate.ts index 620cc8da42..f600ce9e2e 100644 --- a/packages/cli/src/commands/hooks/migrate.ts +++ b/packages/cli/src/commands/hooks/migrate.ts @@ -11,6 +11,7 @@ import { debugLogger, getErrorMessage } from '@google/gemini-cli-core'; import { loadSettings, SettingScope } from '../../config/settings.js'; import { exitCli } from '../utils.js'; import stripJsonComments from 'strip-json-comments'; +import { isRecord } from '../../utils/typeGuards.js'; /** * Mapping from Claude Code event names to Gemini event names @@ -58,10 +59,6 @@ function transformMatcher(matcher: string | undefined): string | undefined { return transformed; } -function isRecord(obj: unknown): obj is Record { - return typeof obj === 'object' && obj !== null && !Array.isArray(obj); -} - /** * Migrate a Claude Code hook configuration to Gemini format */ diff --git a/packages/cli/src/commands/mcp/add.ts b/packages/cli/src/commands/mcp/add.ts index 89ae6668d7..0cfac82bca 100644 --- a/packages/cli/src/commands/mcp/add.ts +++ b/packages/cli/src/commands/mcp/add.ts @@ -213,7 +213,15 @@ export const addCommand: CommandModule = { // Handle -- separator args as server args if present if (argv['--'] && Array.isArray(argv['--'])) { const args = argv['args']; - const existingArgs = Array.isArray(args) ? args : []; + let existingArgs: Array = []; + if (Array.isArray(args)) { + existingArgs = args.filter( + (a): a is string | number => + typeof a === 'string' || typeof a === 'number', + ); + } else if (typeof args === 'string' || typeof args === 'number') { + existingArgs = [args]; + } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment argv['args'] = [...existingArgs, ...argv['--']]; } diff --git a/packages/cli/src/ui/commands/directoryCommand.tsx b/packages/cli/src/ui/commands/directoryCommand.tsx index 718012c494..a54af7684e 100644 --- a/packages/cli/src/ui/commands/directoryCommand.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.tsx @@ -14,7 +14,7 @@ import { type SlashCommand, type CommandContext, } from './types.js'; -import { MessageType, type HistoryItem } from '../types.js'; +import { MessageType, type HistoryItemWithoutId } from '../types.js'; import { refreshServerHierarchicalMemory, type Config, @@ -29,10 +29,7 @@ import * as fs from 'node:fs'; async function finishAddingDirectories( config: Config, - addItem: ( - itemData: Omit, - baseTimestamp?: number, - ) => number, + addItem: (itemData: HistoryItemWithoutId, baseTimestamp?: number) => number, added: string[], errors: string[], ) { diff --git a/packages/cli/src/ui/commands/extensionsCommand.ts b/packages/cli/src/ui/commands/extensionsCommand.ts index 6c0f3529a2..6da8784b35 100644 --- a/packages/cli/src/ui/commands/extensionsCommand.ts +++ b/packages/cli/src/ui/commands/extensionsCommand.ts @@ -209,8 +209,8 @@ async function restartAction( const s = extensionsToRestart.length > 1 ? 's' : ''; - const reloadingMessage = { - type: MessageType.INFO, + const reloadingMessage: HistoryItemInfo = { + type: 'info', text: `Reloading ${extensionsToRestart.length} extension${s}...`, color: theme.text.primary, }; @@ -472,8 +472,7 @@ async function installAction( args: string, requestConsentOverride?: (consent: string) => Promise, ) { - const extensionLoader = - context.services.agentContext?.config.getExtensionLoader(); + const extensionLoader = context.services.config?.getExtensionLoader(); if (!(extensionLoader instanceof ExtensionManager)) { debugLogger.error( `Cannot ${context.invocation?.name} extensions in this environment`, diff --git a/packages/cli/src/ui/components/MultiFolderTrustDialog.tsx b/packages/cli/src/ui/components/MultiFolderTrustDialog.tsx index deab70c0ce..ee5d0d613d 100644 --- a/packages/cli/src/ui/components/MultiFolderTrustDialog.tsx +++ b/packages/cli/src/ui/components/MultiFolderTrustDialog.tsx @@ -16,7 +16,7 @@ import { useKeypress } from '../hooks/useKeypress.js'; import { loadTrustedFolders, TrustLevel } from '../../config/trustedFolders.js'; import { expandHomeDir } from '../utils/directoryUtils.js'; import * as path from 'node:path'; -import { MessageType, type HistoryItem } from '../types.js'; +import { MessageType, type HistoryItemWithoutId } from '../types.js'; import { type Config } from '@google/gemini-cli-core'; export enum MultiFolderTrustChoice { @@ -32,18 +32,12 @@ export interface MultiFolderTrustDialogProps { errors: string[]; finishAddingDirectories: ( config: Config, - addItem: ( - itemData: Omit, - baseTimestamp?: number, - ) => number, + addItem: (itemData: HistoryItemWithoutId, baseTimestamp?: number) => number, added: string[], errors: string[], ) => Promise; config: Config; - addItem: ( - itemData: Omit, - baseTimestamp?: number, - ) => number; + addItem: (itemData: HistoryItemWithoutId, baseTimestamp?: number) => number; } export const MultiFolderTrustDialog: React.FC = ({ diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 3a1f0b4616..b4e10de0aa 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -63,10 +63,7 @@ import { LogoutChoice, } from '../components/LogoutConfirmationDialog.js'; import { runExitCleanup } from '../../utils/cleanup.js'; - -function isRecord(obj: unknown): obj is Record { - return typeof obj === 'object' && obj !== null && !Array.isArray(obj); -} +import { isRecord } from '../../utils/typeGuards.js'; interface SlashCommandProcessorActions { openAuthDialog: () => void; diff --git a/packages/cli/src/ui/hooks/useHistoryManager.test.ts b/packages/cli/src/ui/hooks/useHistoryManager.test.ts index 158d30e7a6..7753153cda 100644 --- a/packages/cli/src/ui/hooks/useHistoryManager.test.ts +++ b/packages/cli/src/ui/hooks/useHistoryManager.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect } from 'vitest'; import { act } from 'react'; import { renderHook } from '../../test-utils/render.js'; import { useHistory } from './useHistoryManager.js'; -import type { HistoryItem } from '../types.js'; +import type { HistoryItem, HistoryItemWithoutId } from '../types.js'; describe('useHistoryManager', () => { it('should initialize with an empty history', async () => { @@ -19,7 +19,7 @@ describe('useHistoryManager', () => { it('should add an item to history with a unique ID', async () => { const { result } = await renderHook(() => useHistory()); const timestamp = Date.now(); - const itemData: Omit = { + const itemData: HistoryItemWithoutId = { type: 'user', // Replaced HistoryItemType.User text: 'Hello', }; @@ -92,11 +92,11 @@ describe('useHistoryManager', () => { it('should generate unique IDs for items added with the same base timestamp', async () => { const { result } = await renderHook(() => useHistory()); const timestamp = Date.now(); - const itemData1: Omit = { + const itemData1: HistoryItemWithoutId = { type: 'user', // Replaced HistoryItemType.User text: 'First', }; - const itemData2: Omit = { + const itemData2: HistoryItemWithoutId = { type: 'gemini', // Replaced HistoryItemType.Gemini text: 'Second', }; @@ -120,7 +120,7 @@ describe('useHistoryManager', () => { it('should update an existing history item', async () => { const { result } = await renderHook(() => useHistory()); const timestamp = Date.now(); - const initialItem: Omit = { + const initialItem: HistoryItemWithoutId = { type: 'gemini', // Replaced HistoryItemType.Gemini text: 'Initial content', }; @@ -146,7 +146,7 @@ describe('useHistoryManager', () => { it('should not change history if updateHistoryItem is called with a nonexistent ID', async () => { const { result } = await renderHook(() => useHistory()); const timestamp = Date.now(); - const itemData: Omit = { + const itemData: HistoryItemWithoutId = { type: 'user', // Replaced HistoryItemType.User text: 'Hello', }; @@ -167,11 +167,11 @@ describe('useHistoryManager', () => { it('should clear the history', async () => { const { result } = await renderHook(() => useHistory()); const timestamp = Date.now(); - const itemData1: Omit = { + const itemData1: HistoryItemWithoutId = { type: 'user', // Replaced HistoryItemType.User text: 'First', }; - const itemData2: Omit = { + const itemData2: HistoryItemWithoutId = { type: 'gemini', // Replaced HistoryItemType.Gemini text: 'Second', }; @@ -193,19 +193,19 @@ describe('useHistoryManager', () => { it('should not add consecutive duplicate user messages', async () => { const { result } = await renderHook(() => useHistory()); const timestamp = Date.now(); - const itemData1: Omit = { + const itemData1: HistoryItemWithoutId = { type: 'user', // Replaced HistoryItemType.User text: 'Duplicate message', }; - const itemData2: Omit = { + const itemData2: HistoryItemWithoutId = { type: 'user', // Replaced HistoryItemType.User text: 'Duplicate message', }; - const itemData3: Omit = { + const itemData3: HistoryItemWithoutId = { type: 'gemini', // Replaced HistoryItemType.Gemini text: 'Gemini response', }; - const itemData4: Omit = { + const itemData4: HistoryItemWithoutId = { type: 'user', // Replaced HistoryItemType.User text: 'Another user message', }; @@ -226,15 +226,15 @@ describe('useHistoryManager', () => { it('should add duplicate user messages if they are not consecutive', async () => { const { result } = await renderHook(() => useHistory()); const timestamp = Date.now(); - const itemData1: Omit = { + const itemData1: HistoryItemWithoutId = { type: 'user', // Replaced HistoryItemType.User text: 'Message 1', }; - const itemData2: Omit = { + const itemData2: HistoryItemWithoutId = { type: 'gemini', // Replaced HistoryItemType.Gemini text: 'Gemini response', }; - const itemData3: Omit = { + const itemData3: HistoryItemWithoutId = { type: 'user', // Replaced HistoryItemType.User text: 'Message 1', // Duplicate text, but not consecutive }; @@ -254,7 +254,7 @@ describe('useHistoryManager', () => { it('should use Date.now() as default baseTimestamp if not provided', async () => { const { result } = await renderHook(() => useHistory()); const before = Date.now(); - const itemData: Omit = { + const itemData: HistoryItemWithoutId = { type: 'user', text: 'Default timestamp test', }; diff --git a/packages/cli/src/ui/hooks/useHistoryManager.ts b/packages/cli/src/ui/hooks/useHistoryManager.ts index c6ceabb920..8c8701ff5f 100644 --- a/packages/cli/src/ui/hooks/useHistoryManager.ts +++ b/packages/cli/src/ui/hooks/useHistoryManager.ts @@ -5,24 +5,24 @@ */ import { useState, useRef, useCallback, useMemo } from 'react'; -import type { HistoryItem } from '../types.js'; +import type { HistoryItem, HistoryItemWithoutId } from '../types.js'; import type { ChatRecordingService } from '@google/gemini-cli-core/src/services/chatRecordingService.js'; // Type for the updater function passed to updateHistoryItem type HistoryItemUpdater = ( prevItem: HistoryItem, -) => Partial>; +) => Partial; export interface UseHistoryManagerReturn { history: HistoryItem[]; addItem: ( - itemData: Omit, + itemData: HistoryItemWithoutId, baseTimestamp?: number, isResuming?: boolean, ) => number; // Returns the generated ID updateItem: ( id: number, - updates: Partial> | HistoryItemUpdater, + updates: Partial | HistoryItemUpdater, ) => void; clearItems: () => void; loadHistory: (newHistory: HistoryItem[]) => void; @@ -63,7 +63,7 @@ export function useHistory({ // Adds a new item to the history state with a unique ID. const addItem = useCallback( ( - itemData: Omit, + itemData: HistoryItemWithoutId, baseTimestamp: number = Date.now(), isResuming: boolean = false, ): number => { @@ -138,7 +138,7 @@ export function useHistory({ const updateItem = useCallback( ( id: number, - updates: Partial> | HistoryItemUpdater, + updates: Partial | HistoryItemUpdater, ) => { setHistory((prevHistory) => prevHistory.map((item) => { diff --git a/packages/cli/src/ui/hooks/useIncludeDirsTrust.tsx b/packages/cli/src/ui/hooks/useIncludeDirsTrust.tsx index ec29a8180c..24ff1b10e6 100644 --- a/packages/cli/src/ui/hooks/useIncludeDirsTrust.tsx +++ b/packages/cli/src/ui/hooks/useIncludeDirsTrust.tsx @@ -14,14 +14,11 @@ import { } from '@google/gemini-cli-core'; import { MultiFolderTrustDialog } from '../components/MultiFolderTrustDialog.js'; import type { UseHistoryManagerReturn } from './useHistoryManager.js'; -import { MessageType, type HistoryItem } from '../types.js'; +import { MessageType, type HistoryItemWithoutId } from '../types.js'; async function finishAddingDirectories( config: Config, - addItem: ( - itemData: Omit, - baseTimestamp?: number, - ) => number, + addItem: (itemData: HistoryItemWithoutId, baseTimestamp?: number) => number, added: string[], errors: string[], ) { diff --git a/packages/cli/src/utils/handleAutoUpdate.ts b/packages/cli/src/utils/handleAutoUpdate.ts index 4f8ca69ed3..5b7ecdff47 100644 --- a/packages/cli/src/utils/handleAutoUpdate.ts +++ b/packages/cli/src/utils/handleAutoUpdate.ts @@ -8,7 +8,7 @@ import type { UpdateObject } from '../ui/utils/updateCheck.js'; import type { LoadedSettings } from '../config/settings.js'; import { getInstallationInfo, PackageManager } from './installationInfo.js'; import { updateEventEmitter } from './updateEventEmitter.js'; -import { MessageType, type HistoryItem } from '../ui/types.js'; +import { MessageType, type HistoryItemWithoutId } from '../ui/types.js'; import { spawnWrapper } from './spawnWrapper.js'; import type { spawn } from 'node:child_process'; import { debugLogger } from '@google/gemini-cli-core'; @@ -115,7 +115,6 @@ export function handleAutoUpdate( } updateEventEmitter.emit('update-received', { ...info, - message: combinedMessage, isUpdating: true, }); if (_updateInProgress) { @@ -163,7 +162,7 @@ export function handleAutoUpdate( } export function setUpdateHandler( - addItem: (item: Omit, timestamp: number) => void, + addItem: (item: HistoryItemWithoutId, timestamp: number) => void, setUpdateInfo: (info: UpdateObject | null) => void, ) { let successfullyInstalled = false; diff --git a/packages/cli/src/utils/typeGuards.ts b/packages/cli/src/utils/typeGuards.ts new file mode 100644 index 0000000000..87155d6942 --- /dev/null +++ b/packages/cli/src/utils/typeGuards.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Type guard to check if a value is a non-null object and not an array. + */ +export function isRecord(obj: unknown): obj is Record { + return typeof obj === 'object' && obj !== null && !Array.isArray(obj); +}