diff --git a/eslint.config.js b/eslint.config.js index 6c82a2bd98..12f596bfbe 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -333,6 +333,18 @@ export default tseslint.config( // Expensive heuristic — NLP analysis on comments with no value for this codebase 'sonarjs/no-commented-code': 'off', + // False-positive or redundant sonarjs rules — see project-plans/issue1569b/README.md + 'sonarjs/declarations-in-global-scope': 'off', // ESM false positives + 'sonarjs/no-unused-vars': 'off', // Redundant with @typescript-eslint/no-unused-vars + 'sonarjs/max-lines-per-function': 'off', // Redundant with base max-lines-per-function at 80 + 'sonarjs/process-argv': 'off', // CLI tool legitimately uses process.argv + 'sonarjs/standard-input': 'off', // CLI tool legitimately uses stdin + 'sonarjs/publicly-writable-directories': 'off', // Test /tmp usage is intentional + 'sonarjs/pseudo-random': 'off', // Non-cryptographic context (IDs, shuffles) + 'sonarjs/no-reference-error': 'off', // TypeScript ambient/global false positives + 'sonarjs/no-undefined-assignment': 'off', // TypeScript undefined convention + 'sonarjs/arrow-function-convention': 'off', // Conflicts with Prettier arrowParens: "always" default + // ESLint comments (recommended rules downgraded to warn) ...Object.fromEntries( Object.entries(eslintComments.configs.recommended.rules ?? {}).map( @@ -454,6 +466,7 @@ export default tseslint.config( // Relax complexity rules for test files 'max-lines-per-function': 'off', + 'sonarjs/no-duplicate-string': 'off', // Tests repeat strings legitimately }, }, // Issue #1576: Enforce strict line-limit errors on AppContainer module files. diff --git a/packages/a2a-server/src/agent/executor.ts b/packages/a2a-server/src/agent/executor.ts index 6965ac46ac..653d50bada 100644 --- a/packages/a2a-server/src/agent/executor.ts +++ b/packages/a2a-server/src/agent/executor.ts @@ -101,10 +101,10 @@ export class CoderAgentExecutor implements AgentExecutor { sdkTask: SDKTask, eventBus?: ExecutionEventBus, ): Promise { - const metadata = sdkTask.metadata || {}; + const metadata = sdkTask.metadata ?? {}; const persistedState = getPersistedState(metadata); - if (!persistedState) { + if (persistedState == null) { throw new Error( `Cannot reconstruct task ${sdkTask.id}: missing persisted state in metadata.`, ); @@ -123,7 +123,7 @@ export class CoderAgentExecutor implements AgentExecutor { runtimeTask.taskState = persistedState._taskState; const contentGeneratorConfig = runtimeTask.config.getContentGeneratorConfig(); - if (contentGeneratorConfig) { + if (contentGeneratorConfig != null) { await runtimeTask.geminiClient.initialize(contentGeneratorConfig); } @@ -139,7 +139,7 @@ export class CoderAgentExecutor implements AgentExecutor { agentSettingsInput?: AgentSettings, eventBus?: ExecutionEventBus, ): Promise { - const agentSettings = agentSettingsInput || ({} as AgentSettings); + const agentSettings = agentSettingsInput ?? ({} as AgentSettings); const config = await this.getConfig(agentSettings, taskId); const runtimeTask = await Task.create( taskId, @@ -150,7 +150,7 @@ export class CoderAgentExecutor implements AgentExecutor { ); const contentGeneratorConfig2 = runtimeTask.config.getContentGeneratorConfig(); - if (contentGeneratorConfig2) { + if (contentGeneratorConfig2 != null) { await runtimeTask.geminiClient.initialize(contentGeneratorConfig2); } @@ -177,13 +177,12 @@ export class CoderAgentExecutor implements AgentExecutor { ); const wrapper = this.tasks.get(taskId); - if (!wrapper) { + if (wrapper == null) { logger.warn( `[CoderAgentExecutor] Task ${taskId} not found for cancellation.`, ); eventBus.publish({ kind: 'status-update', - taskId, contextId: uuidv4(), status: { state: 'failed', @@ -196,6 +195,7 @@ export class CoderAgentExecutor implements AgentExecutor { }, }, final: true, + taskId, }); return; } @@ -208,7 +208,6 @@ export class CoderAgentExecutor implements AgentExecutor { ); eventBus.publish({ kind: 'status-update', - taskId, contextId: task.contextId, status: { state: task.taskState, @@ -226,6 +225,7 @@ export class CoderAgentExecutor implements AgentExecutor { }, }, final: true, + taskId, }); return; } @@ -260,7 +260,6 @@ export class CoderAgentExecutor implements AgentExecutor { ); eventBus.publish({ kind: 'status-update', - taskId, contextId: task.contextId, status: { state: 'failed', @@ -278,6 +277,7 @@ export class CoderAgentExecutor implements AgentExecutor { }, }, final: true, + taskId, }); } }; @@ -289,11 +289,11 @@ export class CoderAgentExecutor implements AgentExecutor { const userMessage = requestContext.userMessage; const sdkTask = requestContext.task; - const taskId = sdkTask?.id || userMessage.taskId || uuidv4(); + const taskId = sdkTask?.id ?? userMessage.taskId ?? uuidv4(); const contextId = - userMessage.contextId || - sdkTask?.contextId || - sdkTask?.metadata?.['_contextId'] || + userMessage.contextId ?? + sdkTask?.contextId ?? + (sdkTask?.metadata?.['_contextId'] as string | undefined) ?? uuidv4(); logger.info( @@ -307,7 +307,7 @@ export class CoderAgentExecutor implements AgentExecutor { ); const store = requestStorage.getStore(); - if (!store) { + if (store == null) { logger.error( '[CoderAgentExecutor] Could not get request from async local storage. Cancellation on socket close will not be handled for this request.', ); @@ -316,7 +316,7 @@ export class CoderAgentExecutor implements AgentExecutor { const abortController = new AbortController(); const abortSignal = abortController.signal; - if (store) { + if (store != null) { // Grab the raw socket from the request object const socket = store.req.socket; const onClientEnd = () => { @@ -344,10 +344,10 @@ export class CoderAgentExecutor implements AgentExecutor { let wrapper: TaskWrapper | undefined = this.tasks.get(taskId); - if (wrapper) { + if (wrapper != null) { wrapper.task.eventBus = eventBus; logger.info(`[CoderAgentExecutor] Task ${taskId} found in memory cache.`); - } else if (sdkTask) { + } else if (sdkTask != null) { logger.info( `[CoderAgentExecutor] Task ${taskId} found in TaskStore. Reconstructing...`, ); @@ -363,7 +363,6 @@ export class CoderAgentExecutor implements AgentExecutor { }; eventBus.publish({ kind: 'status-update', - taskId, contextId: sdkTask.contextId, status: { state: 'failed', @@ -377,12 +376,13 @@ export class CoderAgentExecutor implements AgentExecutor { }, ], messageId: uuidv4(), - taskId, contextId: sdkTask.contextId, + taskId, } as Message, }, final: true, metadata: { coderAgent: stateChange }, + taskId, }); return; } @@ -393,7 +393,7 @@ export class CoderAgentExecutor implements AgentExecutor { ] as AgentSettings; wrapper = await this.createTask( taskId, - contextId as string, + contextId, agentSettings, eventBus, ); @@ -415,13 +415,6 @@ export class CoderAgentExecutor implements AgentExecutor { } } - if (!wrapper) { - logger.error( - `[CoderAgentExecutor] Task ${taskId} is unexpectedly undefined after load/create.`, - ); - return; - } - const currentTask = wrapper.task; if (['canceled', 'failed', 'completed'].includes(currentTask.taskState)) { diff --git a/packages/a2a-server/src/agent/task.test.ts b/packages/a2a-server/src/agent/task.test.ts index 57c290831a..b129dd04e7 100644 --- a/packages/a2a-server/src/agent/task.test.ts +++ b/packages/a2a-server/src/agent/task.test.ts @@ -73,7 +73,7 @@ describe('Task', () => { await task.scheduleToolCalls(requests, abortController.signal); - expect(requests).toEqual(originalRequests); + expect(requests).toStrictEqual(originalRequests); }); describe('acceptAgentMessage', () => { @@ -145,7 +145,7 @@ describe('Task', () => { // Access private field for testing // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((task as any).modelInfo).toEqual({ + expect((task as any).modelInfo).toStrictEqual({ model: 'gemini-2.0-flash-exp', }); }); @@ -286,7 +286,7 @@ describe('Task', () => { // Should have the latest modelInfo // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((task as any).modelInfo).toEqual({ + expect((task as any).modelInfo).toStrictEqual({ model: 'gemini-2.0-flash-exp', }); }); diff --git a/packages/a2a-server/src/agent/task.ts b/packages/a2a-server/src/agent/task.ts index c61977259f..b0b693a3d9 100644 --- a/packages/a2a-server/src/agent/task.ts +++ b/packages/a2a-server/src/agent/task.ts @@ -5,7 +5,7 @@ */ import { - CoreToolScheduler, + type CoreToolScheduler, GeminiClient, GeminiEventType, ToolConfirmationOutcome, @@ -37,7 +37,7 @@ import type { ModelInfo, } from '@vybestack/llxprt-code-core'; import type { RequestContext } from '@a2a-js/sdk/server'; -import { type ExecutionEventBus } from '@a2a-js/sdk/server'; +import type { ExecutionEventBus } from '@a2a-js/sdk/server'; import type { TaskStatusUpdateEvent, TaskArtifactUpdateEvent, @@ -185,7 +185,10 @@ export class Task { this.toolCompletionNotifier = { resolve, reject }; }); // If there are no pending calls when reset, resolve immediately. - if (this.pendingToolCalls.size === 0 && this.toolCompletionNotifier) { + if ( + this.pendingToolCalls.size === 0 && + this.toolCompletionNotifier != null + ) { this.toolCompletionNotifier.resolve(); } } @@ -207,7 +210,10 @@ export class Task { logger.info( `[Task] Resolved tool call: ${toolCallId}. Pending: ${this.pendingToolCalls.size}`, ); - if (this.pendingToolCalls.size === 0 && this.toolCompletionNotifier) { + if ( + this.pendingToolCalls.size === 0 && + this.toolCompletionNotifier != null + ) { this.toolCompletionNotifier.resolve(); } } @@ -229,7 +235,7 @@ export class Task { `[Task] Cancelling all ${this.pendingToolCalls.size} pending tool calls. Reason: ${reason}`, ); } - if (this.toolCompletionNotifier) { + if (this.toolCompletionNotifier != null) { this.toolCompletionNotifier.reject(new Error(reason)); } this.pendingToolCalls.clear(); @@ -243,11 +249,11 @@ export class Task { ): Message { return { kind: 'message', - role, parts: [{ kind: 'text', text }], messageId: uuidv4(), taskId: this.id, contextId: this.contextId, + role, }; } @@ -286,8 +292,8 @@ export class Task { contextId: this.contextId, status: { state: stateToReport, - message, // Shorthand property timestamp: timestamp || new Date().toISOString(), + message, }, final, metadata, @@ -308,7 +314,7 @@ export class Task { if (messageText) { message = this._createTextMessage(messageText); - } else if (messageParts) { + } else if (messageParts != null) { message = { kind: 'message', role: 'agent', @@ -361,9 +367,9 @@ export class Task { kind: 'artifact-update', taskId: this.id, contextId: this.contextId, - artifact, append: true, lastChunk: false, + artifact, }; this.eventBus?.publish(artifactEvent); } @@ -549,7 +555,7 @@ export class Task { 'response', ); - if (tc.tool) { + if (tc.tool != null) { serializableToolCall.tool = this._pickFields( tc.tool, 'name', @@ -712,7 +718,7 @@ export class Task { const request = updatedRequests.find( (req) => req.callId === callId, ); - if (request) { + if (request != null) { request.checkpoint = checkpointPath; logger.info( `[Task] Checkpoint created for callId ${callId}: ${checkpointPath}`, @@ -734,7 +740,7 @@ export class Task { } } - if (!this.scheduler) { + if (this.scheduler == null) { throw new Error('Scheduler not initialized'); } await this.scheduler.schedule(updatedRequests, abortSignal); @@ -875,7 +881,7 @@ export class Task { const confirmationDetails = this.pendingToolConfirmationDetails.get(callId); - if (!confirmationDetails) { + if (confirmationDetails == null) { logger.warn( `[Task] Received tool confirmation for unknown or already processed callId: ${callId}`, ); diff --git a/packages/a2a-server/src/commands/extensions.test.ts b/packages/a2a-server/src/commands/extensions.test.ts index 11d76109a9..29a1468866 100644 --- a/packages/a2a-server/src/commands/extensions.test.ts +++ b/packages/a2a-server/src/commands/extensions.test.ts @@ -22,12 +22,12 @@ vi.mock('@vybestack/llxprt-code-core', async (importOriginal) => { describe('ExtensionsCommand', () => { it('should have the correct name', () => { const command = new ExtensionsCommand(); - expect(command.name).toEqual('extensions'); + expect(command.name).toStrictEqual('extensions'); }); it('should have the correct description', () => { const command = new ExtensionsCommand(); - expect(command.description).toEqual('Manage extensions.'); + expect(command.description).toStrictEqual('Manage extensions.'); }); it('should have "extensions list" as a subcommand', () => { @@ -48,7 +48,10 @@ describe('ExtensionsCommand', () => { const result = await command.execute({ config: mockConfig }, []); - expect(result).toEqual({ name: 'extensions list', data: mockExtensions }); + expect(result).toStrictEqual({ + name: 'extensions list', + data: mockExtensions, + }); expect(mockListExtensions).toHaveBeenCalledWith(mockConfig); }); }); @@ -56,7 +59,7 @@ describe('ExtensionsCommand', () => { describe('ListExtensionsCommand', () => { it('should have the correct name', () => { const command = new ListExtensionsCommand(); - expect(command.name).toEqual('extensions list'); + expect(command.name).toStrictEqual('extensions list'); }); it('should call listExtensions with the provided config', async () => { @@ -67,7 +70,10 @@ describe('ListExtensionsCommand', () => { const result = await command.execute({ config: mockConfig }, []); - expect(result).toEqual({ name: 'extensions list', data: mockExtensions }); + expect(result).toStrictEqual({ + name: 'extensions list', + data: mockExtensions, + }); expect(mockListExtensions).toHaveBeenCalledWith(mockConfig); }); @@ -78,7 +84,7 @@ describe('ListExtensionsCommand', () => { const result = await command.execute({ config: mockConfig }, []); - expect(result).toEqual({ + expect(result).toStrictEqual({ name: 'extensions list', data: 'No extensions installed.', }); diff --git a/packages/a2a-server/src/commands/extensions.ts b/packages/a2a-server/src/commands/extensions.ts index aa59300a84..718124bba3 100644 --- a/packages/a2a-server/src/commands/extensions.ts +++ b/packages/a2a-server/src/commands/extensions.ts @@ -34,7 +34,8 @@ export class ListExtensionsCommand implements Command { _: string[], ): Promise { const extensions = listExtensions(context.config); - const data = extensions.length ? extensions : 'No extensions installed.'; + const data = + extensions.length > 0 ? extensions : 'No extensions installed.'; return { name: this.name, data }; } diff --git a/packages/a2a-server/src/commands/init.ts b/packages/a2a-server/src/commands/init.ts index 5c4020f746..9256f1e250 100644 --- a/packages/a2a-server/src/commands/init.ts +++ b/packages/a2a-server/src/commands/init.ts @@ -96,8 +96,6 @@ Write the complete content to the \`LLXPRT.md\` file. The output must be well-fo const event: AgentExecutionEvent = { kind: 'status-update', - taskId, - contextId, status: { state: statusState, message: { @@ -115,6 +113,8 @@ Write the complete content to the \`LLXPRT.md\` file. The output must be well-fo coderAgent: { kind: eventType }, model: context.config.getModel(), }, + taskId, + contextId, }; logger.info('[EventBus event]: ', event); @@ -135,7 +135,7 @@ Write the complete content to the \`LLXPRT.md\` file. The output must be well-fo ): Promise { fs.writeFileSync(llxprtMdPath, '', 'utf8'); - if (!context.agentExecutor) { + if (context.agentExecutor == null) { throw new Error('Agent executor not found in context.'); } const agentExecutor = context.agentExecutor as CoderAgentExecutor; @@ -149,8 +149,8 @@ Write the complete content to the \`LLXPRT.md\` file. The output must be well-fo const agentSettings: AgentSettings = { kind: CoderAgentEvent.StateAgentSettingsEvent, - workspacePath, autoExecute: true, + workspacePath, }; if (typeof result.content !== 'string') { @@ -164,11 +164,11 @@ Write the complete content to the \`LLXPRT.md\` file. The output must be well-fo role: 'user', parts: [{ kind: 'text', text: promptText }], messageId: uuidv4(), - taskId, - contextId, metadata: { coderAgent: agentSettings, }, + taskId, + contextId, }, taskId, contextId, @@ -185,7 +185,7 @@ Write the complete content to the \`LLXPRT.md\` file. The output must be well-fo context: CommandContext, _args: string[] = [], ): Promise { - if (!context.eventBus) { + if (context.eventBus == null) { return { name: this.name, data: 'Use executeStream to get streaming results.', diff --git a/packages/a2a-server/src/commands/restore.test.ts b/packages/a2a-server/src/commands/restore.test.ts index 2c0418e64c..f0aa133911 100644 --- a/packages/a2a-server/src/commands/restore.test.ts +++ b/packages/a2a-server/src/commands/restore.test.ts @@ -54,7 +54,7 @@ describe('ListCheckpointsCommand', () => { it('should have the correct name', () => { const command = new ListCheckpointsCommand(); - expect(command.name).toEqual('restore list'); + expect(command.name).toStrictEqual('restore list'); }); it('should return error when checkpointing is disabled', async () => { @@ -63,7 +63,7 @@ describe('ListCheckpointsCommand', () => { const result = await command.execute(context, []); - expect(result).toEqual({ + expect(result).toStrictEqual({ name: 'restore list', data: { error: 'Checkpointing is not enabled' }, }); @@ -77,7 +77,7 @@ describe('ListCheckpointsCommand', () => { const result = await command.execute(context, []); - expect(result).toEqual({ + expect(result).toStrictEqual({ name: 'restore list', data: 'No checkpoints found.', }); @@ -100,7 +100,7 @@ describe('ListCheckpointsCommand', () => { 'checkpoint1.json', 'checkpoint2.json', ]); - expect(result).toEqual({ + expect(result).toStrictEqual({ name: 'restore list', data: 'checkpoint1\ncheckpoint2', }); @@ -132,7 +132,7 @@ describe('RestoreCommand', () => { it('should have the correct name', () => { const command = new RestoreCommand(); - expect(command.name).toEqual('restore'); + expect(command.name).toStrictEqual('restore'); }); it('should require workspace', () => { @@ -147,7 +147,7 @@ describe('RestoreCommand', () => { it('should have ListCheckpointsCommand as a subcommand', () => { const command = new RestoreCommand(); - expect(command.subCommands?.map((c) => c.name)).toContain('restore list'); + expect(command.subCommands.map((c) => c.name)).toContain('restore list'); }); it('should return error when no args provided', async () => { @@ -155,7 +155,7 @@ describe('RestoreCommand', () => { const result = await command.execute(context, []); - expect(result.name).toEqual('restore'); + expect(result.name).toStrictEqual('restore'); expect(result.data).toHaveProperty('error'); }); @@ -164,7 +164,7 @@ describe('RestoreCommand', () => { const result = await command.execute(context, ['../../../etc/passwd']); - expect(result.name).toEqual('restore'); + expect(result.name).toStrictEqual('restore'); expect(result.data).toHaveProperty('error'); expect((result.data as { error?: string }).error).toContain('traversal'); }); @@ -174,7 +174,7 @@ describe('RestoreCommand', () => { const result = await command.execute(context, ['subdir/name.json']); - expect(result.name).toEqual('restore'); + expect(result.name).toStrictEqual('restore'); expect(result.data).toHaveProperty('error'); expect((result.data as { error?: string }).error).toContain('traversal'); }); @@ -185,7 +185,7 @@ describe('RestoreCommand', () => { const result = await command.execute(context, ['nonexistent.json']); - expect(result.name).toEqual('restore'); + expect(result.name).toStrictEqual('restore'); expect(result.data).toHaveProperty('error'); }); @@ -197,7 +197,7 @@ describe('RestoreCommand', () => { const result = await command.execute(context, ['symlink.json']); - expect(result.name).toEqual('restore'); + expect(result.name).toStrictEqual('restore'); expect(result.data).toHaveProperty('error'); expect((result.data as { error?: string }).error).toContain('symlink'); }); @@ -218,7 +218,7 @@ describe('RestoreCommand', () => { const result = await command.execute(context, ['invalid.json']); - expect(result.name).toEqual('restore'); + expect(result.name).toStrictEqual('restore'); expect(result.data).toHaveProperty('error'); }); @@ -245,7 +245,7 @@ describe('RestoreCommand', () => { const result = await command.execute(context, ['valid.json']); expect(mockGit.restoreProjectFromSnapshot).toHaveBeenCalledWith('abc123'); - expect(result).toEqual({ + expect(result).toStrictEqual({ name: 'restore', data: { toolCall: validData.toolCall, @@ -277,7 +277,7 @@ describe('RestoreCommand', () => { const result = await command.execute(contextNoGit, ['valid.json']); - expect(result.name).toEqual('restore'); + expect(result.name).toStrictEqual('restore'); expect(result.data).toHaveProperty('error'); expect((result.data as { error?: string }).error).toContain('Git'); }); @@ -304,7 +304,7 @@ describe('RestoreCommand', () => { const result = await command.execute(contextNoGit, ['valid.json']); - expect(result).toEqual({ + expect(result).toStrictEqual({ name: 'restore', data: { toolCall: validData.toolCall, diff --git a/packages/a2a-server/src/commands/restore.ts b/packages/a2a-server/src/commands/restore.ts index 2863522709..56737b3e83 100644 --- a/packages/a2a-server/src/commands/restore.ts +++ b/packages/a2a-server/src/commands/restore.ts @@ -112,7 +112,7 @@ export class RestoreCommand implements Command { // Restore from snapshot if commitHash exists if (validatedData.commitHash) { - if (!context.git) { + if (context.git == null) { return { name: this.name, data: { diff --git a/packages/a2a-server/src/config/config.ts b/packages/a2a-server/src/config/config.ts index faffd43fd6..91204cc4bf 100644 --- a/packages/a2a-server/src/config/config.ts +++ b/packages/a2a-server/src/config/config.ts @@ -41,18 +41,17 @@ export async function loadConfig( sessionId: taskId, model: DEFAULT_GEMINI_MODEL, embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, - sandbox: undefined, // Sandbox might not be relevant for a server-side agent - targetDir: workspaceDir, // Or a specific directory the agent operates on + sandbox: undefined, + targetDir: workspaceDir, debugMode: process.env['DEBUG'] === 'true' || false, - question: '', // Not used in server mode directly like CLI - coreTools: settings.coreTools || undefined, - excludeTools: settings.excludeTools || undefined, - showMemoryUsage: settings.showMemoryUsage || false, + question: '', + coreTools: settings.coreTools ?? undefined, + excludeTools: settings.excludeTools ?? undefined, + showMemoryUsage: (settings.showMemoryUsage ?? false) || false, approvalMode: process.env['GEMINI_YOLO_MODE'] === 'true' ? ApprovalMode.YOLO : ApprovalMode.DEFAULT, - mcpServers, cwd: workspaceDir, telemetry: { enabled: settings.telemetry?.enabled, @@ -62,7 +61,6 @@ export async function loadConfig( settings.telemetry?.otlpEndpoint, logPrompts: settings.telemetry?.logPrompts, }, - // Git-aware file filtering settings fileFiltering: { respectGitIgnore: settings.fileFiltering?.respectGitIgnore, enableRecursiveFileSearch: @@ -71,6 +69,7 @@ export async function loadConfig( ideMode: false, folderTrust: settings.folderTrust === true, interactive: true, + mcpServers, extensions, }; @@ -139,10 +138,10 @@ export function mergeMcpServers( settings: Settings, extensions: GeminiCLIExtension[], ) { - const mcpServers = { ...(settings.mcpServers || {}) }; + const mcpServers = { ...(settings.mcpServers ?? {}) }; for (const extension of extensions) { - Object.entries(extension.mcpServers || {}).forEach(([key, server]) => { - if (mcpServers[key]) { + Object.entries(extension.mcpServers ?? {}).forEach(([key, server]) => { + if (mcpServers[key] != null) { debugLogger.warn( `Skipping extension MCP config for server with key "${key}" as it already exists.`, ); @@ -191,6 +190,7 @@ export function loadEnvironment(): void { function findEnvFile(startDir: string): string | null { let currentDir = path.resolve(startDir); + while (true) { // prefer gemini-specific .env under GEMINI_DIR const geminiEnvPath = path.join(currentDir, GEMINI_CONFIG_DIR, '.env'); diff --git a/packages/a2a-server/src/config/extension.ts b/packages/a2a-server/src/config/extension.ts index 6c79cddb7a..235d4f19d4 100644 --- a/packages/a2a-server/src/config/extension.ts +++ b/packages/a2a-server/src/config/extension.ts @@ -122,11 +122,11 @@ function loadExtension(extensionDir: string): GeminiCLIExtension | null { name: config.name, version: config.version, path: extensionDir, - contextFiles, - installMetadata, mcpServers: config.mcpServers, excludeTools: config.excludeTools, - isActive: true, // Barring any other signals extensions should be considered Active. + isActive: true, + contextFiles, + installMetadata, } as GeminiCLIExtension; } catch (e) { logger.error( @@ -137,7 +137,7 @@ function loadExtension(extensionDir: string): GeminiCLIExtension | null { } function getContextFileNames(config: ExtensionConfig): string[] { - if (!config.contextFileName) { + if (config.contextFileName == null) { return ['LLXPRT.md', 'GEMINI.md']; } else if (!Array.isArray(config.contextFileName)) { return [config.contextFileName]; @@ -151,8 +151,7 @@ export function loadInstallMetadata( const metadataFilePath = path.join(extensionDir, INSTALL_METADATA_FILENAME); try { const configContent = fs.readFileSync(metadataFilePath, 'utf-8'); - const metadata = JSON.parse(configContent) as ExtensionInstallMetadata; - return metadata; + return JSON.parse(configContent) as ExtensionInstallMetadata; } catch (e) { logger.warn( `Failed to load or parse extension install metadata at ${metadataFilePath}: ${e}`, diff --git a/packages/a2a-server/src/config/settings.ts b/packages/a2a-server/src/config/settings.ts index 2b01d4e33c..51c454cc26 100644 --- a/packages/a2a-server/src/config/settings.ts +++ b/packages/a2a-server/src/config/settings.ts @@ -117,8 +117,8 @@ export function loadSettings(workspaceDir: string): Settings { function resolveEnvVarsInString(value: string): string { const envVarRegex = /\$(?:(\w+)|{([^}]+)})/g; // Find $VAR_NAME or ${VAR_NAME} return value.replace(envVarRegex, (match, varName1, varName2) => { - const varName = varName1 || varName2; - if (process && process.env && typeof process.env[varName] === 'string') { + const varName = varName1 ?? varName2; + if (typeof process.env[varName] === 'string') { return process.env[varName]; } return match; @@ -128,7 +128,7 @@ function resolveEnvVarsInString(value: string): string { function resolveEnvVarsInObject(obj: T): T { if ( obj === null || - obj === undefined || + obj == undefined || typeof obj === 'boolean' || typeof obj === 'number' ) { diff --git a/packages/a2a-server/src/http/app.test.ts b/packages/a2a-server/src/http/app.test.ts index f668ef7a46..721267de73 100644 --- a/packages/a2a-server/src/http/app.test.ts +++ b/packages/a2a-server/src/http/app.test.ts @@ -682,7 +682,7 @@ describe('E2E Tests', () => { const agent = request.agent(app); const res = await agent.get('/listCommands').expect(200); - expect(res.body).toEqual({ + expect(res.body).toStrictEqual({ commands: [ { name: 'test-command', @@ -732,7 +732,7 @@ describe('E2E Tests', () => { const res = await agent.get('/listCommands').expect(200); expect(res.body.commands[0].name).toBe('cyclic-command'); - expect(res.body.commands[0].subCommands).toEqual([]); + expect(res.body.commands[0].subCommands).toStrictEqual([]); expect(warnSpy).toHaveBeenCalledWith( 'Command cyclic-command already inserted in the response, skipping', @@ -762,7 +762,7 @@ describe('E2E Tests', () => { .set('Content-Type', 'application/json') .expect(200); - expect(res.body).toEqual({ + expect(res.body).toStrictEqual({ name: 'extensions list', data: mockExtensions, }); @@ -808,7 +808,7 @@ describe('E2E Tests', () => { name: 'context-check-command', description: 'checks context', execute: vi.fn(async (context: CommandContext) => { - if (!context.agentExecutor) { + if (context.agentExecutor == null) { throw new Error('agentExecutor missing'); } return { name: 'context-check-command', data: 'success' }; @@ -871,14 +871,14 @@ describe('E2E Tests', () => { try { const events = streamToSSEEventsForCommand(data); expect(events.length).toBe(2); - expect(events[0].result).toEqual({ + expect(events[0].result).toStrictEqual({ kind: 'status-update', status: { state: 'working' }, taskId: 'test-task', contextId: 'test-context', final: false, }); - expect(events[1].result).toEqual({ + expect(events[1].result).toStrictEqual({ kind: 'status-update', status: { state: 'completed' }, taskId: 'test-task', @@ -912,7 +912,10 @@ describe('E2E Tests', () => { .set('Content-Type', 'application/json') .expect(200); - expect(res.body).toEqual({ name: 'non-stream-test', data: 'done' }); + expect(res.body).toStrictEqual({ + name: 'non-stream-test', + data: 'done', + }); }); }); }); diff --git a/packages/a2a-server/src/http/app.ts b/packages/a2a-server/src/http/app.ts index 2b3ef61e5c..4be16e00fe 100644 --- a/packages/a2a-server/src/http/app.ts +++ b/packages/a2a-server/src/http/app.ts @@ -120,7 +120,7 @@ export async function createApp() { ); let expressApp = express(); - expressApp.use((req, res, next) => { + expressApp.use((req, _res, next) => { requestStorage.run({ req }, next); }); @@ -184,7 +184,7 @@ export async function createApp() { } } - if (!commandToExecute) { + if (commandToExecute == null) { return res .status(404) .json({ error: `Command not found: ${command}` }); @@ -211,11 +211,10 @@ export async function createApp() { eventBus.off('event', eventHandler); eventBus.finished(); return res.end(); - } else { - const result = await commandToExecute.execute(context, args ?? []); - logger.info('[CoreAgent] Sending /executeCommand response: ', result); - return res.status(200).json(result); } + const result = await commandToExecute.execute(context, args ?? []); + logger.info('[CoreAgent] Sending /executeCommand response: ', result); + return res.status(200).json(result); } catch (e) { logger.error( `Error executing /executeCommand: ${command} with args: ${JSON.stringify( @@ -233,7 +232,7 @@ export async function createApp() { void handleExecuteCommand(req, res, { config, git, agentExecutor }); }); - expressApp.get('/listCommands', (req, res) => { + expressApp.get('/listCommands', (_req, res) => { try { const transformCommand = ( command: Command, @@ -275,7 +274,7 @@ export async function createApp() { } }); - expressApp.get('/tasks/metadata', async (req, res) => { + expressApp.get('/tasks/metadata', async (_req, res) => { // This endpoint is only meaningful if the task store is in-memory. if (!(taskStoreForExecutor instanceof InMemoryTaskStore)) { res.status(501).send({ @@ -306,13 +305,13 @@ export async function createApp() { expressApp.get('/tasks/:taskId/metadata', async (req, res) => { const taskId = req.params.taskId; let wrapper = agentExecutor.getTask(taskId); - if (!wrapper) { + if (wrapper == null) { const sdkTask = await taskStoreForExecutor.load(taskId); - if (sdkTask) { + if (sdkTask != null) { wrapper = await agentExecutor.reconstruct(sdkTask); } } - if (!wrapper) { + if (wrapper == null) { res.status(404).send({ error: 'Task not found' }); return; } diff --git a/packages/a2a-server/src/http/endpoints.test.ts b/packages/a2a-server/src/http/endpoints.test.ts index 45c21777c1..dd1f3cc523 100644 --- a/packages/a2a-server/src/http/endpoints.test.ts +++ b/packages/a2a-server/src/http/endpoints.test.ts @@ -56,7 +56,7 @@ vi.mock('../agent/task.js', () => { this.id = id; this.contextId = contextId; } - static create = vi + static readonly create = vi .fn() .mockImplementation((id, contextId) => Promise.resolve(new MockTask(id, contextId)), @@ -115,7 +115,7 @@ describe('Agent Server Endpoints', () => { if (server) { await new Promise((resolve, reject) => { server.close((err) => { - if (err) return reject(err); + if (err != null) return reject(err); resolve(); }); }); diff --git a/packages/a2a-server/src/persistence/gcs.ts b/packages/a2a-server/src/persistence/gcs.ts index d42ae02270..f087186cdf 100644 --- a/packages/a2a-server/src/persistence/gcs.ts +++ b/packages/a2a-server/src/persistence/gcs.ts @@ -98,7 +98,7 @@ export class GCSTaskStore implements TaskStore { task.metadata as PersistedTaskMetadata, ); - if (!persistedState) { + if (persistedState == null) { throw new Error(`Task ${taskId} is missing persisted state in metadata.`); } const workDir = process.cwd(); @@ -249,7 +249,7 @@ export class GCSTaskStore implements TaskStore { logger.info(`Task ${taskId} metadata loaded from GCS.`); const persistedState = getPersistedState(loadedMetadata); - if (!persistedState) { + if (persistedState == null) { throw new Error( `Loaded metadata for task ${taskId} is missing internal persisted state.`, ); diff --git a/packages/a2a-server/src/utils/testing_utils.ts b/packages/a2a-server/src/utils/testing_utils.ts index 7cfba40ed1..0a077db0b1 100644 --- a/packages/a2a-server/src/utils/testing_utils.ts +++ b/packages/a2a-server/src/utils/testing_utils.ts @@ -156,7 +156,7 @@ export function createMockConfig( const confirmationResults = await Promise.all( scheduledCalls.map(async (call) => { const tool = mockConfig - .getToolRegistry?.() + .getToolRegistry() ?.getTool?.(call.request.name); if (!tool || typeof tool.build !== 'function') { return false; @@ -220,19 +220,17 @@ export function createMockConfig( }, ); - const scheduler = { + return { schedule, cancelAll: vi.fn(), dispose: vi.fn(), toolCalls: [], getPreferredEditor: callbacks.getPreferredEditor ?? vi.fn(), config: mockConfig, - toolRegistry: mockConfig?.getToolRegistry?.() || { + toolRegistry: mockConfig.getToolRegistry() || { getTool: vi.fn(), }, } as unknown as CoreToolScheduler; - - return scheduler; }, ), ...overrides, @@ -299,7 +297,7 @@ export function assertUniqueFinalEventIsLast( expect(finalEvent.metadata?.['coderAgent']).toMatchObject({ kind: 'state-change', }); - expect(finalEvent.status?.state).toBe('input-required'); + expect(finalEvent.status.state).toBe('input-required'); expect(finalEvent.final).toBe(true); // There is only one event with final and its the last diff --git a/packages/cli/src/__tests__/sessionBrowserE2E.spec.ts b/packages/cli/src/__tests__/sessionBrowserE2E.spec.ts index 1986657c8e..3d4e3495cb 100644 --- a/packages/cli/src/__tests__/sessionBrowserE2E.spec.ts +++ b/packages/cli/src/__tests__/sessionBrowserE2E.spec.ts @@ -29,7 +29,7 @@ import { SessionRecordingService, SessionDiscovery, SessionLockManager, - RecordingIntegration, + type RecordingIntegration, type SessionRecordingServiceConfig, type IContent, type LockHandle, @@ -63,10 +63,10 @@ function makeConfig( return { sessionId: overrides.sessionId ?? crypto.randomUUID(), projectHash: overrides.projectHash ?? PROJECT_HASH, - chatsDir, workspaceDirs: overrides.workspaceDirs ?? ['/test/workspace'], provider: overrides.provider ?? 'anthropic', model: overrides.model ?? 'claude-4', + chatsDir, }; } @@ -109,7 +109,7 @@ async function createTestSession( const svc = new SessionRecordingService(config); // If contents are provided, use those directly - if (opts.contents) { + if (opts.contents != null) { for (const content of opts.contents) { svc.recordContent(content); } @@ -231,7 +231,7 @@ function makeCommandContext( services: { config: { isInteractive: () => - overrides.services?.config?.isInteractive?.() ?? true, + overrides.services?.config?.isInteractive() ?? true, } as CommandContext['services']['config'], settings: {} as CommandContext['services']['settings'], git: undefined, @@ -367,7 +367,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 }); const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); } }); @@ -402,7 +402,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 }); const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); } }); @@ -432,7 +432,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 }); const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); } }); @@ -556,7 +556,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 recordingsToDispose.push(newRecording!); const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); } }); @@ -640,7 +640,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 expect(fileContent).toContain('new event after resume'); const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); } }); @@ -680,7 +680,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 } const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); } }); }); @@ -757,7 +757,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 }); const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); } }); }); @@ -909,7 +909,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 }); const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); } }); }); @@ -963,7 +963,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) await newLock.release(); + if (newLock != null) await newLock.release(); } } finally { await fs.rm(localTempDir, { recursive: true, force: true }); @@ -1012,7 +1012,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) await newLock.release(); + if (newLock != null) await newLock.release(); } } finally { await fs.rm(localTempDir, { recursive: true, force: true }); @@ -1074,7 +1074,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 // Cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) await newLock.release(); + if (newLock != null) await newLock.release(); } finally { await fs.rm(localTempDir, { recursive: true, force: true }); } @@ -1264,7 +1264,7 @@ describe('Session Browser E2E Integration @plan:PLAN-20260214-SESSIONBROWSER.P30 expect(result.metadata.sessionId).toBe(contentSessionId); const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); } }); diff --git a/packages/cli/src/auth/BucketFailoverHandlerImpl.spec.ts b/packages/cli/src/auth/BucketFailoverHandlerImpl.spec.ts index 2910cb75bf..f03abaa01b 100644 --- a/packages/cli/src/auth/BucketFailoverHandlerImpl.spec.ts +++ b/packages/cli/src/auth/BucketFailoverHandlerImpl.spec.ts @@ -499,7 +499,7 @@ describe('BucketFailoverHandlerImpl', () => { // Mock refresh to fail (token was revoked server-side) const provider = oauthManager.getProvider('anthropic'); expect(provider).toBeDefined(); - if (provider) { + if (provider != null) { provider.refreshToken = vi.fn(async () => null); } @@ -543,7 +543,7 @@ describe('BucketFailoverHandlerImpl', () => { // Mock refresh to fail const provider = oauthManager.getProvider('anthropic'); expect(provider).toBeDefined(); - if (provider) { + if (provider != null) { provider.refreshToken = vi.fn(async () => null); } @@ -588,7 +588,7 @@ describe('BucketFailoverHandlerImpl', () => { const refreshedToken = makeToken('refreshed-token'); const provider = oauthManager.getProvider('anthropic'); expect(provider).toBeDefined(); - if (provider) { + if (provider != null) { provider.refreshToken = vi.fn(async () => refreshedToken); } @@ -806,7 +806,7 @@ describe('BucketFailoverHandlerImpl', () => { const refreshedToken = makeToken('refreshed-b'); const provider = oauthManager.getProvider('anthropic'); expect(provider).toBeDefined(); - if (provider) { + if (provider != null) { provider.refreshToken = vi.fn(async () => refreshedToken); } @@ -849,7 +849,7 @@ describe('BucketFailoverHandlerImpl', () => { // Mock refresh to fail const provider = oauthManager.getProvider('anthropic'); expect(provider).toBeDefined(); - if (provider) { + if (provider != null) { provider.refreshToken = vi.fn(async () => null); } @@ -1024,7 +1024,7 @@ describe('BucketFailoverHandlerImpl', () => { // Mock authenticate to succeed and provide token for bucket-b const authenticateSpy = vi.fn( - async (provider: string, bucket?: string) => { + async (_provider: string, bucket?: string) => { if (bucket === 'bucket-b') { await tokenStore.saveToken( 'anthropic', @@ -1074,13 +1074,13 @@ describe('BucketFailoverHandlerImpl', () => { // Mock refresh to fail for all const provider = oauthManager.getProvider('anthropic'); expect(provider).toBeDefined(); - if (provider) { + if (provider != null) { provider.refreshToken = vi.fn(async () => null); } // Mock authenticate to succeed for bucket-b const authenticateSpy = vi.fn( - async (provider: string, bucket?: string) => { + async (_provider: string, bucket?: string) => { if (bucket === 'bucket-b') { await tokenStore.saveToken( 'anthropic', @@ -1124,7 +1124,7 @@ describe('BucketFailoverHandlerImpl', () => { // Mock authenticate to succeed and save token oauthManager.authenticate = vi.fn( - async (provider: string, bucket?: string) => { + async (_provider: string, bucket?: string) => { if (bucket === 'bucket-b') { await tokenStore.saveToken( 'anthropic', @@ -1228,7 +1228,7 @@ describe('BucketFailoverHandlerImpl', () => { ); const authenticateSpy = vi.fn( - async (provider: string, bucket?: string) => { + async (_provider: string, bucket?: string) => { if (bucket === 'bucket-b') { await tokenStore.saveToken( 'anthropic', @@ -1302,7 +1302,7 @@ describe('BucketFailoverHandlerImpl', () => { // Mock authenticate to succeed and save valid token const reauthToken = makeToken('reauth-success'); oauthManager.authenticate = vi.fn( - async (provider: string, bucket?: string) => { + async (_provider: string, bucket?: string) => { if (bucket === 'bucket-b') { await tokenStore.saveToken('anthropic', reauthToken, bucket); } diff --git a/packages/cli/src/auth/BucketFailoverHandlerImpl.ts b/packages/cli/src/auth/BucketFailoverHandlerImpl.ts index dc94389af1..187e9d777b 100644 --- a/packages/cli/src/auth/BucketFailoverHandlerImpl.ts +++ b/packages/cli/src/auth/BucketFailoverHandlerImpl.ts @@ -11,7 +11,7 @@ */ import { - BucketFailoverHandler, + type BucketFailoverHandler, DebugLogger, type FailoverContext, type BucketFailureReason, @@ -181,7 +181,7 @@ export class BucketFailoverHandlerImpl implements BucketFailoverHandler { reason = 'no-token'; } - if (storedToken && reason === null) { + if (storedToken != null && reason === null) { const nowSec = Math.floor(Date.now() / 1000); const remainingSec = storedToken.expiry - nowSec; @@ -192,7 +192,7 @@ export class BucketFailoverHandlerImpl implements BucketFailoverHandler { this.provider, currentBucket, ); - if (refreshedToken && refreshedToken.expiry > nowSec) { + if (refreshedToken != null && refreshedToken.expiry > nowSec) { // Refresh succeeded for triggering bucket — no failover needed (REQ-1598-CL07) logger.debug( 'Refresh succeeded for triggering bucket — no failover needed', @@ -220,9 +220,9 @@ export class BucketFailoverHandlerImpl implements BucketFailoverHandler { // Token exists and is not expired — failure isn't credential-related reason = 'skipped'; } - } else if (reason === null) { + } else { // No token in store (REQ-1598-CL03) - reason = 'no-token'; + reason ??= 'no-token'; } } @@ -246,10 +246,6 @@ export class BucketFailoverHandlerImpl implements BucketFailoverHandler { for (const bucket of this.buckets) { // Skip buckets already tried in this session (REQ-1598-FL13) if (this.triedBucketsThisSession.has(bucket)) { - // Only mark as skipped if not already classified in Pass 1 - if (!this.lastFailoverReasons[bucket]) { - this.lastFailoverReasons[bucket] = 'skipped'; - } continue; } @@ -280,7 +276,7 @@ export class BucketFailoverHandlerImpl implements BucketFailoverHandler { this.provider, bucket, ); - if (refreshedToken && refreshedToken.expiry > nowSec) { + if (refreshedToken != null && refreshedToken.expiry > nowSec) { // Refresh succeeded — switch bucket const bucketIndex = this.buckets.indexOf(bucket); if (bucketIndex >= 0) { @@ -389,13 +385,13 @@ export class BucketFailoverHandlerImpl implements BucketFailoverHandler { if (candidateBucket !== undefined) { const existingForegroundReauth = this.foregroundReauthInFlightByBucket.get(candidateBucket); - if (existingForegroundReauth) { + if (existingForegroundReauth != null) { return existingForegroundReauth; } const pass3Promise = (async (): Promise => { const eagerAuthInFlight = this.ensureBucketsAuthInFlight; - if (eagerAuthInFlight) { + if (eagerAuthInFlight != null) { logger.debug( `Foreground reauth waiting for in-flight eager auth (provider=${this.provider}, bucket=${candidateBucket})`, ); @@ -450,7 +446,7 @@ export class BucketFailoverHandlerImpl implements BucketFailoverHandler { } const lateEagerAuthInFlight = this.ensureBucketsAuthInFlight; - if (lateEagerAuthInFlight) { + if (lateEagerAuthInFlight != null) { logger.debug( `Foreground reauth detected late in-flight eager auth (provider=${this.provider}, bucket=${candidateBucket})`, ); @@ -578,8 +574,8 @@ export class BucketFailoverHandlerImpl implements BucketFailoverHandler { ); logger.debug('No more buckets available for failover', { provider: this.provider, - currentBucket, attemptedAll: true, + currentBucket, }); return false; } @@ -604,7 +600,7 @@ export class BucketFailoverHandlerImpl implements BucketFailoverHandler { return; } - if (this.ensureBucketsAuthInFlight) { + if (this.ensureBucketsAuthInFlight != null) { await this.ensureBucketsAuthInFlight; return; } diff --git a/packages/cli/src/auth/OAuthBucketManager.ts b/packages/cli/src/auth/OAuthBucketManager.ts index 6c42a1a9fa..0fe3317583 100644 --- a/packages/cli/src/auth/OAuthBucketManager.ts +++ b/packages/cli/src/auth/OAuthBucketManager.ts @@ -106,7 +106,7 @@ export class OAuthBucketManager { ): Promise { const token = await this.tokenStore.getToken(provider, bucket); - if (!token) { + if (token == null) { return { bucket, authenticated: false, @@ -146,7 +146,7 @@ export class OAuthBucketManager { async validateBucketExists(provider: string, bucket: string): Promise { const token = await this.tokenStore.getToken(provider, bucket); - if (!token) { + if (token == null) { throw new Error( `OAuth bucket '${bucket}' for provider '${provider}' not found. ` + `Use /auth ${provider} login ${bucket} to authenticate.`, @@ -159,7 +159,7 @@ export class OAuthBucketManager { * Returns undefined if no more buckets available */ getNextBucket( - provider: string, + _provider: string, currentBucket: string, profileBuckets: string[], ): string | undefined { diff --git a/packages/cli/src/auth/__tests__/auth-flow-orchestrator.spec.ts b/packages/cli/src/auth/__tests__/auth-flow-orchestrator.spec.ts index 027aafd8a4..1881ad3ad1 100644 --- a/packages/cli/src/auth/__tests__/auth-flow-orchestrator.spec.ts +++ b/packages/cli/src/auth/__tests__/auth-flow-orchestrator.spec.ts @@ -8,9 +8,10 @@ */ import { describe, it, expect, vi } from 'vitest'; -import type { OAuthToken, TokenStore } from '../types.js'; -import type { OAuthProvider } from '../types.js'; import type { + OAuthToken, + TokenStore, + OAuthProvider, BucketFailoverOAuthManagerLike, AuthenticatorInterface, } from '../types.js'; diff --git a/packages/cli/src/auth/__tests__/auth-status-service.spec.ts b/packages/cli/src/auth/__tests__/auth-status-service.spec.ts index 7aa02aef7b..e34bf0aef9 100644 --- a/packages/cli/src/auth/__tests__/auth-status-service.spec.ts +++ b/packages/cli/src/auth/__tests__/auth-status-service.spec.ts @@ -244,8 +244,8 @@ describe('AuthStatusService.isAuthenticated', () => { const provider = makeProvider('qwen', { isAuthenticated: isAuthMock }); const service = makeService({ providers: [provider], - tokenStore, oauthEnabled: true, + tokenStore, }); const result = await service.isAuthenticated('qwen'); // Override threw → fallback → valid token found → true @@ -267,8 +267,8 @@ describe('AuthStatusService.isAuthenticated', () => { const provider = makeProvider('qwen', { isAuthenticated: isAuthMock }); const service = makeService({ providers: [provider], - tokenStore, oauthEnabled: true, + tokenStore, }); const result = await service.isAuthenticated('qwen'); // Override returned false → fallback → valid token found → true @@ -295,8 +295,8 @@ describe('AuthStatusService.isAuthenticated', () => { const provider = makeProvider('anthropic'); const service = makeService({ providers: [provider], - tokenStore, oauthEnabled: false, + tokenStore, }); const result = await service.isAuthenticated('anthropic'); expect(result).toBe(false); @@ -534,8 +534,8 @@ describe('AuthStatusService.getAuthStatus', () => { const validToken: OAuthToken = { access_token: 'valid', refresh_token: 'r', - expiry, token_type: 'Bearer', + expiry, }; const tokenStore = makeTokenStore({ getToken: vi.fn().mockResolvedValue(validToken), @@ -543,8 +543,8 @@ describe('AuthStatusService.getAuthStatus', () => { const provider = makeProvider('anthropic'); const service = makeService({ providers: [provider], - tokenStore, oauthEnabled: true, + tokenStore, }); const statuses = await service.getAuthStatus(); expect(statuses[0]).toMatchObject({ @@ -560,8 +560,8 @@ describe('AuthStatusService.getAuthStatus', () => { const expiredToken: OAuthToken = { access_token: 'expired', refresh_token: 'r', - expiry, token_type: 'Bearer', + expiry, }; const tokenStore = makeTokenStore({ getToken: vi.fn().mockResolvedValue(expiredToken), @@ -569,8 +569,8 @@ describe('AuthStatusService.getAuthStatus', () => { const provider = makeProvider('anthropic'); const service = makeService({ providers: [provider], - tokenStore, oauthEnabled: true, + tokenStore, }); const statuses = await service.getAuthStatus(); @@ -644,8 +644,8 @@ describe('AuthStatusService.getAuthStatusWithBuckets', () => { const validToken: OAuthToken = { access_token: 'tok', refresh_token: 'r', - expiry, token_type: 'Bearer', + expiry, }; const tokenStore = makeTokenStore({ listBuckets: vi.fn().mockResolvedValue(['default', 'bucket-a']), diff --git a/packages/cli/src/auth/__tests__/behavioral/bridge-ui.behavioral.spec.ts b/packages/cli/src/auth/__tests__/behavioral/bridge-ui.behavioral.spec.ts index 2bb4e70a5e..6221e6f9b4 100644 --- a/packages/cli/src/auth/__tests__/behavioral/bridge-ui.behavioral.spec.ts +++ b/packages/cli/src/auth/__tests__/behavioral/bridge-ui.behavioral.spec.ts @@ -17,8 +17,8 @@ function makeOAuthItem( Omit { return { type: 'oauth_url', - text, url: `https://example.com/${text}`, + text, }; } diff --git a/packages/cli/src/auth/__tests__/global-oauth-ui.test.ts b/packages/cli/src/auth/__tests__/global-oauth-ui.test.ts index a2657bce99..8df848a6b5 100644 --- a/packages/cli/src/auth/__tests__/global-oauth-ui.test.ts +++ b/packages/cli/src/auth/__tests__/global-oauth-ui.test.ts @@ -20,8 +20,8 @@ type ItemData = Omit; function makeOAuthItem(text: string): ItemData { const item: HistoryItemOAuthURL = { type: 'oauth_url', - text, url: `https://example.com/${text}`, + text, }; return item; } diff --git a/packages/cli/src/auth/__tests__/multi-bucket-auth.spec.ts b/packages/cli/src/auth/__tests__/multi-bucket-auth.spec.ts index 83f264867c..f1acfff17c 100644 --- a/packages/cli/src/auth/__tests__/multi-bucket-auth.spec.ts +++ b/packages/cli/src/auth/__tests__/multi-bucket-auth.spec.ts @@ -468,7 +468,7 @@ describe('Phase 9: Multi-Bucket Authentication Flow', () => { setEphemeralSetting('auth-bucket-delay', 0); const slowAuthenticator = new MultiBucketAuthenticator( - async (provider: string, bucket: string) => { + async (_provider: string, bucket: string) => { await new Promise((resolve) => setTimeout(resolve, 50)); authLog.push(`Authenticated ${bucket}`); }, diff --git a/packages/cli/src/auth/__tests__/oauth-manager.issue913.spec.ts b/packages/cli/src/auth/__tests__/oauth-manager.issue913.spec.ts index 3fd9863111..ef0feb6bbd 100644 --- a/packages/cli/src/auth/__tests__/oauth-manager.issue913.spec.ts +++ b/packages/cli/src/auth/__tests__/oauth-manager.issue913.spec.ts @@ -6,8 +6,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { OAuthManager } from '../oauth-manager.js'; -import type { OAuthProvider } from '../types.js'; -import { TokenStore, OAuthToken } from '../types.js'; +import type { OAuthProvider, TokenStore, OAuthToken } from '../types.js'; import { MessageBus, PolicyEngine } from '@vybestack/llxprt-code-core'; /** diff --git a/packages/cli/src/auth/__tests__/oauth-manager.user-declined.spec.ts b/packages/cli/src/auth/__tests__/oauth-manager.user-declined.spec.ts index 914aa1a1af..2e00c1059e 100644 --- a/packages/cli/src/auth/__tests__/oauth-manager.user-declined.spec.ts +++ b/packages/cli/src/auth/__tests__/oauth-manager.user-declined.spec.ts @@ -6,8 +6,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { OAuthManager } from '../oauth-manager.js'; -import type { OAuthProvider } from '../types.js'; -import { TokenStore, OAuthToken } from '../types.js'; +import type { OAuthProvider, TokenStore, OAuthToken } from '../types.js'; import { MessageBus, PolicyEngine } from '@vybestack/llxprt-code-core'; /** @@ -168,10 +167,7 @@ describe('Issue #828: User Declined Auth Prompt Tracking', () => { messageBus.requestBucketAuthConfirmation = vi.fn( async (): Promise => { confirmationCount++; - if (confirmationCount === 1) { - return false; // User declines on first attempt - } - return true; + return !(confirmationCount === 1); }, ); diff --git a/packages/cli/src/auth/__tests__/oauthManager.proactive-renewal.test.ts b/packages/cli/src/auth/__tests__/oauthManager.proactive-renewal.test.ts index d1bb91cc61..f3df122d28 100644 --- a/packages/cli/src/auth/__tests__/oauthManager.proactive-renewal.test.ts +++ b/packages/cli/src/auth/__tests__/oauthManager.proactive-renewal.test.ts @@ -14,8 +14,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { OAuthManager } from '../oauth-manager.js'; -import type { OAuthProvider } from '../types.js'; -import type { TokenStore, OAuthToken } from '../types.js'; +import type { OAuthProvider, TokenStore, OAuthToken } from '../types.js'; /** * Create a mock token with specified expiry time @@ -322,7 +321,7 @@ describe('Proactive renewal @plan:PLAN-20260223-ISSUE1598.P13', () => { // Mock getToken to return tokens for different buckets vi.mocked(tokenStore.getToken).mockImplementation( - async (providerName, bucket) => { + async (_providerName, bucket) => { if (bucket === 'bucket1') return tokenBucket1; if (bucket === 'bucket2') return tokenBucket2; return null; diff --git a/packages/cli/src/auth/__tests__/proactive-renewal-manager.spec.ts b/packages/cli/src/auth/__tests__/proactive-renewal-manager.spec.ts index a33dfc3973..42e08fa9d7 100644 --- a/packages/cli/src/auth/__tests__/proactive-renewal-manager.spec.ts +++ b/packages/cli/src/auth/__tests__/proactive-renewal-manager.spec.ts @@ -14,8 +14,7 @@ import { ProactiveRenewalManager, MAX_PROACTIVE_RENEWAL_FAILURES, } from '../proactive-renewal-manager.js'; -import type { OAuthProvider } from '../types.js'; -import type { TokenStore, OAuthToken } from '../types.js'; +import type { OAuthProvider, TokenStore, OAuthToken } from '../types.js'; function createMockToken( expirySeconds: number, @@ -564,7 +563,7 @@ describe('ProactiveRenewalManager', () => { }); it('should return "default" for undefined', () => { - expect(manager.normalizeBucket(undefined)).toBe('default'); + expect(manager.normalizeBucket()).toBe('default'); }); it('should return "default" for empty string', () => { diff --git a/packages/cli/src/auth/__tests__/provider-usage-info.spec.ts b/packages/cli/src/auth/__tests__/provider-usage-info.spec.ts index b36e17e483..cc41db8564 100644 --- a/packages/cli/src/auth/__tests__/provider-usage-info.spec.ts +++ b/packages/cli/src/auth/__tests__/provider-usage-info.spec.ts @@ -167,7 +167,7 @@ describe('getAnthropicUsageInfo', () => { }); mockFetchAnthropicUsage.mockResolvedValue({ plan: 'free' }); - await getAnthropicUsageInfo(store, undefined); + await getAnthropicUsageInfo(store); expect(store.getToken).toHaveBeenCalledWith('anthropic', 'default'); }); diff --git a/packages/cli/src/auth/__tests__/token-access-coordinator.spec.ts b/packages/cli/src/auth/__tests__/token-access-coordinator.spec.ts index 938f740b95..8da4af3e11 100644 --- a/packages/cli/src/auth/__tests__/token-access-coordinator.spec.ts +++ b/packages/cli/src/auth/__tests__/token-access-coordinator.spec.ts @@ -21,8 +21,7 @@ import { describe, it, expect, vi } from 'vitest'; import { TokenAccessCoordinator } from '../token-access-coordinator.js'; -import type { OAuthProvider } from '../types.js'; -import type { OAuthToken, TokenStore } from '../types.js'; +import type { OAuthProvider, OAuthToken, TokenStore } from '../types.js'; import type { OAuthTokenRequestMetadata } from '@vybestack/llxprt-code-core'; // -------------------------------------------------------------------------- diff --git a/packages/cli/src/auth/anthropic-oauth-provider.test.ts b/packages/cli/src/auth/anthropic-oauth-provider.test.ts index bd98b576bb..85a59d5de1 100644 --- a/packages/cli/src/auth/anthropic-oauth-provider.test.ts +++ b/packages/cli/src/auth/anthropic-oauth-provider.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { AnthropicOAuthProvider } from './anthropic-oauth-provider.js'; -import { TokenStore } from '@vybestack/llxprt-code-core'; +import type { TokenStore } from '@vybestack/llxprt-code-core'; // Mock the ClipboardService class - do this before importing it vi.mock('../services/ClipboardService.js', () => ({ diff --git a/packages/cli/src/auth/anthropic-oauth-provider.ts b/packages/cli/src/auth/anthropic-oauth-provider.ts index 95e4a2f48e..ea1d820b7f 100644 --- a/packages/cli/src/auth/anthropic-oauth-provider.ts +++ b/packages/cli/src/auth/anthropic-oauth-provider.ts @@ -10,11 +10,11 @@ import type { OAuthProvider } from './types.js'; import { - OAuthToken, + type OAuthToken, AnthropicDeviceFlow, openBrowserSecurely, shouldLaunchBrowser, - TokenStore, + type TokenStore, OAuthError, OAuthErrorFactory, GracefulErrorHandler, @@ -23,9 +23,9 @@ import { debugLogger, } from '@vybestack/llxprt-code-core'; import { ClipboardService } from '../services/ClipboardService.js'; -import { HistoryItemWithoutId, HistoryItemOAuthURL } from '../ui/types.js'; +import type { HistoryItemWithoutId, HistoryItemOAuthURL } from '../ui/types.js'; import { - LocalOAuthCallbackServer, + type LocalOAuthCallbackServer, startLocalOAuthCallback, } from './local-oauth-callback.js'; import { globalOAuthUI } from './global-oauth-ui.js'; @@ -81,7 +81,7 @@ export class AnthropicOAuthProvider implements OAuthProvider { * @requirement REQ-004.2 * Deprecation warning for missing TokenStore */ - if (!_tokenStore) { + if (_tokenStore == null) { debugLogger.warn( `DEPRECATION: ${this.name} OAuth provider created without TokenStore. ` + `Token persistence will not work. Please update your code.`, @@ -143,7 +143,7 @@ export class AnthropicOAuthProvider implements OAuthProvider { this.armPendingAuthDialog(); - if (localCallback) { + if (localCallback != null) { return this.raceCallbackVsManualEntry(attemptId, localCallback); } @@ -201,16 +201,17 @@ export class AnthropicOAuthProvider implements OAuthProvider { const deviceCodeUrl = deviceCodeResponse.verification_uri_complete || `${deviceCodeResponse.verification_uri}?user_code=${deviceCodeResponse.user_code}`; - const callbackUrl = localCallback - ? this.deviceFlow.buildAuthorizationUrl(localCallback.redirectUri) - : null; + const callbackUrl = + localCallback != null + ? this.deviceFlow.buildAuthorizationUrl(localCallback.redirectUri) + : null; const historyItem: HistoryItemOAuthURL = { type: 'oauth_url', text: `Please visit the following URL to authorize with Anthropic Claude:\n${deviceCodeUrl}`, url: deviceCodeUrl, }; - if (this.addItem) { + if (this.addItem != null) { this.addItem(historyItem); } else { globalOAuthUI.callAddItem(historyItem); @@ -378,7 +379,7 @@ export class AnthropicOAuthProvider implements OAuthProvider { * @pseudocode lines 17-25 */ async initializeToken(): Promise { - if (!this._tokenStore) { + if (this._tokenStore == null) { return; } @@ -387,7 +388,7 @@ export class AnthropicOAuthProvider implements OAuthProvider { // @pseudocode line 19: Load saved token from store const savedToken = await this._tokenStore!.getToken('anthropic'); // @pseudocode lines 20-22: Check if token exists and not expired - if (savedToken && !isTokenExpired(savedToken)) { + if (savedToken != null && !isTokenExpired(savedToken)) { return; // Token is valid, ready to use } }, @@ -404,7 +405,7 @@ export class AnthropicOAuthProvider implements OAuthProvider { */ async getToken(): Promise { await this.ensureInitialized(); - if (!this._tokenStore) { + if (this._tokenStore == null) { return null; } @@ -463,9 +464,9 @@ export class AnthropicOAuthProvider implements OAuthProvider { await this.ensureInitialized(); // NO ERROR SUPPRESSION - let it fail loudly - if (token) { + if (token != null) { try { - if (this.deviceFlow.revokeToken) { + if (this.deviceFlow.revokeToken != null) { await this.deviceFlow.revokeToken(token.access_token); } else { this.logger.debug( diff --git a/packages/cli/src/auth/auth-flow-orchestrator.ts b/packages/cli/src/auth/auth-flow-orchestrator.ts index 10645a4746..e6b2fa234a 100644 --- a/packages/cli/src/auth/auth-flow-orchestrator.ts +++ b/packages/cli/src/auth/auth-flow-orchestrator.ts @@ -98,7 +98,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { private requireRuntimeMessageBus(): MessageBus { const messageBus = this.runtimeMessageBus; - if (!messageBus) { + if (messageBus == null) { throw new Error( 'OAuthManager requires a runtime MessageBus from the session/runtime composition root.', ); @@ -126,7 +126,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { } const provider = this.providerRegistry.getProvider(providerName); - if (!provider) { + if (provider == null) { throw new Error(`Unknown provider: ${providerName}`); } @@ -189,7 +189,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { const nowInSeconds = Math.floor(Date.now() / 1000); const thirtySecondsFromNow = nowInSeconds + 30; - if (diskToken && diskToken.expiry > thirtySecondsFromNow) { + if (diskToken != null && diskToken.expiry > thirtySecondsFromNow) { if (!this.providerRegistry.isOAuthEnabled(providerName)) { this.providerRegistry.setOAuthEnabledState(providerName, true); } @@ -218,7 +218,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { () => `[FLOW] provider.initiateAuth() returned token for ${providerName}`, ); - if (!token) { + if (token == null) { throw new Error('Authentication completed but no token was returned'); } @@ -249,7 +249,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { const nowInSeconds = Math.floor(Date.now() / 1000); const thirtySecondsFromNow = nowInSeconds + 30; - if (diskToken && diskToken.expiry > thirtySecondsFromNow) { + if (diskToken != null && diskToken.expiry > thirtySecondsFromNow) { if (!this.providerRegistry.isOAuthEnabled(providerName)) { this.providerRegistry.setOAuthEnabledState(providerName, true); } @@ -275,10 +275,10 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { private async attemptRefreshBeforeBrowser( providerName: string, bucket: string | undefined, - diskToken: import('./types.js').OAuthToken | undefined, + diskToken: OAuthToken | undefined, ): Promise { if ( - !diskToken || + diskToken == null || typeof diskToken.refresh_token !== 'string' || diskToken.refresh_token === '' ) { @@ -286,7 +286,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { } const provider = this.providerRegistry.getProvider(providerName); - if (!provider) { + if (provider == null) { return false; } @@ -362,7 +362,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { } const refreshedToken = await provider.refreshToken(latestToken); - if (refreshedToken) { + if (refreshedToken != null) { const mergedToken = mergeRefreshedToken( latestToken as OAuthTokenWithExtras, refreshedToken as OAuthTokenWithExtras, @@ -467,7 +467,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { private async runMultiBucketAuth( MultiBucketAuthenticator: MultiBucketAuthenticatorLike, providerName: string, - buckets: string[], + _buckets: string[], unauthenticatedBuckets: string[], getEphemeralSetting: (key: string) => T | undefined, ): Promise { @@ -511,7 +511,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { // @plan PLAN-20251213issue490 if (buckets.length > 1) { const config = this.config; - if (config) { + if (config != null) { const handler = new BucketFailoverHandlerImpl( buckets, providerName, @@ -547,11 +547,11 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { providerName, bucket, ); - if (existingToken && existingToken.expiry > nowInSeconds + 30) { + if (existingToken != null && existingToken.expiry > nowInSeconds + 30) { logger.debug(`Bucket ${bucket} already authenticated, skipping`, { provider: providerName, - bucket, expiry: existingToken.expiry, + bucket, }); } else { unauthenticated.push(bucket); @@ -582,7 +582,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { try { const existingToken = await this.tokenStore.getToken(provider, bucket); const now = Math.floor(Date.now() / 1000); - if (existingToken && existingToken.expiry > now + 30) { + if (existingToken != null && existingToken.expiry > now + 30) { logger.debug( `Bucket ${bucket} already authenticated (cross-process), skipping`, ); @@ -607,7 +607,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { * Uses TUI dialog if available, falls back to TTY stdin, then delay. */ private buildOnPromptCallback( - providerName: string, + _providerName: string, buckets: string[], getEphemeralSetting: (key: string) => T | undefined, ): (provider: string, bucket: string) => Promise { @@ -635,7 +635,7 @@ export class AuthFlowOrchestrator implements AuthenticatorInterface { buckets.length, ); - if (showPrompt) { + if (showPrompt === true) { logger.debug( 'Prompt mode enabled - waiting indefinitely for user approval', ); diff --git a/packages/cli/src/auth/auth-status-service.ts b/packages/cli/src/auth/auth-status-service.ts index 0f803f3fd7..04dea911aa 100644 --- a/packages/cli/src/auth/auth-status-service.ts +++ b/packages/cli/src/auth/auth-status-service.ts @@ -67,7 +67,7 @@ export class AuthStatusService { const token = await this.tokenStore.getToken(providerName); - if (token) { + if (token != null) { const now = Date.now() / 1000; const expiresIn = Math.max(0, Math.floor(token.expiry - now)); const authenticated = token.expiry > now; @@ -127,7 +127,7 @@ export class AuthStatusService { // G1: consult provider override only when OAuth is enabled const provider = this.providerRegistry.getProvider(providerName); - if (provider?.isAuthenticated) { + if (provider?.isAuthenticated != null) { try { const overrideResult = await provider.isAuthenticated(); if (overrideResult) { @@ -144,7 +144,7 @@ export class AuthStatusService { } const token = await this.tokenStore.getToken(providerName, bucket); - if (!token) return false; + if (token == null) return false; const now = Date.now() / 1000; return token.expiry > now; @@ -170,7 +170,7 @@ export class AuthStatusService { } const provider = this.providerRegistry.getProvider(providerName); - if (!provider) { + if (provider == null) { throw new Error(`Unknown provider: ${providerName}`); } @@ -195,7 +195,7 @@ export class AuthStatusService { // Best-effort provider-side revoke if ('logout' in provider && typeof provider.logout === 'function') { try { - if (tokenForLogout) { + if (tokenForLogout != null) { await provider.logout(tokenForLogout); } } catch (error) { @@ -304,7 +304,7 @@ export class AuthStatusService { const token = await this.tokenStore.getToken(provider, bucket); const isSessionBucket = bucket === sessionBucket; - if (token) { + if (token != null) { const authenticated = token.expiry > now; statuses.push({ bucket, @@ -364,13 +364,13 @@ export class AuthStatusService { // Still attempt scope flush below } - if (providerManager) { + if (providerManager != null) { const rawProvider = providerManager.getProviderByName(providerName); const provider = unwrapLoggingProvider( rawProvider as { name: string } | undefined, ); - if (!provider) { + if (provider == null) { logger.debug( `Provider ${providerName} not found in runtime manager; skipping cache clear.`, ); @@ -427,11 +427,14 @@ export class AuthStatusService { * prior cleanup failures (G4/E pattern). */ private flushKnownRuntimeScopes( - providerName: string, + _providerName: string, runtimeContext: { runtimeId?: string } | undefined, ): void { const knownRuntimeIds = ['legacy-singleton', 'provider-manager-singleton']; - if (runtimeContext && typeof runtimeContext.runtimeId === 'string') { + if ( + runtimeContext != null && + typeof runtimeContext.runtimeId === 'string' + ) { if (!knownRuntimeIds.includes(runtimeContext.runtimeId)) { knownRuntimeIds.push(runtimeContext.runtimeId); } diff --git a/packages/cli/src/auth/codex-oauth-provider.ts b/packages/cli/src/auth/codex-oauth-provider.ts index 6cb273a389..59c1800c1c 100644 --- a/packages/cli/src/auth/codex-oauth-provider.ts +++ b/packages/cli/src/auth/codex-oauth-provider.ts @@ -85,7 +85,7 @@ export class CodexOAuthProvider implements OAuthProvider { private async initializeToken(): Promise { try { const savedToken = await this.tokenStore.getToken('codex'); - if (savedToken) { + if (savedToken != null) { this.logger.debug(() => 'Loaded existing Codex token from storage'); } } catch (error) { @@ -103,7 +103,7 @@ export class CodexOAuthProvider implements OAuthProvider { () => `[FLOW] initiateAuth() called, authInProgress=${!!this.authInProgress}`, ); - if (this.authInProgress) { + if (this.authInProgress != null) { this.logger.debug(() => '[FLOW] OAuth already in progress, waiting...'); const token = await this.authInProgress; this.logger.debug(() => '[FLOW] Finished waiting for existing auth flow'); @@ -168,7 +168,7 @@ export class CodexOAuthProvider implements OAuthProvider { }); const port = parseInt( - localCallback.redirectUri.match(/:(\d+)\//)?.[1] || '0', + RegExp(/:(\d+)\//).exec(localCallback.redirectUri)?.[1] || '0', 10, ); this.logger.debug( @@ -198,7 +198,7 @@ export class CodexOAuthProvider implements OAuthProvider { text: `Please visit the following URL to authenticate with Codex:\n${authUrl}`, url: authUrl, }; - if (this.addItem) { + if (this.addItem != null) { this.addItem(historyItem); } else { globalOAuthUI.callAddItem(historyItem); @@ -348,7 +348,7 @@ export class CodexOAuthProvider implements OAuthProvider { type: 'info', text: 'Successfully authenticated with Codex!', }; - if (addItem) { + if (addItem != null) { addItem(successMessage); } else { process.stdout.write('Successfully authenticated with Codex!\n'); @@ -385,7 +385,7 @@ export class CodexOAuthProvider implements OAuthProvider { text: `Enter this code in your browser:\n\n ${userCode}\n\n(Code expires in 15 minutes)`, }; - if (this.addItem) { + if (this.addItem != null) { this.addItem(urlHistoryItem); this.addItem(deviceCodeItem); return this.addItem; @@ -421,7 +421,7 @@ export class CodexOAuthProvider implements OAuthProvider { ): Promise { try { await ClipboardService.copyToClipboard(userCode); - if (addItem) { + if (addItem != null) { addItem( { type: 'info', text: 'Code copied to clipboard!' }, Date.now(), @@ -447,7 +447,7 @@ export class CodexOAuthProvider implements OAuthProvider { this.logger.debug(() => '[FLOW] Reading token from tokenStore...'); const token = await this.tokenStore.getToken('codex'); - if (!token) { + if (token == null) { this.logger.debug( () => '[FLOW] No token found in tokenStore, returning null', ); @@ -531,7 +531,7 @@ export class CodexOAuthProvider implements OAuthProvider { */ async logout(_token?: OAuthToken): Promise { this.logger.debug(() => 'Logging out from Codex'); - if (!_token) { + if (_token == null) { await this.tokenStore.removeToken('codex'); } } diff --git a/packages/cli/src/auth/gemini-oauth-provider.ts b/packages/cli/src/auth/gemini-oauth-provider.ts index 3bc4bd4942..aafc818cf2 100644 --- a/packages/cli/src/auth/gemini-oauth-provider.ts +++ b/packages/cli/src/auth/gemini-oauth-provider.ts @@ -11,8 +11,7 @@ * while maintaining compatibility with the LOGIN_WITH_GOOGLE flow. */ -import type { OAuthProvider } from './types.js'; -import { OAuthToken, TokenStore } from './types.js'; +import type { OAuthProvider, OAuthToken, TokenStore } from './types.js'; import { clearOauthClientCache, OAuthErrorFactory, @@ -25,8 +24,8 @@ import { import { promises as fs } from 'node:fs'; import path from 'node:path'; import os from 'node:os'; -import { Credentials } from 'google-auth-library'; -import { HistoryItemWithoutId } from '../ui/types.js'; +import type { Credentials } from 'google-auth-library'; +import type { HistoryItemWithoutId } from '../ui/types.js'; import { globalOAuthUI } from './global-oauth-ui.js'; import { InitializationGuard, AuthCodeDialog } from './oauth-provider-base.js'; @@ -59,7 +58,7 @@ export class GeminiOAuthProvider implements OAuthProvider { this.initGuard = new InitializationGuard('wrap', this.name); this.dialog = new AuthCodeDialog(); - if (!tokenStore) { + if (tokenStore == null) { debugLogger.warn( `DEPRECATION: ${this.name} OAuth provider created without TokenStore. ` + `Token persistence will not work. Please update your code.`, @@ -109,7 +108,7 @@ export class GeminiOAuthProvider implements OAuthProvider { } async initializeToken(): Promise { - if (!this.tokenStore) { + if (this.tokenStore == null) { return; } @@ -118,12 +117,12 @@ export class GeminiOAuthProvider implements OAuthProvider { // Try to load from new location first let savedToken = await this.tokenStore!.getToken('gemini'); - if (!savedToken) { + if (savedToken == null) { // Try to migrate from legacy locations savedToken = await this.migrateFromLegacyTokens(); } - if (savedToken) { + if (savedToken != null) { this.currentToken = savedToken; } }, @@ -180,7 +179,7 @@ export class GeminiOAuthProvider implements OAuthProvider { return await getOauthClient(config); } catch (error) { if (error instanceof Error) { - if (this.addItem) { + if (this.addItem != null) { this.addItem( { type: 'error', @@ -225,7 +224,7 @@ export class GeminiOAuthProvider implements OAuthProvider { private showGeminiFallbackInstructions(): void { const fallbackMessage = `Browser authentication was cancelled or failed.\nFallback options:\n1. Use API key: /keyfile \n2. Set environment: export GEMINI_API_KEY=\n3. Try OAuth again: /auth gemini enable`; - if (this.addItem) { + if (this.addItem != null) { this.addItem({ type: 'info', text: fallbackMessage }, Date.now()); } else { const delivered = globalOAuthUI.callAddItem( @@ -257,13 +256,13 @@ export class GeminiOAuthProvider implements OAuthProvider { } const token = this.credentialsToOAuthToken(credentials); - if (!token) { + if (token == null) { throw OAuthErrorFactory.authenticationRequired(this.name, { reason: 'Failed to convert credentials to OAuthToken', }); } - if (this.tokenStore) { + if (this.tokenStore != null) { try { await this.tokenStore.saveToken('gemini', token); } catch (saveError) { @@ -277,7 +276,7 @@ export class GeminiOAuthProvider implements OAuthProvider { this.currentToken = token; - if (this.addItem) { + if (this.addItem != null) { this.addItem( { type: 'info', @@ -308,7 +307,7 @@ export class GeminiOAuthProvider implements OAuthProvider { async () => { // Read-only - no refresh, no migration writes // Try to get from current token first - if (this.currentToken) { + if (this.currentToken != null) { return this.currentToken; } @@ -316,7 +315,7 @@ export class GeminiOAuthProvider implements OAuthProvider { const token = await this.getTokenFromGoogleOAuth(); // Update in-memory cache (but DO NOT persist) - if (token) { + if (token != null) { this.currentToken = token; } @@ -352,7 +351,7 @@ export class GeminiOAuthProvider implements OAuthProvider { this.currentToken = null; // Remove from new token storage location - THIS MUST SUCCEED - if (this.tokenStore) { + if (this.tokenStore != null) { await this.tokenStore.removeToken('gemini'); } @@ -431,7 +430,7 @@ export class GeminiOAuthProvider implements OAuthProvider { // Try to get token from existing Google OAuth const token = await this.getTokenFromGoogleOAuth(); - if (token) { + if (token != null) { this.logger.debug( () => 'Found Gemini token in legacy location (read-only)', ); diff --git a/packages/cli/src/auth/global-oauth-ui.ts b/packages/cli/src/auth/global-oauth-ui.ts index a61a8a1f79..4990111354 100644 --- a/packages/cli/src/auth/global-oauth-ui.ts +++ b/packages/cli/src/auth/global-oauth-ui.ts @@ -79,7 +79,7 @@ class GlobalOAuthUI { itemData: Omit, baseTimestamp?: number, ): number | undefined { - if (this.addItemCallback) { + if (this.addItemCallback != null) { return this.addItemCallback(itemData, baseTimestamp); } if (this.pendingItems.length >= MAX_PENDING_ITEMS) { diff --git a/packages/cli/src/auth/local-oauth-callback.spec.ts b/packages/cli/src/auth/local-oauth-callback.spec.ts index 809da2d83c..ce0ba2ea7e 100644 --- a/packages/cli/src/auth/local-oauth-callback.spec.ts +++ b/packages/cli/src/auth/local-oauth-callback.spec.ts @@ -15,7 +15,7 @@ const findAvailablePort = async (): Promise => if (address && typeof address === 'object') { const port = address.port; server.close((closeError) => { - if (closeError) { + if (closeError != null) { reject(closeError); return; } diff --git a/packages/cli/src/auth/local-oauth-callback.ts b/packages/cli/src/auth/local-oauth-callback.ts index d6ff7afa6b..0c225920d0 100644 --- a/packages/cli/src/auth/local-oauth-callback.ts +++ b/packages/cli/src/auth/local-oauth-callback.ts @@ -699,15 +699,15 @@ const createCallbackServer = async ( let rejectHandler: ((error: Error) => void) | null = null; const settle = (result: { code: string; state: string } | Error) => { - if (settled) { + if (settled != null) { return; } settled = result; if (result instanceof Error) { - if (rejectHandler) { + if (rejectHandler != null) { rejectHandler(result); } - } else if (resolveHandler) { + } else if (resolveHandler != null) { resolveHandler(result); } resolveHandler = null; @@ -719,7 +719,7 @@ const createCallbackServer = async ( return; } closed = true; - if (timeout) { + if (timeout != null) { clearTimeout(timeout); timeout = null; } @@ -770,7 +770,7 @@ const createCallbackServer = async ( if (settled instanceof Error) { return Promise.reject(settled); } - if (settled && !(settled instanceof Error)) { + if (settled != null && !(settled instanceof Error)) { return Promise.resolve(settled); } return new Promise<{ code: string; state: string }>((resolve, reject) => { diff --git a/packages/cli/src/auth/migration.ts b/packages/cli/src/auth/migration.ts index c9547b9e58..cf30bc8444 100644 --- a/packages/cli/src/auth/migration.ts +++ b/packages/cli/src/auth/migration.ts @@ -14,7 +14,7 @@ */ import type { OAuthProvider } from './types.js'; -import { TokenStore, debugLogger } from '@vybestack/llxprt-code-core'; +import { type TokenStore, debugLogger } from '@vybestack/llxprt-code-core'; /** * Migrate any in-memory tokens to persistent storage @@ -30,9 +30,9 @@ export async function migrateInMemoryTokens( try { // Check for in-memory token const token = await provider.getToken(); - if (token) { + if (token != null) { const stored = await tokenStore.getToken(name); - if (!stored) { + if (stored == null) { // Migrate to storage await tokenStore.saveToken(name, token); debugLogger.log(`Migrated ${name} token to persistent storage`); diff --git a/packages/cli/src/auth/oauth-manager.auth-lock.spec.ts b/packages/cli/src/auth/oauth-manager.auth-lock.spec.ts index a8578827c4..3f7d5738ed 100644 --- a/packages/cli/src/auth/oauth-manager.auth-lock.spec.ts +++ b/packages/cli/src/auth/oauth-manager.auth-lock.spec.ts @@ -53,7 +53,7 @@ describe('OAuthManager auth lock and TOCTOU defense (Issue #1652)', () => { let getTokenCallCount = 0; const tokenStore: TokenStore = { saveToken: vi.fn(), - getToken: vi.fn(async (provider: string, bucket?: string) => { + getToken: vi.fn(async (_provider: string, bucket?: string) => { getTokenCallCount++; // Simulate cross-process write: // First call (upfront check for default): returns null (unauthenticated) @@ -136,7 +136,7 @@ describe('OAuthManager auth lock and TOCTOU defense (Issue #1652)', () => { it('Test 5.2: should filter out already-authenticated buckets in upfront check', async () => { const tokenStore: TokenStore = { saveToken: vi.fn(), - getToken: vi.fn(async (provider: string, bucket?: string) => { + getToken: vi.fn(async (_provider: string, bucket?: string) => { if (bucket === 'default') { return makeToken('existing-default-token'); // Already authenticated } @@ -416,7 +416,7 @@ describe('OAuthManager auth lock and TOCTOU defense (Issue #1652)', () => { const tokenStore: TokenStore = { saveToken: vi.fn(), - getToken: vi.fn(async (provider: string, bucket?: string) => { + getToken: vi.fn(async (_provider: string, bucket?: string) => { getTokenCallCount++; // Upfront check calls: bucket1, bucket2, bucket3 (all return null) if (getTokenCallCount <= 3) { @@ -458,7 +458,7 @@ describe('OAuthManager auth lock and TOCTOU defense (Issue #1652)', () => { const authenticateSpy = vi .spyOn(oauthManager, 'authenticate') - .mockImplementation(async (providerName: string, bucket?: string) => { + .mockImplementation(async (_providerName: string, bucket?: string) => { authenticateCallLog.push(bucket ?? 'default'); }); @@ -766,7 +766,7 @@ describe('OAuthManager auth lock and TOCTOU defense (Issue #1652)', () => { it('should skip all buckets if already authenticated', async () => { const tokenStore: TokenStore = { saveToken: vi.fn(), - getToken: vi.fn(async (provider: string, bucket?: string) => + getToken: vi.fn(async (_provider: string, bucket?: string) => makeToken(`existing-${bucket}`), ), removeToken: vi.fn(), diff --git a/packages/cli/src/auth/oauth-manager.issue1317.spec.ts b/packages/cli/src/auth/oauth-manager.issue1317.spec.ts index 5bb2f50054..78a35ab843 100644 --- a/packages/cli/src/auth/oauth-manager.issue1317.spec.ts +++ b/packages/cli/src/auth/oauth-manager.issue1317.spec.ts @@ -17,8 +17,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { OAuthManager } from './oauth-manager.js'; -import type { OAuthProvider } from './types.js'; -import { OAuthToken, TokenStore } from './types.js'; +import type { OAuthProvider, OAuthToken, TokenStore } from './types.js'; import { LoadedSettings } from '../config/settings.js'; import type { Settings } from '../config/settings.js'; import { diff --git a/packages/cli/src/auth/oauth-manager.issue1468.spec.ts b/packages/cli/src/auth/oauth-manager.issue1468.spec.ts index 792d59a4d9..7d7dd6ed5c 100644 --- a/packages/cli/src/auth/oauth-manager.issue1468.spec.ts +++ b/packages/cli/src/auth/oauth-manager.issue1468.spec.ts @@ -56,8 +56,7 @@ vi.mock('../runtime/runtimeSettings.js', () => ({ })); import { OAuthManager } from './oauth-manager.js'; -import type { OAuthProvider } from './types.js'; -import type { OAuthToken, TokenStore } from './types.js'; +import type { OAuthProvider, OAuthToken, TokenStore } from './types.js'; import { LoadedSettings } from '../config/settings.js'; import type { Settings } from '../config/settings.js'; import type { OAuthTokenRequestMetadata } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/auth/oauth-manager.refresh-race.spec.ts b/packages/cli/src/auth/oauth-manager.refresh-race.spec.ts index cddabafa8d..0a6874aacb 100644 --- a/packages/cli/src/auth/oauth-manager.refresh-race.spec.ts +++ b/packages/cli/src/auth/oauth-manager.refresh-race.spec.ts @@ -11,8 +11,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { OAuthManager } from './oauth-manager.js'; -import type { OAuthProvider } from './types.js'; -import type { OAuthToken, TokenStore } from './types.js'; +import type { OAuthProvider, OAuthToken, TokenStore } from './types.js'; import { promises as fs } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; diff --git a/packages/cli/src/auth/oauth-manager.runtime-messagebus.spec.ts b/packages/cli/src/auth/oauth-manager.runtime-messagebus.spec.ts index 9bc21e6459..f448a6598c 100644 --- a/packages/cli/src/auth/oauth-manager.runtime-messagebus.spec.ts +++ b/packages/cli/src/auth/oauth-manager.runtime-messagebus.spec.ts @@ -7,8 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { MessageBus, PolicyEngine } from '@vybestack/llxprt-code-core'; import { OAuthManager } from './oauth-manager.js'; -import type { OAuthProvider } from './types.js'; -import type { OAuthToken, TokenStore } from './types.js'; +import type { OAuthProvider, OAuthToken, TokenStore } from './types.js'; const mockEphemeralSettings = new Map(); diff --git a/packages/cli/src/auth/oauth-manager.spec.ts b/packages/cli/src/auth/oauth-manager.spec.ts index c579cd3194..9d495c2ff3 100644 --- a/packages/cli/src/auth/oauth-manager.spec.ts +++ b/packages/cli/src/auth/oauth-manager.spec.ts @@ -6,8 +6,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { OAuthManager } from './oauth-manager.js'; -import type { OAuthProvider } from './types.js'; -import { OAuthToken, TokenStore } from './types.js'; +import type { OAuthProvider, OAuthToken, TokenStore } from './types.js'; import { LoadedSettings } from '../config/settings.js'; import type { Settings } from '../config/settings.js'; import { @@ -39,7 +38,7 @@ class MockOAuthProvider implements OAuthProvider { async initiateAuth(): Promise { this.authInitiated = true; // Simulate successful auth flow - if (!this.token) { + if (this.token == null) { this.token = { access_token: `access_${this.name}_${Date.now()}`, refresh_token: `refresh_${this.name}_${Date.now()}`, diff --git a/packages/cli/src/auth/oauth-manager.token-reuse.spec.ts b/packages/cli/src/auth/oauth-manager.token-reuse.spec.ts index 317e274549..c7f73bcea4 100644 --- a/packages/cli/src/auth/oauth-manager.token-reuse.spec.ts +++ b/packages/cli/src/auth/oauth-manager.token-reuse.spec.ts @@ -19,8 +19,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { OAuthManager } from './oauth-manager.js'; -import type { OAuthProvider } from './types.js'; -import { OAuthToken, TokenStore } from './types.js'; +import type { OAuthProvider, OAuthToken, TokenStore } from './types.js'; import { LoadedSettings } from '../config/settings.js'; import type { Settings } from '../config/settings.js'; import { diff --git a/packages/cli/src/auth/oauth-manager.ts b/packages/cli/src/auth/oauth-manager.ts index cd74ddc005..816059b157 100644 --- a/packages/cli/src/auth/oauth-manager.ts +++ b/packages/cli/src/auth/oauth-manager.ts @@ -4,18 +4,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { OAuthToken, AuthStatus, TokenStore } from './types.js'; import type { + OAuthToken, + AuthStatus, + TokenStore, OAuthProvider, OAuthManagerRuntimeMessageBusDeps, BucketFailoverOAuthManagerLike, } from './types.js'; -import { LoadedSettings } from '../config/settings.js'; +import type { LoadedSettings } from '../config/settings.js'; import { ProviderRegistry } from './provider-registry.js'; -import { +import type { MessageBus, Config, - type OAuthTokenRequestMetadata, + OAuthTokenRequestMetadata, } from '@vybestack/llxprt-code-core'; import { ProactiveRenewalManager } from './proactive-renewal-manager.js'; import { OAuthBucketManager } from './OAuthBucketManager.js'; @@ -387,7 +389,7 @@ export class OAuthManager implements BucketFailoverOAuthManagerLike { bucket?: string, ): Promise | null> { const provider = this.providerRegistry.getProvider('anthropic'); - if (!provider) { + if (provider == null) { return null; } diff --git a/packages/cli/src/auth/oauth-provider-base.ts b/packages/cli/src/auth/oauth-provider-base.ts index 07c2eb2d66..4f38920415 100644 --- a/packages/cli/src/auth/oauth-provider-base.ts +++ b/packages/cli/src/auth/oauth-provider-base.ts @@ -68,7 +68,7 @@ export class InitializationGuard { } // Wait for completion — handles concurrent callers sharing the same promise - if (this.promise) { + if (this.promise != null) { try { await this.promise; this.state = InitializationState.Completed; @@ -120,7 +120,7 @@ export class AuthCodeDialog { /** Resolve the pending auth-code promise with the given code. */ submitAuthCode(code: string): void { - if (this.resolver) { + if (this.resolver != null) { this.resolver(code); this.resolver = undefined; this.rejecter = undefined; @@ -134,7 +134,7 @@ export class AuthCodeDialog { rejectWithError(error: Error): void { (global as unknown as { __oauth_needs_code: boolean }).__oauth_needs_code = false; - if (this.rejecter) { + if (this.rejecter != null) { this.rejecter(error); this.resolver = undefined; this.rejecter = undefined; diff --git a/packages/cli/src/auth/proactive-renewal-manager.ts b/packages/cli/src/auth/proactive-renewal-manager.ts index 37145c76d9..6dc811bd5c 100644 --- a/packages/cli/src/auth/proactive-renewal-manager.ts +++ b/packages/cli/src/auth/proactive-renewal-manager.ts @@ -15,8 +15,7 @@ import { mergeRefreshedToken, type OAuthTokenWithExtras, } from '@vybestack/llxprt-code-core'; -import type { OAuthToken, TokenStore } from './types.js'; -import type { OAuthProvider } from './types.js'; +import type { OAuthToken, TokenStore, OAuthProvider } from './types.js'; import { createProfileManager, isLoadBalancerProfileLike, @@ -59,7 +58,7 @@ export class ProactiveRenewalManager { clearProactiveRenewal(key: string): void { const entry = this.proactiveRenewals.get(key); - if (entry) { + if (entry != null) { clearTimeout(entry.timer); this.proactiveRenewals.delete(key); } @@ -76,7 +75,7 @@ export class ProactiveRenewalManager { ): void { const key = this.getProactiveRenewalKey(providerName, bucket); const existing = this.proactiveRenewals.get(key); - if (existing) { + if (existing != null) { clearTimeout(existing.timer); } @@ -183,7 +182,7 @@ export class ProactiveRenewalManager { const delayMs = Math.floor(Math.max(0, (refreshAtSec - nowSec) * 1000)); const existing = this.proactiveRenewals.get(key); - if (existing && existing.expiry === token.expiry) { + if (existing != null && existing.expiry === token.expiry) { return; } @@ -224,7 +223,7 @@ export class ProactiveRenewalManager { } const provider = this.getProvider(providerName); - if (!provider) { + if (provider == null) { // Provider might not be registered in this runtime; keep the timer but back off. this.scheduleProactiveRetry(providerName, normalizedBucket); return; @@ -307,7 +306,7 @@ export class ProactiveRenewalManager { normalizedBucket, ); - if (!currentToken || !currentToken.refresh_token) { + if (!currentToken?.refresh_token) { this.clearProactiveRenewal(key); return; } @@ -329,7 +328,7 @@ export class ProactiveRenewalManager { } const refreshedToken = await provider.refreshToken(currentToken); - if (!refreshedToken) { + if (refreshedToken == null) { // @plan PLAN-20260223-ISSUE1598.P14 // @requirement REQ-1598-PR04, REQ-1598-PR05 this.scheduleProactiveRetry(providerName, normalizedBucket); @@ -360,7 +359,7 @@ export class ProactiveRenewalManager { currentToken: OAuthToken, ): boolean { const scheduled = this.proactiveRenewalTokens.get(key); - if (scheduled) { + if (scheduled != null) { return ( currentToken.access_token !== scheduled.accessToken || (currentToken.refresh_token ?? '') !== scheduled.refreshToken @@ -385,7 +384,7 @@ export class ProactiveRenewalManager { const targets: Array<{ providerName: string; bucket: string }> = []; const direct = getOAuthBucketsFromProfile(profile); - if (direct) { + if (direct != null) { for (const bucket of direct.buckets) { targets.push({ providerName: direct.providerName, bucket }); } @@ -413,7 +412,7 @@ export class ProactiveRenewalManager { const bucket = this.normalizeBucket(target.bucket); const token = await this.tokenStore.getToken(target.providerName, bucket); - if (!token) { + if (token == null) { continue; } this.scheduleProactiveRenewal(target.providerName, bucket, token); @@ -449,7 +448,7 @@ export class ProactiveRenewalManager { return; } const oauth = getOAuthBucketsFromProfile(loaded); - if (oauth) { + if (oauth != null) { for (const bucket of oauth.buckets) { targets.push({ providerName: oauth.providerName, bucket }); } diff --git a/packages/cli/src/auth/profile-utils.ts b/packages/cli/src/auth/profile-utils.ts index 3f68afe33b..560c3978d0 100644 --- a/packages/cli/src/auth/profile-utils.ts +++ b/packages/cli/src/auth/profile-utils.ts @@ -27,7 +27,7 @@ let profileManagerCtorPromise: Promise | undefined; * Caches the promise to avoid repeated imports. */ async function getProfileManagerCtor(): Promise { - if (!profileManagerCtorPromise) { + if (profileManagerCtorPromise == null) { profileManagerCtorPromise = import('@vybestack/llxprt-code-core') .then((mod) => mod.ProfileManager) .catch((error) => { diff --git a/packages/cli/src/auth/provider-registry.ts b/packages/cli/src/auth/provider-registry.ts index c2a0598a4c..ba2cd89057 100644 --- a/packages/cli/src/auth/provider-registry.ts +++ b/packages/cli/src/auth/provider-registry.ts @@ -6,7 +6,7 @@ import { DebugLogger } from '@vybestack/llxprt-code-core'; import type { OAuthProvider } from './types.js'; -import { LoadedSettings, SettingScope } from '../config/settings.js'; +import { type LoadedSettings, SettingScope } from '../config/settings.js'; const logger = new DebugLogger('llxprt:oauth:registry'); @@ -79,7 +79,7 @@ export class ProviderRegistry { } const provider = this.providers.get(providerName); - if (!provider) { + if (provider == null) { throw new Error(`Unknown provider: ${providerName}`); } @@ -106,7 +106,7 @@ export class ProviderRegistry { return this.inMemoryOAuthState.get(providerName) ?? false; } - if (this.settings) { + if (this.settings != null) { // Check settings if available const oauthEnabledProviders = this.settings.merged.oauthEnabledProviders || {}; @@ -127,7 +127,7 @@ export class ProviderRegistry { // Always update in-memory state for precedence this.inMemoryOAuthState.set(providerName, enabled); - if (this.settings) { + if (this.settings != null) { const oauthEnabledProviders = { ...(this.settings.merged.oauthEnabledProviders || {}), }; diff --git a/packages/cli/src/auth/provider-usage-info.ts b/packages/cli/src/auth/provider-usage-info.ts index 7d8226a548..012c212df8 100644 --- a/packages/cli/src/auth/provider-usage-info.ts +++ b/packages/cli/src/auth/provider-usage-info.ts @@ -36,7 +36,7 @@ export async function getAnthropicUsageInfo( const bucketToUse = bucket ?? 'default'; const token = await tokenStore.getToken('anthropic', bucketToUse); - if (!token) { + if (token == null) { return null; } @@ -71,7 +71,7 @@ export async function getAllAnthropicUsageInfo( for (const bucket of bucketsToCheck) { const token = await tokenStore.getToken('anthropic', bucket); - if (!token) { + if (token == null) { continue; } @@ -86,7 +86,7 @@ export async function getAllAnthropicUsageInfo( try { const usageInfo = await fetchAnthropicUsage(token.access_token); - if (usageInfo) { + if (usageInfo != null) { result.set(bucket, usageInfo); } } catch (error) { @@ -121,7 +121,7 @@ export async function getAllCodexUsageInfo( for (const bucket of bucketsToCheck) { const token = await tokenStore.getToken('codex', bucket); - if (!token) { + if (token == null) { continue; } @@ -154,7 +154,7 @@ export async function getAllCodexUsageInfo( accountId, codexBaseUrl, ); - if (usageInfo) { + if (usageInfo != null) { result.set(bucket, usageInfo); } } catch (error) { @@ -187,7 +187,7 @@ export async function getAllGeminiUsageInfo( for (const bucket of bucketsToCheck) { const token = await tokenStore.getToken('gemini', bucket); - if (!token) { + if (token == null) { continue; } @@ -198,7 +198,7 @@ export async function getAllGeminiUsageInfo( try { const quotaInfo = await fetchGeminiQuota(token.access_token); - if (quotaInfo) { + if (quotaInfo != null) { result.set(bucket, quotaInfo as unknown as Record); } } catch (error) { @@ -221,7 +221,7 @@ export async function getHigherPriorityAuth( providerName: string, settings: LoadedSettings | undefined, ): Promise { - if (!settings) { + if (settings == null) { return null; } @@ -236,11 +236,11 @@ export async function getHigherPriorityAuth( // SettingsService not registered (subagent/test context) — skip authOnly check } - if (merged.providerApiKeys && merged.providerApiKeys[providerName]) { + if (merged.providerApiKeys?.[providerName]) { return 'API Key'; } - if (merged.providerKeyfiles && merged.providerKeyfiles[providerName]) { + if (merged.providerKeyfiles?.[providerName]) { return 'Keyfile'; } diff --git a/packages/cli/src/auth/proxy/__tests__/credential-proxy-server.test.ts b/packages/cli/src/auth/proxy/__tests__/credential-proxy-server.test.ts index 0f33f6d9bc..fff3bdb91b 100644 --- a/packages/cli/src/auth/proxy/__tests__/credential-proxy-server.test.ts +++ b/packages/cli/src/auth/proxy/__tests__/credential-proxy-server.test.ts @@ -209,7 +209,7 @@ describe('CredentialProxyServer', () => { server = createServer(); const socketPath = await server.start(); - expect(socketPath).toEqual(expect.any(String)); + expect(socketPath).toStrictEqual(expect.any(String)); expect(socketPath.endsWith('.sock')).toBe(true); const stat = fs.statSync(socketPath); expect(stat.isSocket()).toBe(true); diff --git a/packages/cli/src/auth/proxy/__tests__/deprecation-guard.test.ts b/packages/cli/src/auth/proxy/__tests__/deprecation-guard.test.ts index f11831f410..2c63c08b38 100644 --- a/packages/cli/src/auth/proxy/__tests__/deprecation-guard.test.ts +++ b/packages/cli/src/auth/proxy/__tests__/deprecation-guard.test.ts @@ -94,7 +94,7 @@ describe('Deprecation Guards (P36)', () => { }); } - expect(violations).toEqual([]); + expect(violations).toStrictEqual([]); }); }); @@ -121,7 +121,7 @@ describe('Deprecation Guards (P36)', () => { }); } - expect(violations).toEqual([]); + expect(violations).toStrictEqual([]); }); }); @@ -167,7 +167,7 @@ describe('Deprecation Guards (P36)', () => { }); } - expect(matches).toEqual([]); + expect(matches).toStrictEqual([]); }); }); diff --git a/packages/cli/src/auth/proxy/__tests__/e2e-credential-flow.test.ts b/packages/cli/src/auth/proxy/__tests__/e2e-credential-flow.test.ts index b9273af230..f1e4560f1e 100644 --- a/packages/cli/src/auth/proxy/__tests__/e2e-credential-flow.test.ts +++ b/packages/cli/src/auth/proxy/__tests__/e2e-credential-flow.test.ts @@ -324,7 +324,7 @@ describe('E2E Credential Flow (Phase 37)', () => { refreshCalled = true; // Simulate refresh by updating token const current = await tokenStore.getToken(provider, bucket); - if (current) { + if (current != null) { await tokenStore.saveToken( provider, { diff --git a/packages/cli/src/auth/proxy/__tests__/migration-completeness.test.ts b/packages/cli/src/auth/proxy/__tests__/migration-completeness.test.ts index 4c89204eef..278f0f12a4 100644 --- a/packages/cli/src/auth/proxy/__tests__/migration-completeness.test.ts +++ b/packages/cli/src/auth/proxy/__tests__/migration-completeness.test.ts @@ -201,8 +201,8 @@ describe('Migration Completeness (P35)', () => { mergeRefreshedToken(current, next); // Originals should be unchanged - expect(current).toEqual(currentCopy); - expect(next).toEqual(nextCopy); + expect(current).toStrictEqual(currentCopy); + expect(next).toStrictEqual(nextCopy); }); }); }); diff --git a/packages/cli/src/auth/proxy/__tests__/oauth-exchange.spec.ts b/packages/cli/src/auth/proxy/__tests__/oauth-exchange.spec.ts index b0616151d5..d33119fd50 100644 --- a/packages/cli/src/auth/proxy/__tests__/oauth-exchange.spec.ts +++ b/packages/cli/src/auth/proxy/__tests__/oauth-exchange.spec.ts @@ -155,7 +155,7 @@ class TestOAuthFlow implements OAuthFlowInterface { this.lastExchangeCode = code; this.lastExchangeState = state ?? null; - if (!this.exchangeResult) { + if (this.exchangeResult == null) { throw new Error('TestOAuthFlow: exchangeResult not configured'); } return this.exchangeResult; diff --git a/packages/cli/src/auth/proxy/__tests__/oauth-initiate.spec.ts b/packages/cli/src/auth/proxy/__tests__/oauth-initiate.spec.ts index bf7cd7e473..2b77aa984b 100644 --- a/packages/cli/src/auth/proxy/__tests__/oauth-initiate.spec.ts +++ b/packages/cli/src/auth/proxy/__tests__/oauth-initiate.spec.ts @@ -129,7 +129,7 @@ class TestOAuthFlow { } async initiateDeviceFlow(_redirectUri?: string): Promise { - if (!this.initiateResult) { + if (this.initiateResult == null) { throw new Error('TestOAuthFlow: initiateResult not configured'); } return this.initiateResult; diff --git a/packages/cli/src/auth/proxy/__tests__/oauth-poll.spec.ts b/packages/cli/src/auth/proxy/__tests__/oauth-poll.spec.ts index bae53c0b61..17b2401e63 100644 --- a/packages/cli/src/auth/proxy/__tests__/oauth-poll.spec.ts +++ b/packages/cli/src/auth/proxy/__tests__/oauth-poll.spec.ts @@ -196,7 +196,7 @@ class TestDeviceCodeFlow implements OAuthFlowInterface { this.pollResults[Math.min(this.pollIndex, this.pollResults.length - 1)]; this.pollIndex++; - if (result.token) { + if (result.token != null) { return result.token; } diff --git a/packages/cli/src/auth/proxy/__tests__/oauth-session-manager.test.ts b/packages/cli/src/auth/proxy/__tests__/oauth-session-manager.test.ts index b1401b7dde..f679be3e53 100644 --- a/packages/cli/src/auth/proxy/__tests__/oauth-session-manager.test.ts +++ b/packages/cli/src/auth/proxy/__tests__/oauth-session-manager.test.ts @@ -91,7 +91,7 @@ describe('PKCESessionStore', () => { expect(session.bucket).toBe('default'); expect(session.flowType).toBe('pkce_redirect'); expect(session.flowInstance).toBe(flowInstance); - expect(session.peerIdentity).toEqual(peerA); + expect(session.peerIdentity).toStrictEqual(peerA); expect(session.createdAt).toBe(baseNow); expect(session.used).toBe(false); }); diff --git a/packages/cli/src/auth/proxy/__tests__/platform-matrix.test.ts b/packages/cli/src/auth/proxy/__tests__/platform-matrix.test.ts index 0d23117d68..4a248a7bb2 100644 --- a/packages/cli/src/auth/proxy/__tests__/platform-matrix.test.ts +++ b/packages/cli/src/auth/proxy/__tests__/platform-matrix.test.ts @@ -133,7 +133,7 @@ describe('Platform Matrix Tests (Phase 38)', () => { }); afterEach(async () => { - if (server) { + if (server != null) { try { await server.stop(); } catch { diff --git a/packages/cli/src/auth/proxy/__tests__/platform-uds-probe.test.ts b/packages/cli/src/auth/proxy/__tests__/platform-uds-probe.test.ts index 33e0c9da07..27bf63b80f 100644 --- a/packages/cli/src/auth/proxy/__tests__/platform-uds-probe.test.ts +++ b/packages/cli/src/auth/proxy/__tests__/platform-uds-probe.test.ts @@ -41,7 +41,7 @@ describe('Platform UDS Probe Tests (Phase 38)', () => { }); afterEach(() => { - if (server) { + if (server != null) { server.close(); server = null; } @@ -519,7 +519,7 @@ describe('Platform UDS Probe Tests (Phase 38)', () => { >; expect( (receivedPayload.nested as Record).data, - ).toEqual([1, 2, 3]); + ).toStrictEqual([1, 2, 3]); expect(receivedPayload.unicode).toBe('日本語テスト'); clientSocket.destroy(); diff --git a/packages/cli/src/auth/proxy/__tests__/proactive-scheduler.test.ts b/packages/cli/src/auth/proxy/__tests__/proactive-scheduler.test.ts index 39d8101d00..6783a69ee5 100644 --- a/packages/cli/src/auth/proxy/__tests__/proactive-scheduler.test.ts +++ b/packages/cli/src/auth/proxy/__tests__/proactive-scheduler.test.ts @@ -387,10 +387,10 @@ describe('ProactiveScheduler', () => { expect(refreshFn.calls.length).toBe(3); const providers = refreshFn.calls.map((c) => c.provider).sort(); - expect(providers).toEqual(['anthropic', 'gemini', 'openai']); + expect(providers).toStrictEqual(['anthropic', 'gemini', 'openai']); const buckets = refreshFn.calls.map((c) => c.bucket).sort(); - expect(buckets).toEqual(['default', 'prod', 'staging']); + expect(buckets).toStrictEqual(['default', 'prod', 'staging']); }); }); }); diff --git a/packages/cli/src/auth/proxy/__tests__/proxy-oauth-adapter.test.ts b/packages/cli/src/auth/proxy/__tests__/proxy-oauth-adapter.test.ts index c972438450..3d5291fded 100644 --- a/packages/cli/src/auth/proxy/__tests__/proxy-oauth-adapter.test.ts +++ b/packages/cli/src/auth/proxy/__tests__/proxy-oauth-adapter.test.ts @@ -6,7 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { EventEmitter } from 'node:events'; -import { ProxySocketClient } from '@vybestack/llxprt-code-core'; +import type { ProxySocketClient } from '@vybestack/llxprt-code-core'; import { ProxyOAuthAdapter } from '../proxy-oauth-adapter.js'; describe('ProxyOAuthAdapter', () => { @@ -82,7 +82,7 @@ describe('ProxyOAuthAdapter', () => { const result = await adapter.login('anthropic'); - expect(result).toEqual({ + expect(result).toStrictEqual({ status: 'complete', access_token: 'token', }); diff --git a/packages/cli/src/auth/proxy/__tests__/refresh-flow.spec.ts b/packages/cli/src/auth/proxy/__tests__/refresh-flow.spec.ts index 234edb417a..7cea1bc795 100644 --- a/packages/cli/src/auth/proxy/__tests__/refresh-flow.spec.ts +++ b/packages/cli/src/auth/proxy/__tests__/refresh-flow.spec.ts @@ -17,10 +17,8 @@ import type { TokenStore, OAuthToken, BucketStats, -} from '@vybestack/llxprt-code-core'; -import { ProxySocketClient, - ProviderKeyStorage, + type ProviderKeyStorage, } from '@vybestack/llxprt-code-core'; import { CredentialProxyServer, @@ -174,11 +172,11 @@ class TestOAuthProvider implements OAuthFlowInterface { await new Promise((resolve) => setTimeout(resolve, this.refreshDelayMs)); } - if (this.shouldThrow && this.throwError) { + if (this.shouldThrow && this.throwError != null) { throw this.throwError; } - if (!this.refreshResult) { + if (this.refreshResult == null) { throw new Error('TestOAuthProvider: refreshResult not configured'); } diff --git a/packages/cli/src/auth/proxy/credential-proxy-server.ts b/packages/cli/src/auth/proxy/credential-proxy-server.ts index ab0ddb43b8..a320cdf086 100644 --- a/packages/cli/src/auth/proxy/credential-proxy-server.ts +++ b/packages/cli/src/auth/proxy/credential-proxy-server.ts @@ -85,11 +85,11 @@ export class CredentialProxyServer { tokenStore: options.tokenStore, refreshFn: async (provider, currentToken) => { const flowFactory = options.flowFactories?.get(provider); - if (!flowFactory) { + if (flowFactory == null) { throw new Error(`No OAuth provider configured for: ${provider}`); } const flowInstance = flowFactory(); - if (!flowInstance.refreshToken) { + if (flowInstance.refreshToken == null) { throw new Error( `Provider ${provider} does not support token refresh`, ); @@ -219,7 +219,7 @@ export class CredentialProxyServer { const v = frame.v as number | undefined; if (v === PROTOCOL_VERSION) return true; const payload = frame.payload as Record | undefined; - if (!payload) return false; + if (payload == null) return false; const min = payload.minVersion as number | undefined; const max = payload.maxVersion as number | undefined; if (min !== undefined && max !== undefined) { @@ -264,9 +264,9 @@ export class CredentialProxyServer { const id = typeof frame.id === 'string' ? frame.id : String(frame.id ?? 'unknown'); const op = typeof frame.op === 'string' ? frame.op : undefined; - const payload = (frame.payload as Record) ?? {}; + const payload = (frame.payload ?? {}) as Record; - if (!frame.id || !op) { + if (frame.id == null || !op) { this.sendError(socket, id, 'INVALID_REQUEST', 'Missing request id or op'); return; } @@ -363,7 +363,7 @@ export class CredentialProxyServer { const provider = payload.provider as string | undefined; const tokenData = payload.token as Record | undefined; const bucket = payload.bucket as string | undefined; - if (!provider || !tokenData) { + if (!provider || tokenData == null) { this.sendError( socket, id, @@ -546,7 +546,7 @@ export class CredentialProxyServer { // Get flow factory for this provider const flowFactory = this.options.flowFactories?.get(provider); - if (!flowFactory) { + if (flowFactory == null) { this.sendError( socket, id, @@ -572,26 +572,23 @@ export class CredentialProxyServer { // Store session with flow instance for later exchange this.oauthSessions.set(sessionId, { - provider, - bucket, complete: false, createdAt: Date.now(), used: false, - flowType, - flowInstance, - // Store PKCE state for pkce_redirect flows (NOT returned to client) pkceState: flowType === 'pkce_redirect' ? initiationResult.device_code : undefined, - // Store device_code for device_code flows - needed for polling deviceCode: flowType === 'device_code' ? initiationResult.device_code : undefined, - // Store poll interval for device_code flows (may increase on slow_down) pollInterval: flowType === 'device_code' ? (initiationResult.interval ?? 5) : undefined, + provider, + bucket, + flowType, + flowInstance, }); // Build response based on flow type @@ -607,7 +604,7 @@ export class CredentialProxyServer { response.auth_url = initiationResult.verification_uri_complete ?? initiationResult.verification_uri; - } else if (flowType === 'device_code') { + } else { // For device code flows, return verification URI and user code response.auth_url = initiationResult.verification_uri; response.verification_uri = initiationResult.verification_uri; @@ -700,7 +697,7 @@ export class CredentialProxyServer { // Retrieve session const session = this.oauthSessions.get(sessionId); - if (!session) { + if (session == null) { this.sendError( socket, id, @@ -802,7 +799,7 @@ export class CredentialProxyServer { // Retrieve session const session = this.oauthSessions.get(sessionId); - if (!session) { + if (session == null) { this.sendError( socket, id, @@ -988,7 +985,7 @@ export class CredentialProxyServer { // Check if provider is configured in flowFactories (before checking token) const flowFactory = this.options.flowFactories?.get(provider); - if (!flowFactory) { + if (flowFactory == null) { this.sendError( socket, id, @@ -1003,7 +1000,7 @@ export class CredentialProxyServer { provider, bucket, ); - if (!existingToken) { + if (existingToken == null) { this.sendError( socket, id, diff --git a/packages/cli/src/auth/proxy/credential-store-factory.ts b/packages/cli/src/auth/proxy/credential-store-factory.ts index d09e4bde03..8e06643c0d 100644 --- a/packages/cli/src/auth/proxy/credential-store-factory.ts +++ b/packages/cli/src/auth/proxy/credential-store-factory.ts @@ -52,16 +52,15 @@ let directKeyStorage: ProviderKeyStorage | undefined; export function createTokenStore(): TokenStore { const socketPath = process.env.LLXPRT_CREDENTIAL_SOCKET; if (socketPath) { - if (!proxyTokenStore) { + if (proxyTokenStore == null) { proxyTokenStore = new ProxyTokenStore(socketPath); } return proxyTokenStore; - } else { - if (!directTokenStore) { - directTokenStore = new KeyringTokenStore(); - } - return directTokenStore; } + if (directTokenStore == null) { + directTokenStore = new KeyringTokenStore(); + } + return directTokenStore; } /** @@ -78,17 +77,16 @@ export function createTokenStore(): TokenStore { export function createProviderKeyStorage(): ProviderKeyStorage { const socketPath = process.env.LLXPRT_CREDENTIAL_SOCKET; if (socketPath) { - if (!proxyKeyStorage) { + if (proxyKeyStorage == null) { const client = new ProxySocketClient(socketPath); proxyKeyStorage = new ProxyProviderKeyStorage(client); } return proxyKeyStorage as unknown as ProviderKeyStorage; - } else { - if (!directKeyStorage) { - directKeyStorage = getProviderKeyStorage(); - } - return directKeyStorage; } + if (directKeyStorage == null) { + directKeyStorage = getProviderKeyStorage(); + } + return directKeyStorage; } /** diff --git a/packages/cli/src/auth/proxy/oauth-session-manager.ts b/packages/cli/src/auth/proxy/oauth-session-manager.ts index a3331579d8..0573f24031 100644 --- a/packages/cli/src/auth/proxy/oauth-session-manager.ts +++ b/packages/cli/src/auth/proxy/oauth-session-manager.ts @@ -47,7 +47,7 @@ export class PKCESessionStore { } startGC(): void { - if (this.gcInterval) { + if (this.gcInterval != null) { clearInterval(this.gcInterval); } this.gcInterval = setInterval(() => this.sweepExpired(), 60_000); @@ -72,21 +72,21 @@ export class PKCESessionStore { ): string { const sessionId = crypto.randomBytes(16).toString('hex'); this.sessions.set(sessionId, { + createdAt: Date.now(), + used: false, sessionId, provider, bucket, flowType, flowInstance, - createdAt: Date.now(), peerIdentity, - used: false, }); return sessionId; } getSession(sessionId: string, peerIdentity: unknown): OAuthSession { const session = this.sessions.get(sessionId); - if (!session) { + if (session == null) { throw new Error('SESSION_NOT_FOUND'); } @@ -113,14 +113,14 @@ export class PKCESessionStore { markUsed(sessionId: string): void { const session = this.sessions.get(sessionId); - if (session) { + if (session != null) { session.used = true; } } removeSession(sessionId: string): void { const session = this.sessions.get(sessionId); - if (session?.abortController) { + if (session?.abortController != null) { session.abortController.abort(); } this.sessions.delete(sessionId); @@ -131,7 +131,7 @@ export class PKCESessionStore { session.abortController?.abort(); } this.sessions.clear(); - if (this.gcInterval) { + if (this.gcInterval != null) { clearInterval(this.gcInterval); this.gcInterval = null; } diff --git a/packages/cli/src/auth/proxy/proactive-scheduler.ts b/packages/cli/src/auth/proxy/proactive-scheduler.ts index 0bceda56ed..08420af3f2 100644 --- a/packages/cli/src/auth/proxy/proactive-scheduler.ts +++ b/packages/cli/src/auth/proxy/proactive-scheduler.ts @@ -67,7 +67,7 @@ export class ProactiveScheduler { cancel(provider: string, bucket: string): void { const key = `${provider}:${bucket}`; const timer = this.timers.get(key); - if (timer) { + if (timer != null) { clearTimeout(timer); this.timers.delete(key); } diff --git a/packages/cli/src/auth/proxy/proxy-oauth-adapter.ts b/packages/cli/src/auth/proxy/proxy-oauth-adapter.ts index 2a7fe7418b..fed5006833 100644 --- a/packages/cli/src/auth/proxy/proxy-oauth-adapter.ts +++ b/packages/cli/src/auth/proxy/proxy-oauth-adapter.ts @@ -5,9 +5,9 @@ * @pseudocode analysis/pseudocode/009-proxy-oauth-adapter.md */ -import { +import type { ProxySocketClient, - type OAuthToken, + OAuthToken, } from '@vybestack/llxprt-code-core'; type FlowType = 'pkce_redirect' | 'device_code' | 'browser_redirect'; diff --git a/packages/cli/src/auth/proxy/refresh-coordinator.ts b/packages/cli/src/auth/proxy/refresh-coordinator.ts index d6b20fe553..16636afbcf 100644 --- a/packages/cli/src/auth/proxy/refresh-coordinator.ts +++ b/packages/cli/src/auth/proxy/refresh-coordinator.ts @@ -85,7 +85,7 @@ export class RefreshCoordinator { const key = bucket ? `${provider}:${bucket}` : provider; const inflight = this.inflightMap.get(key); - if (inflight) return inflight; + if (inflight != null) return inflight; const lastRefresh = this.lastRefreshMap.get(key); if (lastRefresh !== undefined) { @@ -122,7 +122,7 @@ export class RefreshCoordinator { const { tokenStore, refreshFn } = this.options; const currentToken = await tokenStore.getToken(provider, bucket); - if (!currentToken) { + if (currentToken == null) { return { status: 'error', error: 'Token not found' }; } diff --git a/packages/cli/src/auth/proxy/sandbox-proxy-lifecycle.ts b/packages/cli/src/auth/proxy/sandbox-proxy-lifecycle.ts index 300fa91d5a..11761078d0 100644 --- a/packages/cli/src/auth/proxy/sandbox-proxy-lifecycle.ts +++ b/packages/cli/src/auth/proxy/sandbox-proxy-lifecycle.ts @@ -113,13 +113,11 @@ class CodexFlowAdapter implements OAuthFlowInterface { ); // Complete the device auth flow - const token = await this.flow.completeDeviceAuth( + return this.flow.completeDeviceAuth( codeResult.authorization_code, codeResult.code_verifier, 'https://auth.openai.com/deviceauth/callback', ); - - return token; } /** @@ -159,7 +157,7 @@ function buildDefaultFlowFactories(): Map OAuthFlowInterface> { export async function createAndStartProxy( config: SandboxProxyConfig, ): Promise { - if (serverInstance) { + if (serverInstance != null) { return { stop: async () => { await stopProxy(); @@ -178,11 +176,11 @@ export async function createAndStartProxy( tokenStore, refreshFn: async (provider, currentToken) => { const flowFactory = flowFactories.get(provider); - if (!flowFactory) { + if (flowFactory == null) { throw new Error(`No OAuth provider configured for: ${provider}`); } const flowInstance = flowFactory(); - if (!flowInstance.refreshToken) { + if (flowInstance.refreshToken == null) { throw new Error(`Provider ${provider} does not support token refresh`); } if (!currentToken.refresh_token) { @@ -221,7 +219,7 @@ export async function createAndStartProxy( * Stops the credential proxy server and cleans up resources. */ export async function stopProxy(): Promise { - if (!serverInstance) { + if (serverInstance == null) { return; } diff --git a/packages/cli/src/auth/qwen-oauth-provider.test.ts b/packages/cli/src/auth/qwen-oauth-provider.test.ts index 142c8917be..18bd9668f8 100644 --- a/packages/cli/src/auth/qwen-oauth-provider.test.ts +++ b/packages/cli/src/auth/qwen-oauth-provider.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { QwenOAuthProvider } from './qwen-oauth-provider.js'; -import { TokenStore } from '@vybestack/llxprt-code-core'; +import type { TokenStore } from '@vybestack/llxprt-code-core'; // Mock the ClipboardService class - do this before importing it vi.mock('../services/ClipboardService.js', () => ({ @@ -194,7 +194,7 @@ describe('Qwen Provider Refactor Tests (Issue #1652 Phase 3)', () => { const result = await provider.getToken(); // Should return expired token as-is (read-only, no refresh attempt) - expect(result).toEqual(expiredToken); + expect(result).toStrictEqual(expiredToken); expect(mockTokenStore.saveToken).not.toHaveBeenCalled(); expect(mockTokenStore.removeToken).not.toHaveBeenCalled(); }); diff --git a/packages/cli/src/auth/qwen-oauth-provider.ts b/packages/cli/src/auth/qwen-oauth-provider.ts index bd61ececf5..568746e65a 100644 --- a/packages/cli/src/auth/qwen-oauth-provider.ts +++ b/packages/cli/src/auth/qwen-oauth-provider.ts @@ -11,12 +11,12 @@ import type { OAuthProvider } from './types.js'; import { - OAuthToken, + type OAuthToken, QwenDeviceFlow, - DeviceFlowConfig, + type DeviceFlowConfig, openBrowserSecurely, shouldLaunchBrowser, - TokenStore, + type TokenStore, OAuthError, OAuthErrorFactory, GracefulErrorHandler, @@ -25,7 +25,7 @@ import { debugLogger, } from '@vybestack/llxprt-code-core'; import { ClipboardService } from '../services/ClipboardService.js'; -import { HistoryItemWithoutId, HistoryItemOAuthURL } from '../ui/types.js'; +import type { HistoryItemWithoutId, HistoryItemOAuthURL } from '../ui/types.js'; import { globalOAuthUI } from './global-oauth-ui.js'; import { InitializationGuard, isTokenExpired } from './oauth-provider-base.js'; @@ -73,7 +73,7 @@ export class QwenOAuthProvider implements OAuthProvider { * @requirement REQ-004.2 * Deprecation warning for missing TokenStore */ - if (!tokenStore) { + if (tokenStore == null) { debugLogger.warn( `DEPRECATION: ${this.name} OAuth provider created without TokenStore. ` + `Token persistence will not work. Please update your code.`, @@ -122,7 +122,7 @@ export class QwenOAuthProvider implements OAuthProvider { const savedToken = await this.tokenStore?.getToken('qwen'); // Line 20: IF savedToken AND NOT this.isTokenExpired(savedToken) - if (savedToken && !isTokenExpired(savedToken)) { + if (savedToken != null && !isTokenExpired(savedToken)) { // Line 21: RETURN return; } @@ -189,7 +189,7 @@ export class QwenOAuthProvider implements OAuthProvider { url: authUrl, }; - if (this.addItem) { + if (this.addItem != null) { this.addItem(historyItem); } else { const delivered = globalOAuthUI.callAddItem(historyItem); @@ -222,7 +222,7 @@ export class QwenOAuthProvider implements OAuthProvider { itemData: Omit, baseTimestamp?: number, ): void { - if (this.addItem) { + if (this.addItem != null) { this.addItem(itemData, baseTimestamp); } else { const delivered = globalOAuthUI.callAddItem(itemData, baseTimestamp); @@ -282,12 +282,10 @@ export class QwenOAuthProvider implements OAuthProvider { await this.ensureInitialized(); return this.errorHandler.handleGracefully( - async () => { + async () => // Line 59: RETURN AWAIT this.tokenStore.getToken('qwen') // Read-only - OAuthManager owns all refresh operations - const token = (await this.tokenStore?.getToken('qwen')) || null; - return token; - }, + (await this.tokenStore?.getToken('qwen')) || null, null, // Return null on error this.name, 'getToken', @@ -346,7 +344,7 @@ export class QwenOAuthProvider implements OAuthProvider { return this.errorHandler.handleGracefully( async () => { // Line 88: AWAIT this.tokenStore.removeToken('qwen') - if (this.tokenStore && !token) { + if (this.tokenStore != null && token == null) { try { await this.tokenStore.removeToken('qwen'); } catch (error) { @@ -361,8 +359,8 @@ export class QwenOAuthProvider implements OAuthProvider { } // Line 89: PRINT "Successfully logged out from Qwen" - if (!token) { - if (this.addItem) { + if (token == null) { + if (this.addItem != null) { this.addItem( { type: 'info', text: 'Successfully logged out from Qwen' }, Date.now(), diff --git a/packages/cli/src/auth/token-access-coordinator.ts b/packages/cli/src/auth/token-access-coordinator.ts index bf21e424a0..90a770dd2f 100644 --- a/packages/cli/src/auth/token-access-coordinator.ts +++ b/packages/cli/src/auth/token-access-coordinator.ts @@ -17,8 +17,9 @@ import { type Config, type OAuthTokenRequestMetadata, } from '@vybestack/llxprt-code-core'; -import type { OAuthToken, TokenStore } from './types.js'; import type { + OAuthToken, + TokenStore, AuthenticatorInterface, BucketFailoverOAuthManagerLike, } from './types.js'; @@ -87,7 +88,7 @@ export class TokenAccessCoordinator { } private requireAuthenticator(): AuthenticatorInterface { - if (!this.authenticator) { + if (this.authenticator == null) { throw new Error( 'TokenAccessCoordinator: authenticator not wired — call setAuthenticator() first', ); @@ -154,7 +155,7 @@ export class TokenAccessCoordinator { throw new Error('Provider name must be a non-empty string'); } - if (!this.providerRegistry.getProvider(providerName)) { + if (this.providerRegistry.getProvider(providerName) == null) { throw new Error(`Unknown provider: ${providerName}`); } @@ -186,7 +187,7 @@ export class TokenAccessCoordinator { throw new Error('Provider name must be a non-empty string'); } - if (!this.providerRegistry.getProvider(providerName)) { + if (this.providerRegistry.getProvider(providerName) == null) { throw new Error(`Unknown provider: ${providerName}`); } @@ -229,7 +230,7 @@ export class TokenAccessCoordinator { () => `[FLOW] Reading token from tokenStore for ${providerName}...`, ); const token = await this.tokenStore.getToken(providerName, bucketToUse); - if (!token) { + if (token == null) { logger.debug(() => `[FLOW] No token in tokenStore for ${providerName}`); return null; } @@ -402,7 +403,7 @@ export class TokenAccessCoordinator { () => `[FLOW] getToken() called for provider: ${providerName}`, ); - if (!this.providerRegistry.getProvider(providerName)) { + if (this.providerRegistry.getProvider(providerName) == null) { logger.debug( () => `[FLOW] Unknown provider for getToken(): ${providerName}`, ); @@ -432,7 +433,7 @@ export class TokenAccessCoordinator { const token = await this.getOAuthToken(providerName, bucket); - if (token) { + if (token != null) { logger.debug( () => `[FLOW] Returning existing token for ${providerName} (expiry=${token.expiry})`, @@ -484,7 +485,10 @@ export class TokenAccessCoordinator { hasExplicitInMemoryOAuthState: boolean, ): boolean { // Respect explicit user settings when available. - if (this.settings && !this.providerRegistry.isOAuthEnabled(providerName)) { + if ( + this.settings != null && + !this.providerRegistry.isOAuthEnabled(providerName) + ) { logger.debug( () => `[FLOW] OAuth is disabled by settings for ${providerName}, returning null`, @@ -494,7 +498,7 @@ export class TokenAccessCoordinator { // In runtimes without LoadedSettings, only block when explicit in-memory state says disabled. if ( - !this.settings && + this.settings == null && hasExplicitInMemoryOAuthState && !this.providerRegistry.isOAuthEnabled(providerName) ) { @@ -590,7 +594,7 @@ export class TokenAccessCoordinator { providerName, peekBucket, ); - if (peekToken && peekToken.expiry > thirtySecondsFromNow) { + if (peekToken != null && peekToken.expiry > thirtySecondsFromNow) { logger.debug( () => `[issue1616] Found valid token in bucket '${peekBucket}' for ${providerName}, switching session`, @@ -662,7 +666,7 @@ export class TokenAccessCoordinator { return undefined; } const thirtySecondsFromNow = Math.floor(Date.now() / 1000) + 30; - if (diskToken && diskToken.expiry > thirtySecondsFromNow) { + if (diskToken != null && diskToken.expiry > thirtySecondsFromNow) { logger.debug( () => `[issue1262/1195] Found valid token on disk after lock timeout for ${providerName}`, @@ -737,7 +741,7 @@ export class TokenAccessCoordinator { providerName, explicitBucket ? bucketToCheck : requestMetadata, ); - return newToken ? newToken.access_token : null; + return newToken != null ? newToken.access_token : null; } // -------------------------------------------------------------------------- @@ -783,7 +787,7 @@ export class TokenAccessCoordinator { providerName: string, metadata?: OAuthTokenRequestMetadata, ): Promise { - if (this._getProfileBucketsDelegate) { + if (this._getProfileBucketsDelegate != null) { return this._getProfileBucketsDelegate(providerName, metadata); } return this.doGetProfileBuckets(providerName, metadata); diff --git a/packages/cli/src/auth/token-bucket-failover-helper.ts b/packages/cli/src/auth/token-bucket-failover-helper.ts index 6633c5a195..358712387d 100644 --- a/packages/cli/src/auth/token-bucket-failover-helper.ts +++ b/packages/cli/src/auth/token-bucket-failover-helper.ts @@ -49,7 +49,7 @@ export function ensureFailoverHandler( return undefined; } - if (!config) { + if (config == null) { logger.warn( `[issue1029] CRITICAL: Profile has ${profileBuckets.length} buckets but no Config available to set failover handler! ` + `Bucket failover will NOT work. Ensure OAuthManager receives the active Config instance from the composition root.`, @@ -81,7 +81,7 @@ export function ensureFailoverHandler( `[issue1029] Failover handler check: hasExisting=${!!failoverHandler}, sameBuckets=${sameBuckets}, sameScope=${sameScope}, existingBuckets=${JSON.stringify(existingBuckets)}`, ); - if (!failoverHandler || !sameBuckets || !sameScope) { + if (failoverHandler == null || !sameBuckets || !sameScope) { const handler = new BucketFailoverHandlerImpl( profileBuckets, providerName, diff --git a/packages/cli/src/auth/token-profile-resolver.ts b/packages/cli/src/auth/token-profile-resolver.ts index 7a6fc1bbda..f0a042923e 100644 --- a/packages/cli/src/auth/token-profile-resolver.ts +++ b/packages/cli/src/auth/token-profile-resolver.ts @@ -88,7 +88,7 @@ export async function loadProfileBuckets( if ( 'auth' in profile && - profile.auth && + profile.auth != null && typeof profile.auth === 'object' && 'type' in profile.auth && profile.auth.type === 'oauth' && diff --git a/packages/cli/src/auth/token-refresh-helper.ts b/packages/cli/src/auth/token-refresh-helper.ts index 88bed0c921..92bbe5a1d3 100644 --- a/packages/cli/src/auth/token-refresh-helper.ts +++ b/packages/cli/src/auth/token-refresh-helper.ts @@ -43,7 +43,7 @@ export async function handleRefreshLockMiss( `[FLOW] Failed to acquire refresh lock for ${providerName}, checking disk...`, ); const reloadedToken = await tokenStore.getToken(providerName, bucketToUse); - if (reloadedToken && reloadedToken.expiry > thirtySecondsFromNow) { + if (reloadedToken != null && reloadedToken.expiry > thirtySecondsFromNow) { logger.debug( () => `[FLOW] Token was refreshed by another process for ${providerName}`, ); @@ -77,7 +77,7 @@ export async function executeTokenRefresh( ): Promise { try { const recheckToken = await tokenStore.getToken(providerName, bucketToUse); - if (recheckToken && recheckToken.expiry > thirtySecondsFromNow) { + if (recheckToken != null && recheckToken.expiry > thirtySecondsFromNow) { logger.debug( () => `[FLOW] Token was refreshed by another process while waiting for lock for ${providerName}`, @@ -94,7 +94,7 @@ export async function executeTokenRefresh( // another process already consumed the single-use refresh token. Skip refresh // to avoid replaying a consumed token (which can trigger revocation). if ( - recheckToken && + recheckToken != null && token.refresh_token && recheckToken.refresh_token !== token.refresh_token ) { @@ -116,10 +116,10 @@ export async function executeTokenRefresh( } const provider = providerRegistry.getProvider(providerName); - if (!provider) return null; + if (provider == null) return null; const refreshedToken = await provider.refreshToken(recheckToken || token); - if (!refreshedToken) { + if (refreshedToken == null) { logger.debug( () => `[FLOW] Token refresh returned null for ${providerName}`, ); @@ -165,10 +165,10 @@ export async function tryRefreshDiskToken( providerRegistry: ProviderRegistry, ): Promise { const provider = providerRegistry.getProvider(providerName); - if (!provider) return undefined; + if (provider == null) return undefined; try { const refreshedToken = await provider.refreshToken(diskToken); - if (refreshedToken) { + if (refreshedToken != null) { const mergedToken = mergeRefreshedToken( diskToken as OAuthTokenWithExtras, refreshedToken as OAuthTokenWithExtras, @@ -203,7 +203,7 @@ export async function performDiskCheckUnderLock( const diskToken = await tokenStore.getToken(providerName, bucketToCheck); const thirtySecondsFromNow = Math.floor(Date.now() / 1000) + 30; - if (diskToken && diskToken.expiry > thirtySecondsFromNow) { + if (diskToken != null && diskToken.expiry > thirtySecondsFromNow) { logger.debug( () => `[issue1262/1195] Found valid token on disk for ${providerName}, skipping OAuth`, @@ -213,7 +213,7 @@ export async function performDiskCheckUnderLock( // @fix issue1317: expired disk token with refresh_token — try refresh if ( - diskToken && + diskToken != null && typeof diskToken.refresh_token === 'string' && diskToken.refresh_token !== '' ) { diff --git a/packages/cli/src/commands/extensions.tsx b/packages/cli/src/commands/extensions.tsx index cf3bb95f0f..0048c90b8a 100644 --- a/packages/cli/src/commands/extensions.tsx +++ b/packages/cli/src/commands/extensions.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CommandModule } from 'yargs'; +import type { CommandModule } from 'yargs'; import { installCommand } from './extensions/install.js'; import { uninstallCommand } from './extensions/uninstall.js'; import { listCommand } from './extensions/list.js'; diff --git a/packages/cli/src/commands/extensions/config.ts b/packages/cli/src/commands/extensions/config.ts index 255afc9fb5..6a50e48fc0 100644 --- a/packages/cli/src/commands/extensions/config.ts +++ b/packages/cli/src/commands/extensions/config.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { type CommandModule } from 'yargs'; +import type { CommandModule } from 'yargs'; import { getExtensionAndConfig } from './utils.js'; import { @@ -57,14 +57,13 @@ async function configureSetting( settingKey: string, scope: ExtensionSettingScope, ): Promise { - const success = await updateSetting( + return updateSetting( extensionName, extensionPath, settingKey, promptForSetting, scope, ); - return success; } /** @@ -157,7 +156,7 @@ async function handleConfig(args: ConfigArgs): Promise { args.name, ); - if (!extension || !extensionConfig) { + if (extension == null || extensionConfig == null) { return; } @@ -176,7 +175,7 @@ async function handleConfig(args: ConfigArgs): Promise { args.name, ); - if (!extension || !extensionConfig) { + if (extension == null || extensionConfig == null) { return; } @@ -205,7 +204,7 @@ async function handleConfig(args: ConfigArgs): Promise { workspaceDir: process.cwd(), }); - if (!extensionConfig) { + if (extensionConfig == null) { console.error( `Failed to load configuration for extension "${extension.name}". Skipping.`, ); diff --git a/packages/cli/src/commands/extensions/disable.ts b/packages/cli/src/commands/extensions/disable.ts index ba718bdf24..bbb8bc01cb 100644 --- a/packages/cli/src/commands/extensions/disable.ts +++ b/packages/cli/src/commands/extensions/disable.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { type CommandModule } from 'yargs'; +import type { CommandModule } from 'yargs'; import { FatalConfigError, getErrorMessage } from '@vybestack/llxprt-code-core'; import { disableExtension } from '../../config/extension.js'; import { SettingScope } from '../../config/settings.js'; diff --git a/packages/cli/src/commands/extensions/enable.ts b/packages/cli/src/commands/extensions/enable.ts index 645f8448f4..01a924ac99 100644 --- a/packages/cli/src/commands/extensions/enable.ts +++ b/packages/cli/src/commands/extensions/enable.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { type CommandModule } from 'yargs'; +import type { CommandModule } from 'yargs'; import { FatalConfigError, getErrorMessage } from '@vybestack/llxprt-code-core'; import { enableExtension } from '../../config/extension.js'; import { SettingScope } from '../../config/settings.js'; diff --git a/packages/cli/src/commands/extensions/install.test.ts b/packages/cli/src/commands/extensions/install.test.ts index 2da103369d..5519a00ade 100644 --- a/packages/cli/src/commands/extensions/install.test.ts +++ b/packages/cli/src/commands/extensions/install.test.ts @@ -98,7 +98,7 @@ describe('handleInstall', () => { ])( 'should install an extension from a $type', async ({ source, name, needsStat }) => { - if (needsStat) { + if (needsStat ?? false) { mockStat.mockResolvedValue({} as Stats); } mockInstallOrUpdateExtension.mockResolvedValue(name); diff --git a/packages/cli/src/commands/extensions/link.test.ts b/packages/cli/src/commands/extensions/link.test.ts index 35eebd6ca9..05cd2194a9 100644 --- a/packages/cli/src/commands/extensions/link.test.ts +++ b/packages/cli/src/commands/extensions/link.test.ts @@ -162,7 +162,7 @@ describe('handleLink', () => { await handleLink({ path: '/custom/path' }); const callArgs = mockInstallOrUpdateExtension.mock.calls[0]; - expect(callArgs[0]).toEqual({ source: '/custom/path', type: 'link' }); + expect(callArgs[0]).toStrictEqual({ source: '/custom/path', type: 'link' }); expect(callArgs[1]).toBe(mockRequestConsentNonInteractive); expect(callArgs[2]).toBe(process.cwd()); }); diff --git a/packages/cli/src/commands/extensions/link.ts b/packages/cli/src/commands/extensions/link.ts index 7f0ee05255..edfb8b5ec2 100644 --- a/packages/cli/src/commands/extensions/link.ts +++ b/packages/cli/src/commands/extensions/link.ts @@ -26,9 +26,10 @@ export async function handleLink(args: InstallArgs) { source: args.path, type: 'link', }; - const requestConsent = args.consent - ? () => Promise.resolve(true) - : requestConsentNonInteractive; + const requestConsent = + (args.consent ?? false) + ? () => Promise.resolve(true) + : requestConsentNonInteractive; const workspaceDir = process.cwd(); const extensionName = await installOrUpdateExtension( installMetadata, diff --git a/packages/cli/src/commands/extensions/settings.ts b/packages/cli/src/commands/extensions/settings.ts index 4538b6c4d2..e3f94b71d1 100644 --- a/packages/cli/src/commands/extensions/settings.ts +++ b/packages/cli/src/commands/extensions/settings.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { type CommandModule } from 'yargs'; +import type { CommandModule } from 'yargs'; import { getExtensionAndConfig } from './utils.js'; import { @@ -65,19 +65,18 @@ export async function promptForSetting( }; stdin.on('data', onData); }); - } else { - const readline = await import('node:readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - return new Promise((resolve) => { - rl.question(prompt, (answer) => { - rl.close(); - resolve(answer); - }); - }); } + const readline = await import('node:readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + rl.close(); + resolve(answer); + }); + }); } /** @@ -86,7 +85,7 @@ export async function promptForSetting( async function handleSet(args: SetArgs): Promise { const { extension, extensionConfig } = await getExtensionAndConfig(args.name); - if (!extension || !extensionConfig) { + if (extension == null || extensionConfig == null) { return; } @@ -111,7 +110,7 @@ async function handleSet(args: SetArgs): Promise { async function handleList(args: ListArgs): Promise { const { extension, extensionConfig } = await getExtensionAndConfig(args.name); - if (!extension || !extensionConfig) { + if (extension == null || extensionConfig == null) { return; } @@ -134,7 +133,8 @@ async function handleList(args: ListArgs): Promise { return; } - const scopeLabel = scope ? ` (${scope} scope)` : ' (merged user + workspace)'; + const scopeLabel = + scope != null ? ` (${scope} scope)` : ' (merged user + workspace)'; console.log(`Settings for extension "${args.name}"${scopeLabel}:`); for (const { name, value } of contents) { console.log(` ${name}: ${value}`); diff --git a/packages/cli/src/commands/extensions/uninstall.ts b/packages/cli/src/commands/extensions/uninstall.ts index a24ce3ddf0..3c003e4060 100644 --- a/packages/cli/src/commands/extensions/uninstall.ts +++ b/packages/cli/src/commands/extensions/uninstall.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CommandModule } from 'yargs'; +import type { CommandModule } from 'yargs'; import { uninstallExtension } from '../../config/extension.js'; import { exitCli } from '../utils.js'; @@ -51,7 +51,7 @@ export const uninstallCommand: CommandModule = { array: true, }) .check((argv) => { - if (!argv.names || argv.names.length === 0) { + if (argv.names == null || argv.names.length === 0) { throw new Error( 'Please include at least one extension name to uninstall.', ); diff --git a/packages/cli/src/commands/extensions/update.ts b/packages/cli/src/commands/extensions/update.ts index 0fc28b090a..c4787d5765 100644 --- a/packages/cli/src/commands/extensions/update.ts +++ b/packages/cli/src/commands/extensions/update.ts @@ -50,11 +50,11 @@ export async function handleUpdate(args: UpdateArgs) { const extension = extensions.find( (extension) => extension.name === args.name, ); - if (!extension) { + if (extension == null) { console.log(`Extension "${args.name}" not found.`); return; } - if (!extension.installMetadata) { + if (extension.installMetadata == null) { console.log( `Unable to install extension "${args.name}" due to missing install metadata`, ); @@ -78,8 +78,7 @@ export async function handleUpdate(args: UpdateArgs) { if (action.type === 'SET_STATE') { updateState = action.payload.state; } - }, - undefined, // enableExtensionReloading - undefined means use default behavior + }, // enableExtensionReloading - undefined means use default behavior ))!; if ( updatedExtensionInfo.originalVersion !== @@ -95,7 +94,7 @@ export async function handleUpdate(args: UpdateArgs) { console.error(getErrorMessage(error)); } } - if (args.all) { + if (args.all ?? false) { try { const extensionState = new Map(); await checkForAllExtensionUpdates( @@ -152,7 +151,7 @@ export const updateCommand: CommandModule = { }) .conflicts('name', 'all') .check((argv) => { - if (!argv.all && !argv.name) { + if (!(argv.all ?? false) && !argv.name) { throw new Error('Either an extension name or --all must be provided'); } return true; diff --git a/packages/cli/src/commands/extensions/utils.ts b/packages/cli/src/commands/extensions/utils.ts index 96f1d00deb..497d84c818 100644 --- a/packages/cli/src/commands/extensions/utils.ts +++ b/packages/cli/src/commands/extensions/utils.ts @@ -32,7 +32,7 @@ export async function getExtensionAndConfig(name: string): Promise<{ }> { const extension = loadExtensionByName(name, process.cwd()); - if (!extension) { + if (extension == null) { debugLogger.error(`Extension "${name}" not found.`); return { extension: null, extensionConfig: null }; } @@ -51,7 +51,7 @@ export async function getExtensionAndConfig(name: string): Promise<{ return { extension: null, extensionConfig: null }; } - if (!extensionConfig) { + if (extensionConfig == null) { debugLogger.error(`Could not load configuration for extension "${name}".`); return { extension: null, extensionConfig: null }; } diff --git a/packages/cli/src/commands/extensions/validate.ts b/packages/cli/src/commands/extensions/validate.ts index b94453b3d1..33a1629706 100644 --- a/packages/cli/src/commands/extensions/validate.ts +++ b/packages/cli/src/commands/extensions/validate.ts @@ -46,7 +46,7 @@ async function validateExtension(args: ValidateArgs) { extensionDir: absoluteInputPath, workspaceDir, }); - if (!extensionConfig) { + if (extensionConfig == null) { throw new Error( `Invalid extension at ${absoluteInputPath}. Please make sure it has a valid llxprt-extension.json or gemini-extension.json file.`, ); diff --git a/packages/cli/src/commands/hooks/migrate.ts b/packages/cli/src/commands/hooks/migrate.ts index ff8800aeda..80fa9209f4 100644 --- a/packages/cli/src/commands/hooks/migrate.ts +++ b/packages/cli/src/commands/hooks/migrate.ts @@ -8,7 +8,7 @@ import type { CommandModule } from 'yargs'; import { loadSettings } from '../../config/settings.js'; import { exitCli } from '../utils.js'; import { - HookEventName, + type HookEventName, type HookDefinition, debugLogger, } from '@vybestack/llxprt-code-core'; @@ -26,7 +26,7 @@ async function migrateHooks(options: MigrateOptions): Promise { const settings = loadSettings(); const userHooks = settings.merged.hooks; - if (!userHooks || Object.keys(userHooks).length === 0) { + if (userHooks == null || Object.keys(userHooks).length === 0) { debugLogger.log('No hooks found in user settings. Nothing to migrate.'); return; } @@ -88,7 +88,7 @@ async function migrateHooks(options: MigrateOptions): Promise { const typedEventName = eventName as HookEventName; - if (!mergedHooks[typedEventName]) { + if (mergedHooks[typedEventName] == null) { mergedHooks[typedEventName] = []; changesMade = true; } diff --git a/packages/cli/src/commands/mcp.test.ts b/packages/cli/src/commands/mcp.test.ts index 381dcc25f8..279a979e8a 100644 --- a/packages/cli/src/commands/mcp.test.ts +++ b/packages/cli/src/commands/mcp.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, vi } from 'vitest'; import { mcpCommand } from './mcp.js'; -import { type Argv } from 'yargs'; +import type { Argv } from 'yargs'; import yargs from 'yargs'; describe('mcp command', () => { diff --git a/packages/cli/src/commands/mcp/add.ts b/packages/cli/src/commands/mcp/add.ts index 6c361c7262..3a28a5c789 100644 --- a/packages/cli/src/commands/mcp/add.ts +++ b/packages/cli/src/commands/mcp/add.ts @@ -7,7 +7,7 @@ // File for 'llxprt mcp add' command import type { CommandModule } from 'yargs'; import { loadSettings, SettingScope } from '../../config/settings.js'; -import { MCPServerConfig, debugLogger } from '@vybestack/llxprt-code-core'; +import { type MCPServerConfig, debugLogger } from '@vybestack/llxprt-code-core'; import { exitCli } from '../utils.js'; async function addMcpServer( diff --git a/packages/cli/src/commands/mcp/list.test.ts b/packages/cli/src/commands/mcp/list.test.ts index 99715379e0..a72c86649a 100644 --- a/packages/cli/src/commands/mcp/list.test.ts +++ b/packages/cli/src/commands/mcp/list.test.ts @@ -11,8 +11,8 @@ import { expect, beforeEach, afterEach, - Mock, - MockInstance, + type Mock, + type MockInstance, } from 'vitest'; import { DebugLogger, createTransport } from '@vybestack/llxprt-code-core'; import { listMcpServers } from './list.js'; diff --git a/packages/cli/src/commands/mcp/list.ts b/packages/cli/src/commands/mcp/list.ts index 6939ecf74d..0f366f2ea4 100644 --- a/packages/cli/src/commands/mcp/list.ts +++ b/packages/cli/src/commands/mcp/list.ts @@ -9,7 +9,7 @@ import type { CommandModule } from 'yargs'; import { loadSettings } from '../../config/settings.js'; import { exitCli } from '../utils.js'; import { - MCPServerConfig, + type MCPServerConfig, MCPServerStatus, createTransport, debugLogger, diff --git a/packages/cli/src/commands/skills.test.tsx b/packages/cli/src/commands/skills.test.tsx index e37aa767e8..f4da5b0780 100644 --- a/packages/cli/src/commands/skills.test.tsx +++ b/packages/cli/src/commands/skills.test.tsx @@ -22,7 +22,7 @@ vi.mock('../gemini.js', () => ({ describe('skillsCommand', () => { it('should have correct command and aliases', () => { expect(skillsCommand.command).toBe('skills '); - expect(skillsCommand.aliases).toEqual(['skill']); + expect(skillsCommand.aliases).toStrictEqual(['skill']); expect(skillsCommand.describe).toBe('Manage skills.'); }); diff --git a/packages/cli/src/commands/skills/list.ts b/packages/cli/src/commands/skills/list.ts index 7d3dfa300f..bde7fa4941 100644 --- a/packages/cli/src/commands/skills/list.ts +++ b/packages/cli/src/commands/skills/list.ts @@ -8,7 +8,7 @@ import type { CommandModule } from 'yargs'; import { MessageBus, debugLogger } from '@vybestack/llxprt-code-core'; import { loadSettings } from '../../config/settings.js'; import { loadCliConfig } from '../../config/config.js'; -import { type CliArgs } from '../../config/cliArgParser.js'; +import type { CliArgs } from '../../config/cliArgParser.js'; import { loadExtensions, ExtensionEnablementManager, @@ -65,9 +65,10 @@ export async function handleList(showAll = false) { debugLogger.log(''); for (const skill of skills) { - const status = skill.disabled - ? chalk.red('[Disabled]') - : chalk.green('[Enabled]'); + const status = + (skill.disabled ?? false) + ? chalk.red('[Disabled]') + : chalk.green('[Enabled]'); // Show source indicator for non-user/project skills let sourceLabel = ''; diff --git a/packages/cli/src/commands/skills/uninstall.ts b/packages/cli/src/commands/skills/uninstall.ts index e7f4346466..9f63262951 100644 --- a/packages/cli/src/commands/skills/uninstall.ts +++ b/packages/cli/src/commands/skills/uninstall.ts @@ -23,7 +23,7 @@ export async function handleUninstall(args: UninstallArgs) { const result = await uninstallSkill(name, scope); - if (result) { + if (result != null) { debugLogger.log( chalk.green( `Successfully uninstalled skill: ${chalk.bold(name)} (scope: ${scope}, location: ${result.location})`, diff --git a/packages/cli/src/config/__tests__/folderTrustOriginalSettingsParity.test.ts b/packages/cli/src/config/__tests__/folderTrustOriginalSettingsParity.test.ts index 596b2c5ad8..75743425eb 100644 --- a/packages/cli/src/config/__tests__/folderTrustOriginalSettingsParity.test.ts +++ b/packages/cli/src/config/__tests__/folderTrustOriginalSettingsParity.test.ts @@ -292,7 +292,7 @@ describe('folderTrustOriginalSettingsParity: trust uses original settings', () = vi.mocked(isWorkspaceTrusted).mockImplementation((s: Settings) => { capturedTrustCheckSettings = s; // Return the original trust value from the captured settings - return !(s.folderTrust === false); + return s.folderTrust !== false; }); }); diff --git a/packages/cli/src/config/__tests__/parseArgumentsParity.test.ts b/packages/cli/src/config/__tests__/parseArgumentsParity.test.ts index 243e8bb1b7..1c4514ffe1 100644 --- a/packages/cli/src/config/__tests__/parseArgumentsParity.test.ts +++ b/packages/cli/src/config/__tests__/parseArgumentsParity.test.ts @@ -99,7 +99,7 @@ describe('parseArgumentsParity: positional mapping', () => { it('promptWords array populated from positional args', async () => { process.argv = ['node', 'script.js', 'hello', 'world']; const argv = await parseArguments({} as Settings); - expect(argv.promptWords).toEqual(['hello', 'world']); + expect(argv.promptWords).toStrictEqual(['hello', 'world']); }); }); @@ -203,7 +203,7 @@ describe('parseArgumentsParity: array coercion', () => { '/path/a,/path/b', ]; const argv = await parseArguments({} as Settings); - expect(argv.includeDirectories).toEqual(['/path/a', '/path/b']); + expect(argv.includeDirectories).toStrictEqual(['/path/a', '/path/b']); }); it('--set repeated → collects into array', async () => { @@ -216,7 +216,7 @@ describe('parseArgumentsParity: array coercion', () => { 'tool-output-max-tokens=4096', ]; const argv = await parseArguments({} as Settings); - expect(argv.set).toEqual([ + expect(argv.set).toStrictEqual([ 'context-limit=32000', 'tool-output-max-tokens=4096', ]); @@ -230,7 +230,10 @@ describe('parseArgumentsParity: array coercion', () => { 'read_file,ShellTool(git status)', ]; const argv = await parseArguments({} as Settings); - expect(argv.allowedTools).toEqual(['read_file', 'ShellTool(git status)']); + expect(argv.allowedTools).toStrictEqual([ + 'read_file', + 'ShellTool(git status)', + ]); }); it('--allowed-mcp-server-names comma-separated → split into array', async () => { @@ -241,13 +244,13 @@ describe('parseArgumentsParity: array coercion', () => { 'server1,server2', ]; const argv = await parseArguments({} as Settings); - expect(argv.allowedMcpServerNames).toEqual(['server1', 'server2']); + expect(argv.allowedMcpServerNames).toStrictEqual(['server1', 'server2']); }); it('--extensions comma-separated → split into array', async () => { process.argv = ['node', 'script.js', '--extensions', 'ext1,ext2']; const argv = await parseArguments({} as Settings); - expect(argv.extensions).toEqual(['ext1', 'ext2']); + expect(argv.extensions).toStrictEqual(['ext1', 'ext2']); }); }); diff --git a/packages/cli/src/config/__tests__/profileBootstrap.test.ts b/packages/cli/src/config/__tests__/profileBootstrap.test.ts index 526d8730a8..a5335c27ae 100644 --- a/packages/cli/src/config/__tests__/profileBootstrap.test.ts +++ b/packages/cli/src/config/__tests__/profileBootstrap.test.ts @@ -132,7 +132,7 @@ describe('profileBootstrap helpers', () => { '--set=shell-replacement=true', ]; const parsed = parseArgs(); - expect(parsed.bootstrapArgs.setOverrides).toEqual([ + expect(parsed.bootstrapArgs.setOverrides).toStrictEqual([ 'modelparam.temperature=1', 'context-limit=190000', 'shell-replacement=true', @@ -310,7 +310,7 @@ describe('--profile flag parsing @plan:PLAN-20251118-ISSUE533.P04', () => { const result = parseBootstrapArgs(); expect(result.bootstrapArgs.profileJson).toBe('{"model":"gpt-4"}'); expect(result.bootstrapArgs.keyOverride).toBe('test-key'); - expect(result.bootstrapArgs.setOverrides).toEqual(['debug=true']); + expect(result.bootstrapArgs.setOverrides).toStrictEqual(['debug=true']); }); // Group 2: Error Cases (4 tests) @@ -510,7 +510,7 @@ describe('parseInlineProfile() @plan:PLAN-20251118-ISSUE533.P07', () => { expect(result.providerName).toBe('anthropic'); expect(result.modelName).toBe('claude-3-5-sonnet-20241022'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); /** @@ -534,7 +534,7 @@ describe('parseInlineProfile() @plan:PLAN-20251118-ISSUE533.P07', () => { expect(result.providerName).toBe('openai'); expect(result.modelName).toBe('gpt-4'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); /** @@ -558,7 +558,7 @@ describe('parseInlineProfile() @plan:PLAN-20251118-ISSUE533.P07', () => { expect(result.providerName).toBe('anthropic'); expect(result.modelName).toBe('claude-3-5-sonnet-20241022'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); /** @@ -584,7 +584,7 @@ describe('parseInlineProfile() @plan:PLAN-20251118-ISSUE533.P07', () => { expect(result.providerName).toBe('openai'); expect(result.modelName).toBe('gpt-4'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); /** @@ -611,7 +611,7 @@ describe('parseInlineProfile() @plan:PLAN-20251118-ISSUE533.P07', () => { const result = parseInlineProfile(JSON.stringify(profile)); expect(result.providerName).toBe(profile.provider); expect(result.modelName).toBe(profile.model); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); }); @@ -732,7 +732,7 @@ describe('parseInlineProfile() @plan:PLAN-20251118-ISSUE533.P07', () => { expect(result.error).toBeUndefined(); expect(result.providerName).toBe('Synthetic'); expect(result.modelName).toBe('hf:zai-org/GLM-4.6'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); /** @@ -821,7 +821,7 @@ describe('parseInlineProfile() @plan:PLAN-20251118-ISSUE533.P07', () => { expect(result.providerName).toBe('anthropic'); expect(result.modelName).toBe('claude-3-5-sonnet-20241022'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); /** @@ -936,7 +936,7 @@ describe('applyBootstrapProfile() with --profile @plan:PLAN-20251118-ISSUE533.P0 expect(result.providerName).toBe('openai'); expect(result.modelName).toBe('gpt-4'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); /** @@ -974,7 +974,7 @@ describe('applyBootstrapProfile() with --profile @plan:PLAN-20251118-ISSUE533.P0 expect(result.providerName).toBe('anthropic'); expect(result.modelName).toBe('claude-3-5-sonnet-20241022'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); /** @@ -1012,7 +1012,7 @@ describe('applyBootstrapProfile() with --profile @plan:PLAN-20251118-ISSUE533.P0 expect(result.providerName).toBe('openai'); expect(result.modelName).toBe('gpt-4'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); /** @@ -1049,7 +1049,7 @@ describe('applyBootstrapProfile() with --profile @plan:PLAN-20251118-ISSUE533.P0 expect(result.providerName).toBeNull(); expect(result.modelName).toBeNull(); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); // Group 2: Override Precedence (4 tests) @@ -1088,7 +1088,7 @@ describe('applyBootstrapProfile() with --profile @plan:PLAN-20251118-ISSUE533.P0 expect(result.providerName).toBe('openai'); expect(result.modelName).toBe('gpt-3.5-turbo'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); /** @@ -1347,7 +1347,7 @@ describe('applyBootstrapProfile() with --profile @plan:PLAN-20251118-ISSUE533.P0 expect(result.providerName).toBe('openai'); expect(result.modelName).toBe('gpt-4'); - expect(result.warnings).toEqual([]); + expect(result.warnings).toStrictEqual([]); }); }); diff --git a/packages/cli/src/config/__tests__/providerModelPrecedenceParity.test.ts b/packages/cli/src/config/__tests__/providerModelPrecedenceParity.test.ts index 9665f0d9f3..74b6d2d78d 100644 --- a/packages/cli/src/config/__tests__/providerModelPrecedenceParity.test.ts +++ b/packages/cli/src/config/__tests__/providerModelPrecedenceParity.test.ts @@ -260,7 +260,7 @@ function makeExtMgr() { } async function runConfig(settings: Settings, argv?: string[]) { - if (argv) { + if (argv != null) { process.argv = ['node', 'script.js', ...argv]; } const parsedArgv = await parseArguments(settings); diff --git a/packages/cli/src/config/__tests__/sandboxConfig.test.ts b/packages/cli/src/config/__tests__/sandboxConfig.test.ts index b1695200eb..59f5f47559 100644 --- a/packages/cli/src/config/__tests__/sandboxConfig.test.ts +++ b/packages/cli/src/config/__tests__/sandboxConfig.test.ts @@ -73,7 +73,7 @@ describe('loadSandboxConfig', () => { sandboxEngine: 'auto', }); - expect(config).toEqual({ + expect(config).toStrictEqual({ command: 'docker', image: 'ghcr.io/vybestack/llxprt-code/sandbox:0.7.0', }); diff --git a/packages/cli/src/config/approvalModeResolver.ts b/packages/cli/src/config/approvalModeResolver.ts index 4ce06417ab..64a86c2153 100644 --- a/packages/cli/src/config/approvalModeResolver.ts +++ b/packages/cli/src/config/approvalModeResolver.ts @@ -52,7 +52,7 @@ export function resolveApprovalMode(input: ApprovalModeInput): ApprovalMode { ); } } else { - approvalMode = cliYolo || false ? ApprovalMode.YOLO : ApprovalMode.DEFAULT; + approvalMode = cliYolo ? ApprovalMode.YOLO : ApprovalMode.DEFAULT; } if (disableYoloMode || secureModeEnabled) { diff --git a/packages/cli/src/config/cliArgParser.ts b/packages/cli/src/config/cliArgParser.ts index f83813bfa3..7182ef107e 100644 --- a/packages/cli/src/config/cliArgParser.ts +++ b/packages/cli/src/config/cliArgParser.ts @@ -96,11 +96,11 @@ function registerCommands(yargsInstance: Argv, settings: Settings): Argv { yargsInstance.command(hooksCommand); } - if (settings?.extensionManagement ?? false) { + if (settings.extensionManagement ?? false) { yargsInstance.command(extensionsCommand); } - if (settings?.experimental?.skills ?? false) { + if (settings.experimental?.skills ?? false) { yargsInstance.command(skillsCommand); } @@ -155,8 +155,8 @@ function mapParsedArgsToCliArgs(result: Record): CliArgs { sandboxProfileLoad: result['sandboxProfileLoad'] as string | undefined, debug: result['debug'] as boolean | undefined, prompt: - (result['prompt'] as string | undefined) || - queryFromPromptWords || + (result['prompt'] as string | undefined) ?? + queryFromPromptWords ?? undefined, promptInteractive: result['promptInteractive'] as string | undefined, outputFormat: result['outputFormat'] as string | undefined, @@ -206,7 +206,7 @@ function mapParsedArgsToCliArgs(result: Record): CliArgs { /** Checks for subcommand dispatch (mcp, hooks, extensions) and exits if handled. */ function handleSubcommandExit(result: Record): void { const commands = result['_'] as unknown[]; - if (!commands || commands.length === 0) { + if (commands.length === 0) { return; } @@ -244,17 +244,24 @@ export async function parseArguments(settings: Settings): Promise { }) .check((argv) => { const pw = argv['promptWords']; - if (argv['prompt'] && Array.isArray(pw) && pw.length > 0) { + if ( + argv['prompt'] !== undefined && + Array.isArray(pw) && + pw.length > 0 + ) { throw new Error( 'Cannot use both a positional prompt and the --prompt (-p) flag together', ); } - if (argv['prompt'] && argv['promptInteractive']) { + if ( + argv['prompt'] !== undefined && + argv['promptInteractive'] !== undefined + ) { throw new Error( 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together', ); } - if (argv['yolo'] && argv['approvalMode']) { + if (argv['yolo'] === true && argv['approvalMode'] !== undefined) { throw new Error( 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.', ); @@ -277,12 +284,15 @@ export async function parseArguments(settings: Settings): Promise { .alias('h', 'help') .strict() .check((argv) => { - if (argv['prompt'] && argv['promptInteractive']) { + if ( + argv['prompt'] !== undefined && + argv['promptInteractive'] !== undefined + ) { throw new Error( 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together', ); } - if (argv['profile'] && argv['profileLoad']) { + if (argv['profile'] !== undefined && argv['profileLoad'] !== undefined) { throw new Error( 'Cannot use both --profile and --profile-load. Use one at a time.', ); diff --git a/packages/cli/src/config/cliEphemeralSettings.test.ts b/packages/cli/src/config/cliEphemeralSettings.test.ts index 017365203f..a5fc6741a1 100644 --- a/packages/cli/src/config/cliEphemeralSettings.test.ts +++ b/packages/cli/src/config/cliEphemeralSettings.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect } from 'vitest'; import { applyCliSetArguments, - EphemeralSettingTarget, + type EphemeralSettingTarget, } from './cliEphemeralSettings.js'; class TestTarget implements EphemeralSettingTarget { @@ -77,7 +77,7 @@ describe('applyCliSetArguments', () => { 'modelparam.stop_sequences=["END"]', ]); - expect(result.modelParams).toEqual({ + expect(result.modelParams).toStrictEqual({ temperature: 0.7, stop_sequences: ['END'], }); diff --git a/packages/cli/src/config/cliEphemeralSettings.ts b/packages/cli/src/config/cliEphemeralSettings.ts index 9501e60dbf..1b7278726f 100644 --- a/packages/cli/src/config/cliEphemeralSettings.ts +++ b/packages/cli/src/config/cliEphemeralSettings.ts @@ -23,7 +23,7 @@ export function applyCliSetArguments( ): CliSetResult { const cliModelParams: Record = {}; - if (!setArgs || setArgs.length === 0) { + if (setArgs == null || setArgs.length === 0) { return { modelParams: cliModelParams }; } diff --git a/packages/cli/src/config/config.integration.test.ts b/packages/cli/src/config/config.integration.test.ts index 99ca7a71a9..edd8cc4cba 100644 --- a/packages/cli/src/config/config.integration.test.ts +++ b/packages/cli/src/config/config.integration.test.ts @@ -10,8 +10,8 @@ import * as path from 'path'; import { tmpdir } from 'os'; import { Config, - ConfigParameters, - ContentGeneratorConfig, + type ConfigParameters, + type ContentGeneratorConfig, DEFAULT_FILE_FILTERING_OPTIONS, } from '@vybestack/llxprt-code-core'; import type { Settings } from './settings.js'; @@ -234,7 +234,7 @@ describe('Configuration Integration Tests', () => { debugMode: false, }; const config = new Config(configParams); - expect(config.getExtensionContextFilePaths()).toEqual([]); + expect(config.getExtensionContextFilePaths()).toStrictEqual([]); }); it('should correctly store and return extension context file paths', () => { @@ -249,7 +249,7 @@ describe('Configuration Integration Tests', () => { extensionContextFilePaths: contextFiles, }; const config = new Config(configParams); - expect(config.getExtensionContextFilePaths()).toEqual(contextFiles); + expect(config.getExtensionContextFilePaths()).toStrictEqual(contextFiles); }); }); @@ -422,7 +422,7 @@ describe('Configuration Integration Tests', () => { const argv = await parseArguments(settings); - expect(argv.set).toEqual([ + expect(argv.set).toStrictEqual([ 'context-limit=32000', 'tool-output-max-tokens=4096', ]); diff --git a/packages/cli/src/config/config.loadMemory.test.ts b/packages/cli/src/config/config.loadMemory.test.ts index 25ee12b46b..50bd73e370 100644 --- a/packages/cli/src/config/config.loadMemory.test.ts +++ b/packages/cli/src/config/config.loadMemory.test.ts @@ -15,7 +15,7 @@ import { type SettingsService, } from '@vybestack/llxprt-code-core'; import { loadCliConfig } from './config.js'; -import { type CliArgs } from './cliArgParser.js'; +import type { CliArgs } from './cliArgParser.js'; import type { Settings } from './settings.js'; vi.mock('@vybestack/llxprt-code-core', async (importOriginal) => { diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 7c064d7c75..b4396fd7cb 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -226,7 +226,7 @@ vi.mock('@vybestack/llxprt-code-core', async () => { }, loadEnvironment: vi.fn(), loadServerHierarchicalMemory: vi.fn( - (cwd, dirs, debug, fileService, extensionPaths, _maxDirs) => + (_cwd, _dirs, _debug, _fileService, extensionPaths, _maxDirs) => Promise.resolve({ memoryContent: extensionPaths?.join(',') || '', fileCount: extensionPaths?.length || 0, @@ -618,7 +618,10 @@ describe('parseArguments', () => { 'read_file,ShellTool(git status)', ]; const argv = await parseArguments({} as Settings); - expect(argv.allowedTools).toEqual(['read_file', 'ShellTool(git status)']); + expect(argv.allowedTools).toStrictEqual([ + 'read_file', + 'ShellTool(git status)', + ]); }); it('should support comma-separated values for --allowed-mcp-server-names', async () => { @@ -629,13 +632,13 @@ describe('parseArguments', () => { 'server1,server2', ]; const argv = await parseArguments({} as Settings); - expect(argv.allowedMcpServerNames).toEqual(['server1', 'server2']); + expect(argv.allowedMcpServerNames).toStrictEqual(['server1', 'server2']); }); it('should support comma-separated values for --extensions', async () => { process.argv = ['node', 'script.js', '--extensions', 'ext1,ext2']; const argv = await parseArguments({} as Settings); - expect(argv.extensions).toEqual(['ext1', 'ext2']); + expect(argv.extensions).toStrictEqual(['ext1', 'ext2']); }); }); @@ -700,7 +703,7 @@ describe('loadCliConfig', () => { const loadMemoryMock = vi.mocked(ServerConfig.loadServerHierarchicalMemory); expect(loadMemoryMock).toHaveBeenCalled(); expect(loadMemoryMock.mock.calls.at(-1)?.[0]).toBe(process.cwd()); - expect(loadMemoryMock.mock.calls.at(-1)?.[1]).toEqual( + expect(loadMemoryMock.mock.calls.at(-1)?.[1]).toStrictEqual( expectedIncludeDirectories, ); expect(config.shouldLoadMemoryFromIncludeDirectories()).toBe(true); @@ -742,7 +745,7 @@ describe('loadCliConfig chatCompression', () => { 'test-session', argv, ); - expect(config.getChatCompression()).toEqual({ + expect(config.getChatCompression()).toStrictEqual({ contextPercentageThreshold: 0.5, }); }); @@ -1267,7 +1270,7 @@ describe('loadCliConfig interactive', () => { ); expect(config.isInteractive()).toBe(false); expect(argv.query).toBe('hello'); - expect(argv.extensions).toEqual(['none']); + expect(argv.extensions).toStrictEqual(['none']); }); it('should handle multiple positional words correctly', async () => { @@ -1363,7 +1366,7 @@ describe('loadCliConfig interactive', () => { ); expect(config.isInteractive()).toBe(false); expect(argv.query).toBe('hello world how are you'); - expect(argv.extensions).toEqual(['none']); + expect(argv.extensions).toStrictEqual(['none']); }); it('should be interactive if no positional prompt words are provided with flags', async () => { @@ -1739,7 +1742,7 @@ describe('parseArguments with positional prompt', () => { it('should correctly parse a positional prompt', async () => { process.argv = ['node', 'script.js', 'positional', 'prompt']; const argv = await parseArguments({} as Settings); - expect(argv.promptWords).toEqual(['positional', 'prompt']); + expect(argv.promptWords).toStrictEqual(['positional', 'prompt']); }); it('should correctly parse positional query argument', async () => { @@ -1786,7 +1789,9 @@ describe('defaultDisabledTools', () => { argv, ); const disabled = config.getEphemeralSetting('tools.disabled'); - expect(disabled).toEqual(expect.arrayContaining(['google_web_fetch'])); + expect(disabled).toStrictEqual( + expect.arrayContaining(['google_web_fetch']), + ); }); it('should merge defaultDisabledTools with existing tools.disabled', async () => { @@ -1812,7 +1817,7 @@ describe('defaultDisabledTools', () => { ); const currentDisabled = (config.getEphemeralSetting('tools.disabled') as string[]) || []; - expect(currentDisabled).toEqual( + expect(currentDisabled).toStrictEqual( expect.arrayContaining(['read_file', 'google_web_fetch']), ); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 90121815dc..ae7104d717 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -7,16 +7,16 @@ */ import process from 'node:process'; -import { +import type { Config, - type GeminiCLIExtension, + GeminiCLIExtension, SettingsService, } from '@vybestack/llxprt-code-core'; -import { Settings } from './settings.js'; +import type { Settings } from './settings.js'; import { loadSandboxConfig } from './sandboxConfig.js'; import { resolveMcpServers } from './mcpServerConfig.js'; -import { type CliArgs } from './cliArgParser.js'; +import type { CliArgs } from './cliArgParser.js'; import type { ExtensionEnablementManager } from './extensions/extensionEnablement.js'; // @plan:PLAN-20251020-STATELESSPROVIDER3.P04 @@ -169,42 +169,42 @@ export async function loadCliConfig( const sandboxConfig = await loadSandboxConfig(profileMergedSettings, argv); const config = buildConfig({ + profileSettingsWithTools: intermediate.profileSettingsWithTools, + excludeTools: intermediate.excludeTools, + policyEngineConfig: intermediate.policyEngineConfig, + question: intermediate.question, + screenReader: intermediate.screenReader, + useRipgrepSetting: intermediate.useRipgrepSetting, + mcpEnabled: intermediate.mcpEnabled, + extensionsEnabled: intermediate.extensionsEnabled, + adminSkillsEnabled: intermediate.adminSkillsEnabled, + outputFormat: intermediate.outputFormat, + allowedTools: intermediate.allowedTools, sessionId, cwd, argv, - profileSettingsWithTools: intermediate.profileSettingsWithTools, context, approvalMode, providerModel, sandboxConfig, mcpServers, blockedMcpServers, - excludeTools: intermediate.excludeTools, memoryContent, fileCount, filePaths, - policyEngineConfig: intermediate.policyEngineConfig, - question: intermediate.question, - screenReader: intermediate.screenReader, - useRipgrepSetting: intermediate.useRipgrepSetting, - mcpEnabled: intermediate.mcpEnabled, - extensionsEnabled: intermediate.extensionsEnabled, - adminSkillsEnabled: intermediate.adminSkillsEnabled, - outputFormat: intermediate.outputFormat, - allowedTools: intermediate.allowedTools, }); return finalizeConfig({ - config, - runtimeState, - bootstrapArgs, - argv, profileSettingsWithTools: intermediate.profileSettingsWithTools, profileLoadResult: profileResult, providerModelResult: providerModel, defaultDisabledTools: profileMergedSettings.defaultDisabledTools ?? [], + interactive: context.interactive, + config, + runtimeState, + bootstrapArgs, + argv, runtimeOverrides, approvalMode, - interactive: context.interactive, }); } diff --git a/packages/cli/src/config/configBuilder.ts b/packages/cli/src/config/configBuilder.ts index 5ada0aec1d..152d52a62f 100644 --- a/packages/cli/src/config/configBuilder.ts +++ b/packages/cli/src/config/configBuilder.ts @@ -95,7 +95,7 @@ function buildHooksConfig( adminSkillsEnabled: boolean, cwd: string, ) { - const hooksConfig = settings.hooks || {}; + const hooksConfig = settings.hooks ?? {}; const { disabled: _disabled, ...eventHooks } = hooksConfig as { disabled?: string[]; [key: string]: unknown; @@ -126,9 +126,8 @@ function buildToolConfig( policyEngineConfig: PolicyEngineConfig, ) { return { - coreTools: profileSettingsWithTools.coreTools || undefined, + coreTools: profileSettingsWithTools.coreTools ?? undefined, allowedTools: allowedTools.length > 0 ? [...allowedTools] : undefined, - policyEngineConfig, excludeTools: [...excludeTools], toolDiscoveryCommand: profileSettingsWithTools.toolDiscoveryCommand, toolCallCommand: profileSettingsWithTools.toolCallCommand, @@ -136,10 +135,11 @@ function buildToolConfig( ? profileSettingsWithTools.mcpServerCommand : undefined, mcpServers: mcpEnabled ? mcpServers : {}, - mcpEnabled, allowedMcpServers: mcpEnabled ? (argv.allowedMcpServerNames ?? profileSettingsWithTools.mcp?.allowed) : undefined, + policyEngineConfig, + mcpEnabled, }; } @@ -168,7 +168,6 @@ function buildSessionBaseArgs( adminSkillsEnabled, } = input; return { - sessionId, embeddingModel: undefined, sandbox: sandboxConfig, targetDir: cwd, @@ -176,41 +175,42 @@ function buildSessionBaseArgs( loadMemoryFromIncludeDirectories: context.resolvedLoadMemoryFromIncludeDirectories, debugMode: context.debugMode, - outputFormat, - question, ...toolConfig, - extensionsEnabled, - adminSkillsEnabled, userMemory: memoryContent, llxprtMdFileCount: fileCount, llxprtMdFilePaths: [...filePaths], - approvalMode, showMemoryUsage: - argv.showMemoryUsage || - profileSettingsWithTools.ui?.showMemoryUsage || - false, + (argv.showMemoryUsage ?? false) || + (profileSettingsWithTools.ui?.showMemoryUsage ?? false), disableYoloMode: - profileSettingsWithTools.security?.disableYoloMode || - profileSettingsWithTools.admin?.secureModeEnabled, + (profileSettingsWithTools.security?.disableYoloMode ?? false) || + (profileSettingsWithTools.admin?.secureModeEnabled ?? false), accessibility: { ...profileSettingsWithTools.accessibility, screenReader }, - telemetry, usageStatisticsEnabled: profileSettingsWithTools.ui?.usageStatisticsEnabled ?? true, fileFiltering: context.fileFiltering, checkpointing: - argv.checkpointing || profileSettingsWithTools.checkpointing?.enabled, - dumpOnError: argv.dumponerror || false, + (argv.checkpointing ?? false) || + (profileSettingsWithTools.checkpointing?.enabled ?? false), + dumpOnError: argv.dumponerror ?? false, proxy: - argv.proxy || - process.env.HTTPS_PROXY || - process.env.https_proxy || - process.env.HTTP_PROXY || + argv.proxy ?? + process.env.HTTPS_PROXY ?? + process.env.https_proxy ?? + process.env.HTTP_PROXY ?? process.env.http_proxy, - cwd, fileDiscoveryService: context.fileService, bugCommand: profileSettingsWithTools.bugCommand, model: providerModel.model, provider: providerModel.provider, + sessionId, + outputFormat, + question, + extensionsEnabled, + adminSkillsEnabled, + approvalMode, + telemetry, + cwd, sanitizationConfig, }; } @@ -229,8 +229,8 @@ function buildFeatureArgs( return { extensionContextFilePaths: [...context.extensionContextFilePaths], maxSessionTurns: profileSettingsWithTools.ui?.maxSessionTurns ?? -1, - experimentalZedIntegration: argv.experimentalAcp || false, - listExtensions: argv.listExtensions || false, + experimentalZedIntegration: argv.experimentalAcp ?? false, + listExtensions: argv.listExtensions ?? false, activeExtensions: context.activeExtensions.map((e) => ({ name: e.name, version: e.version, @@ -268,7 +268,7 @@ function buildFeatureArgs( continueSession: argv.continue === '' || argv.continue === true ? true - : argv.continue || false, + : (argv.continue ?? false), jitContextEnabled: context.jitContextEnabled, ...hooksConfig, }; diff --git a/packages/cli/src/config/extension.skills.test.ts b/packages/cli/src/config/extension.skills.test.ts index 6db2922820..796ee94061 100644 --- a/packages/cli/src/config/extension.skills.test.ts +++ b/packages/cli/src/config/extension.skills.test.ts @@ -85,7 +85,7 @@ describe('extension skills loading', () => { }); expect(extension).not.toBeNull(); - expect(extension!.skills).toEqual([]); + expect(extension!.skills).toStrictEqual([]); }); it('should load skills from a skills subdirectory', () => { @@ -145,7 +145,7 @@ describe('extension skills loading', () => { expect(extension).not.toBeNull(); expect(extension!.skills).toHaveLength(2); const names = extension!.skills!.map((s) => s.name).sort(); - expect(names).toEqual(['skill-alpha', 'skill-beta']); + expect(names).toStrictEqual(['skill-alpha', 'skill-beta']); }); it('should return empty skills when skills directory exists but has no valid SKILL.md files', () => { @@ -165,7 +165,7 @@ describe('extension skills loading', () => { }); expect(extension).not.toBeNull(); - expect(extension!.skills).toEqual([]); + expect(extension!.skills).toStrictEqual([]); }); it('should skip SKILL.md files with invalid frontmatter', () => { @@ -188,7 +188,7 @@ describe('extension skills loading', () => { }); expect(extension).not.toBeNull(); - expect(extension!.skills).toEqual([]); + expect(extension!.skills).toStrictEqual([]); }); it('should make skills available through loadExtensions', () => { diff --git a/packages/cli/src/config/extension.test.ts b/packages/cli/src/config/extension.test.ts index 1032733b79..609520f906 100644 --- a/packages/cli/src/config/extension.test.ts +++ b/packages/cli/src/config/extension.test.ts @@ -208,10 +208,10 @@ describe('extension tests', () => { expect(extensions).toHaveLength(2); const ext1 = extensions.find((e) => e.name === 'ext1'); const ext2 = extensions.find((e) => e.name === 'ext2'); - expect(ext1?.contextFiles).toEqual([ + expect(ext1?.contextFiles).toStrictEqual([ path.join(userExtensionsDir, 'ext1', 'LLXPRT.md'), ]); - expect(ext2?.contextFiles).toEqual([]); + expect(ext2?.contextFiles).toStrictEqual([]); }); it('should load context file path from the extension config', () => { @@ -229,7 +229,7 @@ describe('extension tests', () => { expect(extensions).toHaveLength(1); const ext1 = extensions.find((e) => e.name === 'ext1'); - expect(ext1?.contextFiles).toEqual([ + expect(ext1?.contextFiles).toStrictEqual([ path.join(userExtensionsDir, 'ext1', 'my-context-file.md'), ]); }); @@ -306,7 +306,7 @@ describe('extension tests', () => { async (_) => true, ); - expect(extensionName).toEqual('my-linked-extension'); + expect(extensionName).toStrictEqual('my-linked-extension'); const extensions = loadExtensions( new ExtensionEnablementManager(ExtensionStorage.getUserExtensionsDir()), ); @@ -316,11 +316,11 @@ describe('extension tests', () => { expect(linkedExt.name).toBe('my-linked-extension'); expect(linkedExt.path).toBe(sourceExtDir); - expect(linkedExt.installMetadata).toEqual({ + expect(linkedExt.installMetadata).toStrictEqual({ source: sourceExtDir, type: 'link', }); - expect(linkedExt.contextFiles).toEqual([ + expect(linkedExt.contextFiles).toStrictEqual([ path.join(sourceExtDir, 'context.md'), ]); }); @@ -354,7 +354,7 @@ describe('extension tests', () => { expect(extensions[0].mcpServers?.['test-server'].cwd).toBe( path.join(sourceExtDir, 'server'), ); - expect(extensions[0].mcpServers?.['test-server'].args).toEqual([ + expect(extensions[0].mcpServers?.['test-server'].args).toStrictEqual([ path.join(sourceExtDir, 'server', 'index.js'), ]); }); @@ -1165,7 +1165,7 @@ describe('extension tests', () => { expect(fs.existsSync(targetExtDir)).toBe(true); expect(fs.existsSync(metadataPath)).toBe(true); const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); - expect(metadata).toEqual({ + expect(metadata).toStrictEqual({ source: sourceExtDir, type: 'local', }); @@ -1268,7 +1268,7 @@ describe('extension tests', () => { expect(fs.existsSync(targetExtDir)).toBe(true); expect(fs.existsSync(metadataPath)).toBe(true); const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); - expect(metadata).toEqual({ + expect(metadata).toStrictEqual({ source: gitUrl, type: 'git', }); @@ -1295,7 +1295,7 @@ describe('extension tests', () => { expect(fs.existsSync(configPath)).toBe(false); const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); - expect(metadata).toEqual({ + expect(metadata).toStrictEqual({ source: sourceExtDir, type: 'link', }); @@ -1417,7 +1417,7 @@ This extension will run the following MCP servers: expect(fs.existsSync(targetExtDir)).toBe(true); expect(fs.existsSync(metadataPath)).toBe(true); const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); - expect(metadata).toEqual({ + expect(metadata).toStrictEqual({ source: sourceExtDir, type: 'local', autoUpdate: true, @@ -1607,7 +1607,7 @@ This extension will run the following MCP servers: async () => false, // User declines to trust workspace ); - expect(failed).toEqual(['ext1']); + expect(failed).toStrictEqual(['ext1']); }); it('does not copy extensions to the user dir when user declines trust', async () => { @@ -1659,7 +1659,7 @@ This extension will run the following MCP servers: ), ); - expect(extensions).toEqual([]); + expect(extensions).toStrictEqual([]); }); it('allows extension install when user approves trust prompt', async () => { @@ -1681,7 +1681,7 @@ This extension will run the following MCP servers: ); // Extension should install successfully when user approves - expect(failed).toEqual([]); + expect(failed).toStrictEqual([]); }); }); @@ -1711,7 +1711,7 @@ This extension will run the following MCP servers: async (_) => true, ); - expect(failed).toEqual([]); + expect(failed).toStrictEqual([]); const userExtensionsDir = path.join( tempHomeDir, @@ -1727,7 +1727,7 @@ This extension will run the following MCP servers: const metadataPath = path.join(userExt1Path, INSTALL_METADATA_FILENAME); expect(fs.existsSync(metadataPath)).toBe(true); const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); - expect(metadata).toEqual({ + expect(metadata).toStrictEqual({ source: ext1Path, type: 'local', }); @@ -1758,7 +1758,7 @@ This extension will run the following MCP servers: extensions, async (_) => true, ); - expect(failed).toEqual(['ext2']); + expect(failed).toStrictEqual(['ext2']); }); }); diff --git a/packages/cli/src/config/extension.ts b/packages/cli/src/config/extension.ts index 2ead1ee39b..e66571a217 100644 --- a/packages/cli/src/config/extension.ts +++ b/packages/cli/src/config/extension.ts @@ -5,8 +5,8 @@ */ import { - MCPServerConfig, - GeminiCLIExtension, + type MCPServerConfig, + type GeminiCLIExtension, Storage, getErrorMessage, type SkillDefinition, @@ -186,7 +186,10 @@ export function loadExtensions( const allExtensions = [...loadUserExtensions()]; - if ((isWorkspaceTrusted(settings) ?? true) && !settings.extensionManagement) { + if ( + (isWorkspaceTrusted(settings) ?? true) && + settings.extensionManagement !== true + ) { allExtensions.push(...getWorkspaceExtensions(workspaceDir)); } @@ -255,7 +258,7 @@ export function loadExtension( if ( (installMetadata?.type === 'git' || installMetadata?.type === 'github-release') && - settings.security?.blockGitExtensions + settings.security.blockGitExtensions === true ) { return null; } @@ -369,17 +372,17 @@ export function loadExtension( name: config.name, version: config.version, path: effectiveExtensionPath, - contextFiles, - installMetadata, mcpServers: config.mcpServers, excludeTools: config.excludeTools, - skills, - subagents, - isActive: true, // Barring any other signals extensions should be considered Active. + isActive: true, settings: config.settings as Array> | undefined, resolvedSettings: resolvedSettings as unknown as Array< Record >, + contextFiles, + installMetadata, + skills, + subagents, }; } catch (e) { console.error( @@ -433,10 +436,14 @@ export async function resolveExtensionSettingsWithSource( // Workspace overrides user when workspace value is explicitly set if (workspaceValue !== undefined && workspaceValue !== '') { - value = setting.sensitive ? '[value stored in keychain]' : workspaceValue; + value = + setting.sensitive === true + ? '[value stored in keychain]' + : workspaceValue; source = 'workspace'; } else if (userValue !== undefined && userValue !== '') { - value = setting.sensitive ? '[value stored in keychain]' : userValue; + value = + setting.sensitive === true ? '[value stored in keychain]' : userValue; source = 'user'; } else { value = '[not set]'; @@ -446,9 +453,9 @@ export async function resolveExtensionSettingsWithSource( return { name: setting.name, envVar: setting.envVar, - value, description: setting.description, sensitive: setting.sensitive ?? false, + value, source, }; }); @@ -489,15 +496,14 @@ export function loadInstallMetadata( const metadataFilePath = path.join(extensionDir, INSTALL_METADATA_FILENAME); try { const configContent = fs.readFileSync(metadataFilePath, 'utf-8'); - const metadata = JSON.parse(configContent) as ExtensionInstallMetadata; - return metadata; + return JSON.parse(configContent) as ExtensionInstallMetadata; } catch (_e) { return undefined; } } function getContextFileNames(config: ExtensionConfig): string[] { - if (!config.contextFileName) { + if (config.contextFileName == null) { return ['LLXPRT.md']; } else if (!Array.isArray(config.contextFileName)) { return [config.contextFileName]; @@ -549,10 +555,7 @@ export async function requestConsentNonInteractive( consentDescription: string, ): Promise { console.info(consentDescription); - const result = await promptForConsentNonInteractive( - 'Do you want to continue? [Y/n]: ', - ); - return result; + return promptForConsentNonInteractive('Do you want to continue? [Y/n]: '); } /** @@ -654,7 +657,7 @@ export async function inferInstallMetadata( } // Local path - verify it exists - if (ref || autoUpdate) { + if (ref != null || autoUpdate === true) { throw new Error( 'The --ref and --autoUpdate flags are only applicable for git-based installations.', ); @@ -682,7 +685,7 @@ export async function installOrUpdateExtension( if ( (installMetadata.type === 'git' || installMetadata.type === 'github-release') && - settings.security?.blockGitExtensions + settings.security.blockGitExtensions === true ) { throw new Error( 'Installing extensions from remote sources is disallowed by your current settings.', @@ -731,13 +734,8 @@ export async function installOrUpdateExtension( // Update the ref in metadata to the actual tag that was downloaded installMetadata.ref = result.tagName; localSourcePath = tempDir; - } else if ( - installMetadata.type === 'local' || - installMetadata.type === 'link' - ) { - localSourcePath = installMetadata.source; } else { - throw new Error(`Unsupported install type: ${installMetadata.type}`); + localSourcePath = installMetadata.source; } try { @@ -840,23 +838,26 @@ export function validateName(name: string): void { function extensionConsentString(extensionConfig: ExtensionConfig): string { const sanitizedConfig = escapeAnsiCtrlCodes(extensionConfig); const output: string[] = []; - const mcpServerEntries = Object.entries(sanitizedConfig.mcpServers || {}); + const mcpServerEntries = Object.entries(sanitizedConfig.mcpServers ?? {}); output.push(`Installing extension "${sanitizedConfig.name}".`); output.push( '**Extensions may introduce unexpected behavior. Ensure you have investigated the extension source and trust the author.**', ); - if (mcpServerEntries.length) { + if (mcpServerEntries.length > 0) { output.push('This extension will run the following MCP servers:'); for (const [key, mcpServer] of mcpServerEntries) { - const isLocal = !!mcpServer.command; + const isLocal = mcpServer.command != null; const source = mcpServer.httpUrl ?? - `${mcpServer.command || ''}${mcpServer.args ? ' ' + mcpServer.args.join(' ') : ''}`; + `${mcpServer.command ?? ''}${mcpServer.args ? ' ' + mcpServer.args.join(' ') : ''}`; output.push(` * ${key} (${isLocal ? 'local' : 'remote'}): ${source}`); } } - if (sanitizedConfig.hooks && Object.keys(sanitizedConfig.hooks).length > 0) { + if ( + sanitizedConfig.hooks != null && + Object.keys(sanitizedConfig.hooks).length > 0 + ) { output.push( `This extension will register hooks: ${Object.keys(sanitizedConfig.hooks).join(', ')}`, ); @@ -864,12 +865,12 @@ function extensionConsentString(extensionConfig: ExtensionConfig): string { 'Note: Hooks can intercept and modify LLxprt Code behavior. Additional consent will be requested.', ); } - if (sanitizedConfig.contextFileName) { + if (sanitizedConfig.contextFileName != null) { output.push( `This extension will append info to your LLXPRT.md context using ${sanitizedConfig.contextFileName}`, ); } - if (sanitizedConfig.excludeTools) { + if (sanitizedConfig.excludeTools != null) { output.push( `This extension will exclude the following core tools: ${sanitizedConfig.excludeTools}`, ); diff --git a/packages/cli/src/config/extensions/consent.test.ts b/packages/cli/src/config/extensions/consent.test.ts index f78a63d0e4..6779ca703d 100644 --- a/packages/cli/src/config/extensions/consent.test.ts +++ b/packages/cli/src/config/extensions/consent.test.ts @@ -65,7 +65,7 @@ describe('consent', () => { beforeEach(async () => { vi.clearAllMocks(); - if (originalReaddir.current) { + if (originalReaddir.current != null) { // eslint-disable-next-line @typescript-eslint/no-explicit-any mockReaddir.mockImplementation(originalReaddir.current as any); } @@ -135,8 +135,8 @@ describe('consent', () => { const delta = computeHookConsentDelta(currentHooks, previousHooks); - expect(delta.newHooks).toEqual(['post-install']); - expect(delta.changedHooks).toEqual([]); + expect(delta.newHooks).toStrictEqual(['post-install']); + expect(delta.changedHooks).toStrictEqual([]); }); it('should not require consent for unchanged hooks', () => { @@ -149,8 +149,8 @@ describe('consent', () => { const delta = computeHookConsentDelta(currentHooks, previousHooks); - expect(delta.newHooks).toEqual([]); - expect(delta.changedHooks).toEqual([]); + expect(delta.newHooks).toStrictEqual([]); + expect(delta.changedHooks).toStrictEqual([]); }); it('should not require consent for removed hooks', () => { @@ -164,8 +164,8 @@ describe('consent', () => { const delta = computeHookConsentDelta(currentHooks, previousHooks); - expect(delta.newHooks).toEqual([]); - expect(delta.changedHooks).toEqual([]); + expect(delta.newHooks).toStrictEqual([]); + expect(delta.changedHooks).toStrictEqual([]); }); it('should require consent for changed hook definitions', () => { @@ -178,8 +178,8 @@ describe('consent', () => { const delta = computeHookConsentDelta(currentHooks, previousHooks); - expect(delta.newHooks).toEqual([]); - expect(delta.changedHooks).toEqual(['pre-commit']); + expect(delta.newHooks).toStrictEqual([]); + expect(delta.changedHooks).toStrictEqual(['pre-commit']); }); it('should use sorted JSON comparison for hook definitions', () => { @@ -192,8 +192,8 @@ describe('consent', () => { const delta = computeHookConsentDelta(currentHooks, previousHooks); - expect(delta.newHooks).toEqual([]); - expect(delta.changedHooks).toEqual([]); + expect(delta.newHooks).toStrictEqual([]); + expect(delta.changedHooks).toStrictEqual([]); }); it('should treat case-sensitive hook names as distinct', () => { @@ -207,8 +207,8 @@ describe('consent', () => { const delta = computeHookConsentDelta(currentHooks, previousHooks); - expect(delta.newHooks).toEqual(['pre-commit']); - expect(delta.changedHooks).toEqual([]); + expect(delta.newHooks).toStrictEqual(['pre-commit']); + expect(delta.changedHooks).toStrictEqual([]); }); it('should handle undefined previous hooks', () => { @@ -218,8 +218,8 @@ describe('consent', () => { const delta = computeHookConsentDelta(currentHooks, undefined); - expect(delta.newHooks).toEqual(['pre-commit']); - expect(delta.changedHooks).toEqual([]); + expect(delta.newHooks).toStrictEqual(['pre-commit']); + expect(delta.changedHooks).toStrictEqual([]); }); it('should handle undefined current hooks', () => { @@ -229,8 +229,8 @@ describe('consent', () => { const delta = computeHookConsentDelta(undefined, previousHooks); - expect(delta.newHooks).toEqual([]); - expect(delta.changedHooks).toEqual([]); + expect(delta.newHooks).toStrictEqual([]); + expect(delta.changedHooks).toStrictEqual([]); }); }); @@ -338,12 +338,7 @@ describe('consent', () => { it('should request consent if there is no previous config', async () => { const requestConsent = vi.fn().mockResolvedValue(true); - await maybeRequestConsentOrFail( - baseConfig, - requestConsent, - false, - undefined, - ); + await maybeRequestConsentOrFail(baseConfig, requestConsent, false); expect(requestConsent).toHaveBeenCalledTimes(1); }); @@ -383,12 +378,7 @@ describe('consent', () => { it('should include warning when hooks are present', async () => { const requestConsent = vi.fn().mockResolvedValue(true); - await maybeRequestConsentOrFail( - baseConfig, - requestConsent, - true, - undefined, - ); + await maybeRequestConsentOrFail(baseConfig, requestConsent, true); expect(requestConsent).toHaveBeenCalledWith( expect.stringContaining( diff --git a/packages/cli/src/config/extensions/consent.ts b/packages/cli/src/config/extensions/consent.ts index 2109260efd..4f47a28015 100644 --- a/packages/cli/src/config/extensions/consent.ts +++ b/packages/cli/src/config/extensions/consent.ts @@ -122,7 +122,7 @@ export async function requestHookConsent( const consentPrompt = buildHookConsentPrompt(extensionName, hookNames); - if (requestConsent) { + if (requestConsent != null) { return requestConsent(consentPrompt); } @@ -175,10 +175,7 @@ export async function requestConsentNonInteractive( consentDescription: string, ): Promise { debugLogger.log(consentDescription); - const result = await promptForConsentNonInteractive( - 'Do you want to continue? [Y/n]: ', - ); - return result; + return promptForConsentNonInteractive('Do you want to continue? [Y/n]: '); } /** @@ -309,7 +306,7 @@ async function extensionConsentString( const isLocal = !!mcpServer.command; const source = mcpServer.httpUrl ?? - `${mcpServer.command || ''}${mcpServer.args ? ' ' + mcpServer.args.join(' ') : ''}`; + `${mcpServer.command || ''}${mcpServer.args != null ? ' ' + mcpServer.args.join(' ') : ''}`; output.push(` * ${key} (${isLocal ? 'local' : 'remote'}): ${source}`); } } @@ -318,7 +315,7 @@ async function extensionConsentString( `This extension will append info to your LLXPRT.md context using ${sanitizedConfig.contextFileName}`, ); } - if (sanitizedConfig.excludeTools) { + if (sanitizedConfig.excludeTools != null) { output.push( `This extension will exclude the following core tools: ${sanitizedConfig.excludeTools}`, ); @@ -360,7 +357,7 @@ export async function maybeRequestConsentOrFail( hasHooks, skills, ); - if (previousExtensionConfig) { + if (previousExtensionConfig != null) { const previousExtensionConsent = await extensionConsentString( previousExtensionConfig, previousHasHooks ?? false, diff --git a/packages/cli/src/config/extensions/extensionSettings.test.ts b/packages/cli/src/config/extensions/extensionSettings.test.ts index abdf6bfa8a..478851e748 100644 --- a/packages/cli/src/config/extensions/extensionSettings.test.ts +++ b/packages/cli/src/config/extensions/extensionSettings.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { ExtensionSettingSchema, ExtensionSettingsArraySchema, - ExtensionSetting, + type ExtensionSetting, } from './extensionSettings.js'; describe('ExtensionSettingSchema', () => { @@ -85,7 +85,7 @@ describe('ExtensionSettingSchema', () => { describe('ExtensionSettingsArraySchema', () => { it('should validate empty array', () => { const result = ExtensionSettingsArraySchema.parse([]); - expect(result).toEqual([]); + expect(result).toStrictEqual([]); }); it('should validate array of settings', () => { diff --git a/packages/cli/src/config/extensions/github.test.ts b/packages/cli/src/config/extensions/github.test.ts index d1b2a3a384..b54aaa190d 100644 --- a/packages/cli/src/config/extensions/github.test.ts +++ b/packages/cli/src/config/extensions/github.test.ts @@ -136,7 +136,7 @@ describe('git extension helpers', () => { ['git@github.com:owner/repo.git', 'owner', 'repo'], ['owner/repo', 'owner', 'repo'], ])('should parse %s to %s/%s', (url, owner, repo) => { - expect(tryParseGithubUrl(url)).toEqual({ owner, repo }); + expect(tryParseGithubUrl(url)).toStrictEqual({ owner, repo }); }); it.each([ @@ -340,14 +340,14 @@ describe('git extension helpers', () => { mockPlatform.mockReturnValue('darwin'); mockArch.mockReturnValue('arm64'); const result = findReleaseAsset(assets); - expect(result).toEqual(assets[0]); + expect(result).toStrictEqual(assets[0]); }); it('should find asset matching platform if arch does not match', () => { mockPlatform.mockReturnValue('linux'); mockArch.mockReturnValue('arm64'); const result = findReleaseAsset(assets); - expect(result).toEqual(assets[2]); + expect(result).toStrictEqual(assets[2]); }); it('should return undefined if no matching asset is found', () => { @@ -363,7 +363,7 @@ describe('git extension helpers', () => { mockPlatform.mockReturnValue('darwin'); mockArch.mockReturnValue('arm64'); const result = findReleaseAsset(singleAsset); - expect(result).toEqual(singleAsset[0]); + expect(result).toStrictEqual(singleAsset[0]); }); it('should return undefined if multiple generic assets exist', () => { @@ -577,7 +577,7 @@ describe('git extension helpers', () => { mockPlatform.mockReturnValue('darwin'); mockArch.mockReturnValue('x64'); - mockHttpsGet.mockImplementation((url, options, callback) => { + mockHttpsGet.mockImplementation((_url, _options, callback) => { const response = makeMockResponse(403); callback(response); return { on: vi.fn().mockReturnThis() }; diff --git a/packages/cli/src/config/extensions/github.ts b/packages/cli/src/config/extensions/github.ts index 8e3a25708d..81597b0be9 100644 --- a/packages/cli/src/config/extensions/github.ts +++ b/packages/cli/src/config/extensions/github.ts @@ -214,7 +214,7 @@ export async function checkForExtensionUpdate( extensionDir: installMetadata.source, workspaceDir: cwd, }); - if (!newExtension) { + if (newExtension == null) { debugLogger.warn( `Failed to check for update for local extension "${extension.name}". Could not load extension from source path: ${installMetadata.source}`, ); @@ -236,7 +236,7 @@ export async function checkForExtensionUpdate( } } if ( - !installMetadata || + installMetadata == null || (installMetadata.type !== 'git' && installMetadata.type !== 'github-release') ) { @@ -288,27 +288,26 @@ export async function checkForExtensionUpdate( } setExtensionUpdateState(ExtensionUpdateState.UPDATE_AVAILABLE); return; - } else { - const { source, releaseTag } = installMetadata; - if (!source) { - debugLogger.error(`No "source" provided for extension.`); - setExtensionUpdateState(ExtensionUpdateState.ERROR); - return; - } - const { owner, repo } = parseGitHubRepoForReleases(source); + } + const { source, releaseTag } = installMetadata; + if (!source) { + debugLogger.error(`No "source" provided for extension.`); + setExtensionUpdateState(ExtensionUpdateState.ERROR); + return; + } + const { owner, repo } = parseGitHubRepoForReleases(source); - const releaseData = await fetchReleaseFromGithub( - owner, - repo, - installMetadata.ref, - ); - if (releaseData.tag_name !== releaseTag) { - setExtensionUpdateState(ExtensionUpdateState.UPDATE_AVAILABLE); - return; - } - setExtensionUpdateState(ExtensionUpdateState.UP_TO_DATE); + const releaseData = await fetchReleaseFromGithub( + owner, + repo, + installMetadata.ref, + ); + if (releaseData.tag_name !== releaseTag) { + setExtensionUpdateState(ExtensionUpdateState.UPDATE_AVAILABLE); return; } + setExtensionUpdateState(ExtensionUpdateState.UP_TO_DATE); + return; } catch (error) { debugLogger.error( `Failed to check for updates for extension "${installMetadata.source}": ${getErrorMessage(error)}`, @@ -342,17 +341,15 @@ export async function downloadFromGitHubRelease( let isZip = false; let fileName: string | undefined; - if (asset) { + if (asset != null) { archiveUrl = asset.url; fileName = asset.name; - } else { - if (releaseData.tarball_url) { - archiveUrl = releaseData.tarball_url; - isTar = true; - } else if (releaseData.zipball_url) { - archiveUrl = releaseData.zipball_url; - isZip = true; - } + } else if (releaseData.tarball_url) { + archiveUrl = releaseData.tarball_url; + isTar = true; + } else if (releaseData.zipball_url) { + archiveUrl = releaseData.zipball_url; + isZip = true; } if (!archiveUrl) { throw new Error( @@ -375,7 +372,7 @@ export async function downloadFromGitHubRelease( // a 302 Redirect to the actual download location (codeload.github.com). // Sending 'application/octet-stream' for tarballs results in a 415 Unsupported Media Type error. const headers = { - ...(asset + ...(asset != null ? { Accept: 'application/octet-stream' } : { Accept: 'application/vnd.github+json' }), }; @@ -394,7 +391,7 @@ export async function downloadFromGitHubRelease( if (entries.length === 2) { const lonelyDir = entries.find((entry) => entry.isDirectory()); if ( - lonelyDir && + lonelyDir != null && (fs.existsSync( path.join(destination, lonelyDir.name, EXTENSIONS_CONFIG_FILENAME), ) || @@ -453,7 +450,7 @@ export function findReleaseAsset(assets: Asset[]): Asset | undefined { const platformArchAsset = assets.find((asset) => asset.name.toLowerCase().startsWith(platformArchPrefix), ); - if (platformArchAsset) { + if (platformArchAsset != null) { return platformArchAsset; } @@ -461,7 +458,7 @@ export function findReleaseAsset(assets: Asset[]): Asset | undefined { const platformAsset = assets.find((asset) => asset.name.toLowerCase().startsWith(platformPrefix), ); - if (platformAsset) { + if (platformAsset != null) { return platformAsset; } @@ -530,7 +527,7 @@ export async function downloadFile( const cleanupAndReject = (error: Error) => { // Try to clean up the partial file, but don't mask the original error fs.unlink(dest, (unlinkErr) => { - if (unlinkErr && unlinkErr.code !== 'ENOENT') { + if (unlinkErr != null && unlinkErr.code !== 'ENOENT') { // Log cleanup failure but still reject with original error debugLogger.error( `Failed to clean up partial file ${dest}:`, diff --git a/packages/cli/src/config/extensions/settingsIntegration.test.ts b/packages/cli/src/config/extensions/settingsIntegration.test.ts index 31b89939b1..be20096949 100644 --- a/packages/cli/src/config/extensions/settingsIntegration.test.ts +++ b/packages/cli/src/config/extensions/settingsIntegration.test.ts @@ -74,12 +74,12 @@ describe('settingsIntegration', () => { const settings = loadExtensionSettingsFromManifest(tempDir); expect(settings).toHaveLength(2); - expect(settings[0]).toEqual({ + expect(settings[0]).toStrictEqual({ name: 'API Key', envVar: 'API_KEY', sensitive: true, }); - expect(settings[1]).toEqual({ + expect(settings[1]).toStrictEqual({ name: 'API URL', description: 'The API endpoint URL', envVar: 'API_URL', @@ -102,12 +102,12 @@ describe('settingsIntegration', () => { const settings = loadExtensionSettingsFromManifest(tempDir); - expect(settings).toEqual([]); + expect(settings).toStrictEqual([]); }); it('should return empty array if manifest not found', () => { const settings = loadExtensionSettingsFromManifest(tempDir); - expect(settings).toEqual([]); + expect(settings).toStrictEqual([]); }); }); @@ -154,15 +154,15 @@ describe('settingsIntegration', () => { const contents = await getEnvContents('test-ext', tempDir); expect(contents).toHaveLength(3); - expect(contents[0]).toEqual({ + expect(contents[0]).toStrictEqual({ name: 'Public Setting', value: 'public-value', }); - expect(contents[1]).toEqual({ + expect(contents[1]).toStrictEqual({ name: 'Secret Setting', value: '[not set]', }); - expect(contents[2]).toEqual({ + expect(contents[2]).toStrictEqual({ name: 'Unset Setting', value: '[not set]', }); @@ -182,7 +182,7 @@ describe('settingsIntegration', () => { ); const contents = await getEnvContents('test-ext', tempDir); - expect(contents).toEqual([]); + expect(contents).toStrictEqual([]); }); }); @@ -689,7 +689,7 @@ describe('settingsIntegration', () => { ); expect(resolved).toHaveLength(1); - expect(resolved[0]).toEqual({ + expect(resolved[0]).toStrictEqual({ name: 'API Key', envVar: 'API_KEY', value: 'user-value', @@ -752,7 +752,7 @@ describe('settingsIntegration', () => { ); expect(resolved).toHaveLength(1); - expect(resolved[0]).toEqual({ + expect(resolved[0]).toStrictEqual({ name: 'API Key', envVar: 'API_KEY', value: 'workspace-value', @@ -794,7 +794,7 @@ describe('settingsIntegration', () => { ); expect(resolved).toHaveLength(1); - expect(resolved[0]).toEqual({ + expect(resolved[0]).toStrictEqual({ name: 'API Key', envVar: 'API_KEY', value: '[not set]', @@ -836,7 +836,7 @@ describe('settingsIntegration', () => { ); expect(resolved).toHaveLength(1); - expect(resolved[0]).toEqual({ + expect(resolved[0]).toStrictEqual({ name: 'Secret', envVar: 'SECRET', value: '[not set]', @@ -857,7 +857,7 @@ describe('settingsIntegration', () => { [], ); - expect(resolved).toEqual([]); + expect(resolved).toStrictEqual([]); }); }); }); diff --git a/packages/cli/src/config/extensions/settingsIntegration.ts b/packages/cli/src/config/extensions/settingsIntegration.ts index cb07880e43..8b06853821 100644 --- a/packages/cli/src/config/extensions/settingsIntegration.ts +++ b/packages/cli/src/config/extensions/settingsIntegration.ts @@ -223,10 +223,9 @@ export function getEnvFilePath( extensionName, '.env', ); - } else { - // User settings go in extension directory .env - return path.join(extensionDir, '.env'); } + // User settings go in extension directory .env + return path.join(extensionDir, '.env'); } /** @@ -335,7 +334,7 @@ export async function getEnvContents( * Checks both .env files and keychain for sensitive settings. */ export async function getMissingSettings( - extensionName: string, + _extensionName: string, extensionDir: string, ): Promise> { const settings = loadExtensionSettingsFromManifest(extensionDir); @@ -386,7 +385,7 @@ export async function updateSetting( s.envVar.toLowerCase() === settingKey.toLowerCase(), ); - if (!setting) { + if (setting == null) { debugLogger.error( `Setting "${settingKey}" not found in extension "${extensionName}".`, ); diff --git a/packages/cli/src/config/extensions/settingsPrompt.test.ts b/packages/cli/src/config/extensions/settingsPrompt.test.ts index 7c8efb30ef..4f2ffa959d 100644 --- a/packages/cli/src/config/extensions/settingsPrompt.test.ts +++ b/packages/cli/src/config/extensions/settingsPrompt.test.ts @@ -119,7 +119,7 @@ describe('maybePromptForSettings', () => { const result = await maybePromptForSettings(settings, existingValues); - expect(result).toEqual({ API_KEY: 'already-set' }); + expect(result).toStrictEqual({ API_KEY: 'already-set' }); expect(mockQuestion).not.toHaveBeenCalled(); }); @@ -130,14 +130,14 @@ describe('maybePromptForSettings', () => { const existingValues: Record = {}; // Simulate user entering a value - mockQuestion.mockImplementation((prompt, callback) => { + mockQuestion.mockImplementation((_prompt, callback) => { callback('user-entered-value'); }); const result = await maybePromptForSettings(settings, existingValues); expect(mockQuestion).toHaveBeenCalled(); - expect(result).toEqual({ API_KEY: 'user-entered-value' }); + expect(result).toStrictEqual({ API_KEY: 'user-entered-value' }); }); it('should return null when user enters empty value for required setting', async () => { @@ -147,7 +147,7 @@ describe('maybePromptForSettings', () => { const existingValues: Record = {}; // Simulate user pressing enter without value (cancel) - mockQuestion.mockImplementation((prompt, callback) => { + mockQuestion.mockImplementation((_prompt, callback) => { callback(''); }); @@ -163,13 +163,13 @@ describe('maybePromptForSettings', () => { ]; const existingValues = { API_URL: 'https://api.example.com' }; - mockQuestion.mockImplementation((prompt, callback) => { + mockQuestion.mockImplementation((_prompt, callback) => { callback('new-api-key'); }); const result = await maybePromptForSettings(settings, existingValues); - expect(result).toEqual({ + expect(result).toStrictEqual({ API_KEY: 'new-api-key', API_URL: 'https://api.example.com', }); @@ -180,7 +180,7 @@ describe('maybePromptForSettings', () => { { name: 'API Key', envVar: 'API_KEY', sensitive: true }, ]; - mockQuestion.mockImplementation((prompt, callback) => { + mockQuestion.mockImplementation((_prompt, callback) => { callback('value'); }); diff --git a/packages/cli/src/config/extensions/settingsStorage.ts b/packages/cli/src/config/extensions/settingsStorage.ts index 6ccfbfab3c..a35cedbaf2 100644 --- a/packages/cli/src/config/extensions/settingsStorage.ts +++ b/packages/cli/src/config/extensions/settingsStorage.ts @@ -46,9 +46,9 @@ export function getKeychainServiceName( const sanitized = extensionName.replace(/[^a-zA-Z0-9-_]/g, ''); // Check if this is a workspace-scoped path - const isWorkspaceScope = - extensionDir && - extensionDir.replace(/\\/g, '/').includes('.llxprt/extensions'); + const isWorkspaceScope = extensionDir + ?.replace(/\\/g, '/') + .includes('.llxprt/extensions'); if (isWorkspaceScope) { // Include workspace identifier for workspace scope diff --git a/packages/cli/src/config/extensions/update.test.ts b/packages/cli/src/config/extensions/update.test.ts index 0d8379821f..e8d9c0edc4 100644 --- a/packages/cli/src/config/extensions/update.test.ts +++ b/packages/cli/src/config/extensions/update.test.ts @@ -145,7 +145,7 @@ describe('update tests', () => { () => {}, ); - expect(updateInfo).toEqual({ + expect(updateInfo).toStrictEqual({ name: 'gemini-extensions', originalVersion: '1.0.0', updatedVersion: '1.1.0', diff --git a/packages/cli/src/config/extensions/update.ts b/packages/cli/src/config/extensions/update.ts index 05064751eb..064102c6cf 100644 --- a/packages/cli/src/config/extensions/update.ts +++ b/packages/cli/src/config/extensions/update.ts @@ -83,7 +83,7 @@ export async function updateExtension( extensionDir: updatedExtensionStorage.getExtensionDir(), workspaceDir: cwd, }); - if (!updatedExtension) { + if (updatedExtension == null) { dispatchExtensionStateUpdate({ type: 'SET_STATE', payload: { name: extension.name, state: ExtensionUpdateState.ERROR }, @@ -164,7 +164,7 @@ export async function checkForAllExtensionUpdates( try { const promises: Array> = []; for (const extension of extensions) { - if (!extension.installMetadata) { + if (extension.installMetadata == null) { dispatch({ type: 'SET_STATE', payload: { diff --git a/packages/cli/src/config/interactiveContext.ts b/packages/cli/src/config/interactiveContext.ts index 8dafa75d83..bcfa28cfc5 100644 --- a/packages/cli/src/config/interactiveContext.ts +++ b/packages/cli/src/config/interactiveContext.ts @@ -188,8 +188,7 @@ function resolveExtensions( } function resolveInteractiveMode(argv: CliArgs): boolean { - const hasPromptWords = - argv.promptWords && argv.promptWords.some((word) => word.trim() !== ''); + const hasPromptWords = argv.promptWords?.some((word) => word.trim() !== ''); return ( !!argv.promptInteractive || !!argv.experimentalAcp || diff --git a/packages/cli/src/config/keyBindings.test.ts b/packages/cli/src/config/keyBindings.test.ts index f99e4f72fa..e6a4a661c6 100644 --- a/packages/cli/src/config/keyBindings.test.ts +++ b/packages/cli/src/config/keyBindings.test.ts @@ -119,7 +119,7 @@ describe('keyBindings config', () => { it('has a description entry for every command', () => { const describedCommands = Object.keys(commandDescriptions); - expect(describedCommands.sort()).toEqual([...commandValues].sort()); + expect(describedCommands.sort()).toStrictEqual([...commandValues].sort()); for (const command of commandValues) { expect(typeof commandDescriptions[command]).toBe('string'); diff --git a/packages/cli/src/config/logging/loggingConfig.test.ts b/packages/cli/src/config/logging/loggingConfig.test.ts index 5e4581268a..3acaac9aed 100644 --- a/packages/cli/src/config/logging/loggingConfig.test.ts +++ b/packages/cli/src/config/logging/loggingConfig.test.ts @@ -110,7 +110,7 @@ class MockExtendedConfig implements ExtendedConfig { maxLogFiles?: number; }; }): void { - if (settings.telemetry) { + if (settings.telemetry != null) { this.telemetrySettings = { ...this.telemetrySettings, ...settings.telemetry, diff --git a/packages/cli/src/config/mcpServerConfig.ts b/packages/cli/src/config/mcpServerConfig.ts index 61fa96e47e..605ec604eb 100644 --- a/packages/cli/src/config/mcpServerConfig.ts +++ b/packages/cli/src/config/mcpServerConfig.ts @@ -56,15 +56,14 @@ export function allowedMcpServers( return isAllowed; }), ); - } else { - blockedMcpServers.push( - ...Object.entries(mcpServers).map(([key, server]) => ({ - name: key, - extensionName: server.extensionName || '', - })), - ); - return {}; } + blockedMcpServers.push( + ...Object.entries(mcpServers).map(([key, server]) => ({ + name: key, + extensionName: server.extensionName || '', + })), + ); + return {}; } export function resolveMcpServers( @@ -81,15 +80,15 @@ export function resolveMcpServers( ); const blockedMcpServers: Array<{ name: string; extensionName: string }> = []; - if (!allowedMcpServerNames) { - if (profileMergedSettings.allowMCPServers) { + if (allowedMcpServerNames == null) { + if (profileMergedSettings.allowMCPServers != null) { mcpServers = allowedMcpServers( mcpServers, profileMergedSettings.allowMCPServers, blockedMcpServers, ); } - if (profileMergedSettings.excludeMCPServers) { + if (profileMergedSettings.excludeMCPServers != null) { const excludedNames = new Set( profileMergedSettings.excludeMCPServers.filter(Boolean), ); @@ -108,7 +107,7 @@ export function resolveMcpServers( } } } - if (allowedMcpServerNames) { + if (allowedMcpServerNames != null) { mcpServers = allowedMcpServers( mcpServers, allowedMcpServerNames, diff --git a/packages/cli/src/config/policy.ts b/packages/cli/src/config/policy.ts index 24dc3a5301..94e930ce94 100644 --- a/packages/cli/src/config/policy.ts +++ b/packages/cli/src/config/policy.ts @@ -13,7 +13,7 @@ import { createPolicyEngineConfig as createCorePolicyEngineConfig, createPolicyUpdater as createCorePolicyUpdater, } from '@vybestack/llxprt-code-core'; -import { type Settings } from './settings.js'; +import type { Settings } from './settings.js'; export async function createPolicyEngineConfig( settings: Settings, diff --git a/packages/cli/src/config/postConfigRuntime.ts b/packages/cli/src/config/postConfigRuntime.ts index ec55a624d0..d1bbbfee5b 100644 --- a/packages/cli/src/config/postConfigRuntime.ts +++ b/packages/cli/src/config/postConfigRuntime.ts @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { +import type { + SettingsService, ApprovalMode, DebugLogger, ProfileManager, @@ -30,7 +31,6 @@ import type { CliArgs } from './cliArgParser.js'; import type { Settings } from './settings.js'; import type { ProfileLoadResult } from './profileResolution.js'; import type { ProviderModelResult } from './providerModelResolver.js'; -import type { SettingsService } from '@vybestack/llxprt-code-core'; const logger = new DebugLogger('llxprt:config:postConfigRuntime'); @@ -154,11 +154,11 @@ async function activateProviderAndProfile( const profileApplicationResult = await applyProfileToRuntime({ loadedProfile: profileLoadResult.loadedProfile, profileToLoad: profileLoadResult.profileToLoad ?? undefined, - bootstrapArgs, - argv, finalModel: providerModelResult.model, finalProvider: providerModelResult.provider, profileWarnings: [...profileLoadResult.profileWarnings], + bootstrapArgs, + argv, }); const finalProvider = profileApplicationResult.resolvedFinalProvider; @@ -168,7 +168,6 @@ async function activateProviderAndProfile( runtime: runtimeContext, providerManager: input.runtimeState.providerManager, oauthManager: input.runtimeState.oauthManager, - bootstrapArgs, profileApplication: { providerName: profileApplicationResult.resolvedProviderAfterProfile ?? finalProvider, @@ -180,6 +179,7 @@ async function activateProviderAndProfile( : {}), warnings: [...profileApplicationResult.profileWarnings], }, + bootstrapArgs, }); // Store bootstrap args on config @@ -246,11 +246,10 @@ async function reapplyCliOverrides( } if ( - bootstrapArgs && - (bootstrapArgs.keyOverride || - bootstrapArgs.keyfileOverride || - bootstrapArgs.baseurlOverride || - (bootstrapArgs.setOverrides && bootstrapArgs.setOverrides.length > 0)) + bootstrapArgs.keyOverride || + bootstrapArgs.keyfileOverride || + bootstrapArgs.baseurlOverride || + (bootstrapArgs.setOverrides && bootstrapArgs.setOverrides.length > 0) ) { const { applyCliArgumentOverrides } = await import( '../runtime/runtimeSettings.js' @@ -351,7 +350,7 @@ function applyEphemeralSettings(input: PostConfigInput): void { ); } if ( - profileSettingsWithTools.emojifilter && + profileSettingsWithTools.emojifilter != null && !settingsService.get('emojifilter') ) { settingsService.set('emojifilter', profileSettingsWithTools.emojifilter); @@ -362,7 +361,6 @@ function applyEphemeralSettings(input: PostConfigInput): void { const profileToLoad = profileLoadResult.profileToLoad; if ( (profileToLoad || bootstrapArgs.profileJson !== null) && - profileSettingsWithTools && argv.provider === undefined ) { const ephemeralKeys = [ diff --git a/packages/cli/src/config/profileBootstrap.ts b/packages/cli/src/config/profileBootstrap.ts index 6779818eab..e95cec7cb7 100644 --- a/packages/cli/src/config/profileBootstrap.ts +++ b/packages/cli/src/config/profileBootstrap.ts @@ -286,7 +286,7 @@ ${baseProfile.error}`); } if (setValues.length > 0) { - if (!bootstrapArgs.setOverrides) { + if (bootstrapArgs.setOverrides == null) { bootstrapArgs.setOverrides = []; } bootstrapArgs.setOverrides.push(...setValues); @@ -376,7 +376,7 @@ export async function prepareRuntimeForProfile( } as ProviderRuntimeContext; const runtimeMessageBus = runtimeInit.messageBus ?? - (runtimeConfig + (runtimeConfig != null ? new MessageBus( runtimeConfig.getPolicyEngine(), runtimeConfig.getDebugMode(), @@ -667,7 +667,7 @@ ${baseProfile.error}`); // Handle profileName (from --profile-load) if (bootstrapArgs.profileName !== null) { const settingsService = runtimeMetadata.settingsService; - if (settingsService) { + if (settingsService != null) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const profile = (settingsService as any).getProfile( bootstrapArgs.profileName, diff --git a/packages/cli/src/config/profileResolution.ts b/packages/cli/src/config/profileResolution.ts index e1e745fd20..7e8794d10f 100644 --- a/packages/cli/src/config/profileResolution.ts +++ b/packages/cli/src/config/profileResolution.ts @@ -293,7 +293,7 @@ export async function loadAndPrepareProfile(input: { settings, profileWarnings, ); - if (result) { + if (result != null) { ({ profileMergedSettings, profileModel, diff --git a/packages/cli/src/config/profileRuntimeApplication.ts b/packages/cli/src/config/profileRuntimeApplication.ts index bb8083cbc5..eb26d58607 100644 --- a/packages/cli/src/config/profileRuntimeApplication.ts +++ b/packages/cli/src/config/profileRuntimeApplication.ts @@ -95,7 +95,7 @@ export async function applyProfileToRuntime( logger.debug( () => - `[bootstrap] profileToLoad=${profileToLoad ?? 'none'} providerArg=${argv.provider ?? 'unset'} loadedProfile=${loadedProfile ? 'yes' : 'no'}`, + `[bootstrap] profileToLoad=${profileToLoad ?? 'none'} providerArg=${argv.provider ?? 'unset'} loadedProfile=${loadedProfile != null ? 'yes' : 'no'}`, ); if ( @@ -155,7 +155,7 @@ export async function applyProfileToRuntime( `[bootstrap] Applied CLI auth -> provider=${resolvedProviderAfterProfile}, model=${resolvedModelAfterProfile}, baseUrl=${resolvedBaseUrlAfterProfile ?? 'default'}`, ); } else if ( - loadedProfile && + loadedProfile != null && (profileToLoad || bootstrapArgs.profileJson !== null) && argv.provider === undefined ) { diff --git a/packages/cli/src/config/sandboxConfig.ts b/packages/cli/src/config/sandboxConfig.ts index 266f3e4e0a..c1fcdb36bd 100644 --- a/packages/cli/src/config/sandboxConfig.ts +++ b/packages/cli/src/config/sandboxConfig.ts @@ -11,7 +11,7 @@ import { } from '@vybestack/llxprt-code-core'; import commandExists from 'command-exists'; import * as os from 'node:os'; -import { Settings } from './settings.js'; +import type { Settings } from './settings.js'; import { resolvePath } from '../utils/resolvePath.js'; import { ensureDefaultSandboxProfiles, @@ -84,8 +84,8 @@ function parseMemoryLimit(memory: string): string { if (trimmed.length === 0) { throw new FatalSandboxError('Sandbox memory value cannot be empty'); } - const match = trimmed.match(/^(\d+)([kKmMgG])?$/); - if (!match) { + const match = RegExp(/^(\d+)([kKmMgG])?$/).exec(trimmed); + if (match == null) { throw new FatalSandboxError( `Invalid sandbox memory value '${memory}'. Expected values like 512m or 2g.`, ); @@ -136,7 +136,7 @@ function resolveMountPath(input: string): string { function normalizeEnvEntries( env: Record | undefined, ): Record | undefined { - if (!env) { + if (env == null) { return undefined; } const entries: Record = {}; @@ -162,7 +162,7 @@ function normalizeEnvEntries( function normalizeMounts( mounts: SandboxProfileMount[] | undefined, ): SandboxProfileMount[] | undefined { - if (!mounts) { + if (mounts == null) { return undefined; } return mounts.map((mount) => { @@ -215,7 +215,7 @@ function normalizeSandboxProfile(profile: SandboxProfile): SandboxProfile { normalizedSsh as (typeof VALID_SSH_AGENT_CHOICES)[number]; } - if (profile.resources) { + if (profile.resources != null) { normalized.resources = { ...profile.resources, cpus: @@ -351,7 +351,7 @@ function applyProfileEnvironment( ): Record { const env: Record = {}; - if (profile.env) { + if (profile.env != null) { for (const [key, value] of Object.entries(profile.env)) { env[key] = value; } @@ -377,7 +377,7 @@ function applyProfileEnvironment( env.LLXPRT_SANDBOX_PIDS = String(profile.resources.pids); } - if (profile.mounts && profile.mounts.length > 0) { + if (profile.mounts != null && profile.mounts.length > 0) { const mountsValue = profile.mounts .map((mount) => { const target = mount.to ?? mount.from; @@ -393,7 +393,7 @@ function applyProfileEnvironment( } function applySandboxProfileEnv(profile: SandboxProfile | undefined): void { - if (!profile) { + if (profile == null) { return; } const env = applyProfileEnvironment(profile); @@ -489,7 +489,7 @@ export async function loadSandboxConfig( } // Loading a sandbox profile implies sandboxing intent, even if --sandbox isn't set. - if (!baseCommand && sandboxProfile) { + if (!baseCommand && sandboxProfile != null) { baseCommand = commandExists.sync('docker') ? 'docker' : commandExists.sync('podman') diff --git a/packages/cli/src/config/settings-validation.ts b/packages/cli/src/config/settings-validation.ts index 00f7c06f0b..e0a27ca7c2 100644 --- a/packages/cli/src/config/settings-validation.ts +++ b/packages/cli/src/config/settings-validation.ts @@ -120,15 +120,14 @@ function buildEnumSchema( ...Array>, ], ); - } else { - return z.union( - values.map((v) => z.literal(v)) as [ - z.ZodLiteral, - z.ZodLiteral, - ...Array>, - ], - ); } + return z.union( + values.map((v) => z.literal(v)) as [ + z.ZodLiteral, + z.ZodLiteral, + ...Array>, + ], + ); } /** @@ -195,7 +194,7 @@ function buildZodSchemaFromDefinition( } case 'array': - if (definition.items) { + if (definition.items != null) { const itemSchema = buildZodSchemaFromCollection(definition.items); baseSchema = z.array(itemSchema); } else { @@ -204,17 +203,17 @@ function buildZodSchemaFromDefinition( break; case 'object': - if (definition.properties) { + if (definition.properties != null) { const shape = buildObjectShapeFromProperties(definition.properties); baseSchema = z.object(shape).passthrough(); - if (definition.additionalProperties) { + if (definition.additionalProperties != null) { const additionalSchema = buildZodSchemaFromCollection( definition.additionalProperties, ); baseSchema = z.object(shape).catchall(additionalSchema); } - } else if (definition.additionalProperties) { + } else if (definition.additionalProperties != null) { const valueSchema = buildZodSchemaFromCollection( definition.additionalProperties, ); @@ -253,14 +252,14 @@ function buildZodSchemaFromCollection( } case 'array': - if (collection.properties) { + if (collection.properties != null) { const shape = buildObjectShapeFromProperties(collection.properties); return z.array(z.object(shape)); } return z.array(z.unknown()); case 'object': - if (collection.properties) { + if (collection.properties != null) { const shape = buildObjectShapeFromProperties(collection.properties); return z.object(shape).passthrough(); } @@ -295,8 +294,7 @@ export function validateSettings(data: unknown): { data?: unknown; error?: z.ZodError; } { - const result = settingsZodSchema.safeParse(data); - return result; + return settingsZodSchema.safeParse(data); } /** diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 3d4df41c85..49696eaa62 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -21,13 +21,14 @@ import * as commentJson from 'comment-json'; import { DefaultLight } from '../ui/themes/default-light.js'; import { DefaultDark } from '../ui/themes/default.js'; import { isWorkspaceTrusted, isFolderTrustEnabled } from './trustedFolders.js'; -import { +import type { Settings, - type MergedSettings, MemoryImportFormat, - SETTINGS_SCHEMA, + MergedSettings, SettingDefinition, } from './settingsSchema.js'; +import { SETTINGS_SCHEMA } from './settingsSchema.js'; + import { resolveEnvVarsInObject } from '../utils/envVarResolver.js'; import { SETTINGS_DIRECTORY_NAME, @@ -91,9 +92,8 @@ export function getSystemSettingsPath(): string { return '/Library/Application Support/LLxprt-Code/settings.json'; } else if (platform() === 'win32') { return 'C:\\ProgramData\\llxprt-code\\settings.json'; - } else { - return '/etc/llxprt-code/settings.json'; } + return '/etc/llxprt-code/settings.json'; } export function getSystemDefaultsPath(): string { @@ -192,12 +192,10 @@ function getSchemaDefaults(): Partial { if ( schemaEntry.type === 'object' && 'properties' in schemaEntry && - schemaEntry.properties + schemaEntry.properties != null ) { // Initialize nested object if it doesn't exist - if (!target[key]) { - target[key] = {}; - } + target[key] ??= {}; extractDefaults( schemaEntry.properties as Record, target[key] as Record, @@ -256,109 +254,109 @@ function mergeSettings( ...safeWorkspace, ...system, ui: { - ...(schemaDefaults.ui || {}), + ...(schemaDefaults.ui ?? {}), // Migrate legacy top-level UI settings to ui.* namespace for backwards compatibility // This ensures users with old settings.json files don't lose their preferences ...extractLegacyUiKeys(systemDefaults), - ...(systemDefaults.ui || {}), + ...(systemDefaults.ui ?? {}), ...extractLegacyUiKeys(user), - ...(user.ui || {}), + ...(user.ui ?? {}), ...extractLegacyUiKeys(safeWorkspace), - ...(safeWorkspace.ui || {}), + ...(safeWorkspace.ui ?? {}), ...extractLegacyUiKeys(system), - ...(system.ui || {}), + ...(system.ui ?? {}), customThemes: { - ...(systemDefaults.ui?.customThemes || {}), - ...(user.ui?.customThemes || {}), - ...(safeWorkspace.ui?.customThemes || {}), - ...(system.ui?.customThemes || {}), + ...(systemDefaults.ui?.customThemes ?? {}), + ...(user.ui?.customThemes ?? {}), + ...(safeWorkspace.ui?.customThemes ?? {}), + ...(system.ui?.customThemes ?? {}), }, }, mcpServers: { - ...(systemDefaults.mcpServers || {}), - ...(user.mcpServers || {}), - ...(safeWorkspace.mcpServers || {}), - ...(system.mcpServers || {}), + ...(systemDefaults.mcpServers ?? {}), + ...(user.mcpServers ?? {}), + ...(safeWorkspace.mcpServers ?? {}), + ...(system.mcpServers ?? {}), }, includeDirectories: [ - ...(systemDefaults.includeDirectories || []), - ...(user.includeDirectories || []), - ...(safeWorkspace.includeDirectories || []), - ...(system.includeDirectories || []), + ...(systemDefaults.includeDirectories ?? []), + ...(user.includeDirectories ?? []), + ...(safeWorkspace.includeDirectories ?? []), + ...(system.includeDirectories ?? []), ], chatCompression: { - ...(systemDefaults.chatCompression || {}), - ...(user.chatCompression || {}), - ...(safeWorkspace.chatCompression || {}), - ...(system.chatCompression || {}), + ...(systemDefaults.chatCompression ?? {}), + ...(user.chatCompression ?? {}), + ...(safeWorkspace.chatCompression ?? {}), + ...(system.chatCompression ?? {}), }, security: { - ...(schemaDefaults.security || {}), - ...(systemDefaults.security || {}), - ...(user.security || {}), - ...(safeWorkspace.security || {}), - ...(system.security || {}), + ...(schemaDefaults.security ?? {}), + ...(systemDefaults.security ?? {}), + ...(user.security ?? {}), + ...(safeWorkspace.security ?? {}), + ...(system.security ?? {}), }, telemetry: { - ...(schemaDefaults.telemetry || {}), - ...(systemDefaults.telemetry || {}), - ...(user.telemetry || {}), - ...(safeWorkspace.telemetry || {}), - ...(system.telemetry || {}), + ...(schemaDefaults.telemetry ?? {}), + ...(systemDefaults.telemetry ?? {}), + ...(user.telemetry ?? {}), + ...(safeWorkspace.telemetry ?? {}), + ...(system.telemetry ?? {}), }, mcp: { - ...(schemaDefaults.mcp || {}), - ...(systemDefaults.mcp || {}), - ...(user.mcp || {}), - ...(safeWorkspace.mcp || {}), - ...(system.mcp || {}), + ...(schemaDefaults.mcp ?? {}), + ...(systemDefaults.mcp ?? {}), + ...(user.mcp ?? {}), + ...(safeWorkspace.mcp ?? {}), + ...(system.mcp ?? {}), }, tools: { - ...(schemaDefaults.tools || {}), - ...(systemDefaults.tools || {}), - ...(user.tools || {}), - ...(safeWorkspace.tools || {}), - ...(system.tools || {}), + ...(schemaDefaults.tools ?? {}), + ...(systemDefaults.tools ?? {}), + ...(user.tools ?? {}), + ...(safeWorkspace.tools ?? {}), + ...(system.tools ?? {}), }, extensions: { - ...(systemDefaults.extensions || {}), - ...(user.extensions || {}), - ...(safeWorkspace.extensions || {}), - ...(system.extensions || {}), + ...(systemDefaults.extensions ?? {}), + ...(user.extensions ?? {}), + ...(safeWorkspace.extensions ?? {}), + ...(system.extensions ?? {}), disabled: [ ...new Set([ - ...(systemDefaults.extensions?.disabled || []), - ...(user.extensions?.disabled || []), - ...(safeWorkspace.extensions?.disabled || []), - ...(system.extensions?.disabled || []), + ...(systemDefaults.extensions?.disabled ?? []), + ...(user.extensions?.disabled ?? []), + ...(safeWorkspace.extensions?.disabled ?? []), + ...(system.extensions?.disabled ?? []), ]), ], workspacesWithMigrationNudge: [ ...new Set([ - ...(systemDefaults.extensions?.workspacesWithMigrationNudge || []), - ...(user.extensions?.workspacesWithMigrationNudge || []), - ...(safeWorkspace.extensions?.workspacesWithMigrationNudge || []), - ...(system.extensions?.workspacesWithMigrationNudge || []), + ...(systemDefaults.extensions?.workspacesWithMigrationNudge ?? []), + ...(user.extensions?.workspacesWithMigrationNudge ?? []), + ...(safeWorkspace.extensions?.workspacesWithMigrationNudge ?? []), + ...(system.extensions?.workspacesWithMigrationNudge ?? []), ]), ], }, // coreToolSettings is UI-only and should not be merged from settings files // It only exists in memory for the UI and manipulates excludeTools/allowedTools // But it should have schema defaults for proper UI display - coreToolSettings: schemaDefaults.coreToolSettings || {}, + coreToolSettings: schemaDefaults.coreToolSettings ?? {}, hooksConfig: { - ...(schemaDefaults.hooksConfig || {}), - ...(systemDefaults.hooksConfig || {}), - ...(user.hooksConfig || {}), - ...(safeWorkspace.hooksConfig || {}), - ...(system.hooksConfig || {}), + ...(schemaDefaults.hooksConfig ?? {}), + ...(systemDefaults.hooksConfig ?? {}), + ...(user.hooksConfig ?? {}), + ...(safeWorkspace.hooksConfig ?? {}), + ...(system.hooksConfig ?? {}), }, hooks: { - ...(schemaDefaults.hooks || {}), - ...(systemDefaults.hooks || {}), - ...(user.hooks || {}), - ...(safeWorkspace.hooks || {}), - ...(system.hooks || {}), + ...(schemaDefaults.hooks ?? {}), + ...(systemDefaults.hooks ?? {}), + ...(user.hooks ?? {}), + ...(safeWorkspace.hooks ?? {}), + ...(system.hooks ?? {}), }, }; @@ -368,20 +366,14 @@ function mergeSettings( system.ui?.theme ?? systemDefaults.ui?.theme ?? schemaDefaults.ui?.theme; - if (merged.ui) { - merged.ui.theme = prioritizedTheme; - } + merged.ui.theme = prioritizedTheme; return merged as MergedSettings; } function migrateLegacyInteractiveShellSetting(settings: Settings): void { - if (!settings || typeof settings !== 'object') { - return; - } - const tools = settings.tools; - if (!tools || typeof tools !== 'object') { + if (tools == null || typeof tools !== 'object') { return; } @@ -395,7 +387,7 @@ function migrateLegacyInteractiveShellSetting(settings: Settings): void { } const legacyShell = toolSettings['shell']; - if (legacyShell && typeof legacyShell === 'object') { + if (legacyShell != null && typeof legacyShell === 'object') { const shellSettings = legacyShell as Record; const shellFlag = shellSettings['enableInteractiveShell']; if (typeof shellFlag === 'boolean') { @@ -421,10 +413,6 @@ function migrateLegacyInteractiveShellSetting(settings: Settings): void { * Called per-scope before merging, so each scope's settings file is independently migrated. */ function migrateHooksConfig(settings: Settings): void { - if (!settings || typeof settings !== 'object') { - return; - } - const hooks = settings.hooks as Record | undefined; if (!hooks) return; @@ -433,7 +421,8 @@ function migrateHooksConfig(settings: Settings): void { if (!needsMigration) return; - const hooksConfig = (settings.hooksConfig as Record) ?? {}; + const hooksConfig: Record = + (settings.hooksConfig as Record | undefined) ?? {}; const newHooks: Record = {}; for (const [key, value] of Object.entries(hooks)) { @@ -501,6 +490,7 @@ export class LoadedSettings { return this.system; case SettingScope.SystemDefaults: return this.systemDefaults; + case SettingScope.Session: default: throw new Error(`Invalid scope: ${scope}`); } @@ -520,7 +510,7 @@ export class LoadedSettings { const nested = parts.slice(1).join('.'); // Ensure the top-level object exists - if (!settingsFile.settings[topLevel]) { + if (settingsFile.settings[topLevel] == null) { (settingsFile.settings as Record)[topLevel] = {}; } @@ -528,9 +518,7 @@ export class LoadedSettings { let current = settingsFile.settings[topLevel] as Record; const nestedParts = nested.split('.'); for (let i = 0; i < nestedParts.length - 1; i++) { - if (!current[nestedParts[i]]) { - current[nestedParts[i]] = {}; - } + current[nestedParts[i]] ??= {}; current = current[nestedParts[i]] as Record; } current[nestedParts[nestedParts.length - 1]] = value; @@ -545,25 +533,25 @@ export class LoadedSettings { // Provider keyfile methods for llxprt multi-provider support getProviderKeyfile(providerName: string): string | undefined { - const keyfiles = this.merged.providerKeyfiles || {}; + const keyfiles = this.merged.providerKeyfiles ?? {}; return keyfiles[providerName]; } setProviderKeyfile(providerName: string, keyfilePath: string): void { - const keyfiles = this.merged.providerKeyfiles || {}; + const keyfiles = this.merged.providerKeyfiles ?? {}; keyfiles[providerName] = keyfilePath; this.setValue(SettingScope.User, 'providerKeyfiles', keyfiles); } removeProviderKeyfile(providerName: string): void { - const keyfiles = this.merged.providerKeyfiles || {}; + const keyfiles = this.merged.providerKeyfiles ?? {}; delete keyfiles[providerName]; this.setValue(SettingScope.User, 'providerKeyfiles', keyfiles); } // OAuth enablement methods getOAuthEnabledProviders(): Record { - return this.merged.oauthEnabledProviders || {}; + return this.merged.oauthEnabledProviders ?? {}; } // Note: setRemoteAdminSettings from upstream omitted - not applicable to LLxprt (no Google admin integration) @@ -586,7 +574,7 @@ export class LoadedSettings { function findEnvFile(startDir: string): string | null { let currentDir = path.resolve(startDir); - while (true) { + for (;;) { // prefer gemini-specific .env under LLXPRT_DIR const geminiEnvPath = path.join(currentDir, LLXPRT_DIR, '.env'); if (fs.existsSync(geminiEnvPath)) { @@ -597,7 +585,7 @@ function findEnvFile(startDir: string): string | null { return envPath; } const parentDir = path.dirname(currentDir); - if (parentDir === currentDir || !parentDir) { + if (parentDir === currentDir) { // check .env under home as fallback, again preferring gemini-specific .env const homeGeminiEnvPath = path.join(homedir(), LLXPRT_DIR, '.env'); if (fs.existsSync(homeGeminiEnvPath)) { @@ -660,7 +648,7 @@ export function loadEnvironment(settings: Settings): void { const parsedEnv = dotenv.parse(envFileContent); const excludedVars = - settings?.excludedProjectEnvVars || DEFAULT_EXCLUDED_ENV_VARS; + settings.excludedProjectEnvVars ?? DEFAULT_EXCLUDED_ENV_VARS; const isProjectEnvFile = !envFilePath.includes(LLXPRT_DIR); for (const key in parsedEnv) { @@ -690,7 +678,7 @@ function validateSettingsOrThrow( filePath: string, ): void { const validationResult = validateSettings(settingsObject); - if (!validationResult.success && validationResult.error) { + if (!validationResult.success && validationResult.error != null) { const errorMessage = formatValidationError( validationResult.error, filePath, diff --git a/packages/cli/src/config/settingsSchema.test.ts b/packages/cli/src/config/settingsSchema.test.ts index d80d6a1d70..5e46d9bacd 100644 --- a/packages/cli/src/config/settingsSchema.test.ts +++ b/packages/cli/src/config/settingsSchema.test.ts @@ -11,7 +11,7 @@ import { type SettingCollectionDefinition, type SettingDefinition, SETTINGS_SCHEMA, - Settings, + type Settings, getEnableHooks, getEnableHooksUI, } from './settingsSchema.js'; @@ -133,7 +133,7 @@ describe('SettingsSchema', () => { const defWithProps = definition as typeof definition & { properties?: Record; }; - if (defWithProps.properties) { + if (defWithProps.properties != null) { Object.values(defWithProps.properties).forEach( (nestedDef: unknown) => { const nestedDefTyped = nestedDef as { category?: string }; @@ -169,7 +169,7 @@ describe('SettingsSchema', () => { def.type !== 'boolean' || ['boolean', 'undefined'].includes(typeof def.default), ).toBe(true); - if (def.properties) { + if (def.properties != null) { checkBooleanDefaults(def.properties); } }, @@ -233,7 +233,7 @@ describe('SettingsSchema', () => { // TypeScript should not complain about these properties expect(settings.ui?.theme).toBe('dark'); - expect(settings.includeDirectories).toEqual(['/path/to/dir']); + expect(settings.includeDirectories).toStrictEqual(['/path/to/dir']); expect(settings.loadMemoryFromIncludeDirectories).toBe(true); }); @@ -241,7 +241,7 @@ describe('SettingsSchema', () => { expect(SETTINGS_SCHEMA.includeDirectories).toBeDefined(); expect(SETTINGS_SCHEMA.includeDirectories.type).toBe('array'); expect(SETTINGS_SCHEMA.includeDirectories.category).toBe('General'); - expect(SETTINGS_SCHEMA.includeDirectories.default).toEqual([]); + expect(SETTINGS_SCHEMA.includeDirectories.default).toStrictEqual([]); }); it('should have loadMemoryFromIncludeDirectories setting in schema', () => { @@ -269,7 +269,7 @@ describe('SettingsSchema', () => { expect(SETTINGS_SCHEMA.defaultDisabledTools).toBeDefined(); expect(SETTINGS_SCHEMA.defaultDisabledTools.type).toBe('array'); expect(SETTINGS_SCHEMA.defaultDisabledTools.category).toBe('Advanced'); - expect(SETTINGS_SCHEMA.defaultDisabledTools.default).toEqual([ + expect(SETTINGS_SCHEMA.defaultDisabledTools.default).toStrictEqual([ 'google_web_fetch', ]); expect(SETTINGS_SCHEMA.defaultDisabledTools.showInDialog).toBe(false); @@ -288,13 +288,13 @@ describe('SettingsSchema', () => { missing.push(definition.ref); } } - if (definition.properties) { + if (definition.properties != null) { Object.values(definition.properties).forEach(visitDefinition); } - if (definition.items) { + if (definition.items != null) { visitCollection(definition.items); } - if (definition.additionalProperties) { + if (definition.additionalProperties != null) { visitCollection(definition.additionalProperties); } }; @@ -307,10 +307,10 @@ describe('SettingsSchema', () => { } return; } - if (collection.properties) { + if (collection.properties != null) { Object.values(collection.properties).forEach(visitDefinition); } - if (collection.type === 'array' && collection.properties) { + if (collection.type === 'array' && collection.properties != null) { Object.values(collection.properties).forEach(visitDefinition); } }; @@ -318,13 +318,13 @@ describe('SettingsSchema', () => { Object.values(schema).forEach(visitDefinition); // Check all referenced definitions exist - expect(missing).toEqual([]); + expect(missing).toStrictEqual([]); // Ensure definitions map doesn't accumulate stale entries. const unreferenced = Object.keys(SETTINGS_SCHEMA_DEFINITIONS).filter( (key) => !referenced.has(key), ); - expect(unreferenced).toEqual([]); + expect(unreferenced).toStrictEqual([]); }); }); }); diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 8011129255..5faa7496b6 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -5,17 +5,17 @@ */ import { - MCPServerConfig, - BugCommandSettings, - TelemetrySettings, - ChatCompressionSettings, DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD, DEFAULT_TRUNCATE_TOOL_OUTPUT_LINES, - HookDefinition, - HookEventName, + type MCPServerConfig, + type BugCommandSettings, + type TelemetrySettings, + type ChatCompressionSettings, + type HookDefinition, + type HookEventName, } from '@vybestack/llxprt-code-core'; -import { CustomTheme } from '../ui/themes/theme.js'; -import { type WittyPhraseStyle } from '../ui/constants/phrasesCollections.js'; +import type { CustomTheme } from '../ui/themes/theme.js'; +import type { WittyPhraseStyle } from '../ui/constants/phrasesCollections.js'; import type { SessionRetentionSettings } from './settings.js'; export type SettingsType = diff --git a/packages/cli/src/config/toolGovernance.ts b/packages/cli/src/config/toolGovernance.ts index 3f76c7f4a8..f761a70d1d 100644 --- a/packages/cli/src/config/toolGovernance.ts +++ b/packages/cli/src/config/toolGovernance.ts @@ -11,8 +11,8 @@ import { SHELL_TOOL_NAMES, type ApprovalMode, ApprovalMode as ApprovalModeEnum, + type GeminiCLIExtension, } from '@vybestack/llxprt-code-core'; -import type { GeminiCLIExtension } from '@vybestack/llxprt-code-core'; import type { Settings } from './settings.js'; import type { CliArgs } from './cliArgParser.js'; import type { ContextResolutionResult } from './interactiveContext.js'; diff --git a/packages/cli/src/config/trustedFolders.test.ts b/packages/cli/src/config/trustedFolders.test.ts index 0bdcd51466..fc320157b3 100644 --- a/packages/cli/src/config/trustedFolders.test.ts +++ b/packages/cli/src/config/trustedFolders.test.ts @@ -79,8 +79,8 @@ describe('Trusted Folders Loading', () => { it('should load empty rules if no files exist', () => { const { rules, errors } = loadTrustedFolders(); - expect(rules).toEqual([]); - expect(errors).toEqual([]); + expect(rules).toStrictEqual([]); + expect(errors).toStrictEqual([]); }); describe('isPathTrusted', () => { @@ -141,10 +141,10 @@ describe('Trusted Folders Loading', () => { }); const { rules, errors } = loadTrustedFolders(); - expect(rules).toEqual([ + expect(rules).toStrictEqual([ { path: '/user/folder', trustLevel: TrustLevel.TRUST_FOLDER }, ]); - expect(errors).toEqual([]); + expect(errors).toStrictEqual([]); }); it('should handle JSON parsing errors gracefully', () => { @@ -156,7 +156,7 @@ describe('Trusted Folders Loading', () => { }); const { rules, errors } = loadTrustedFolders(); - expect(rules).toEqual([]); + expect(rules).toStrictEqual([]); expect(errors.length).toBe(1); expect(errors[0].path).toBe(userPath); expect(errors[0].message).toContain('Unexpected token'); @@ -176,13 +176,13 @@ describe('Trusted Folders Loading', () => { }); const { rules, errors } = loadTrustedFolders(); - expect(rules).toEqual([ + expect(rules).toStrictEqual([ { path: '/user/folder/from/env', trustLevel: TrustLevel.TRUST_FOLDER, }, ]); - expect(errors).toEqual([]); + expect(errors).toStrictEqual([]); delete process.env['LLXPRT_CODE_TRUSTED_FOLDERS_PATH']; }); diff --git a/packages/cli/src/config/trustedFolders.ts b/packages/cli/src/config/trustedFolders.ts index 624ac05dd5..e88b1ab16f 100644 --- a/packages/cli/src/config/trustedFolders.ts +++ b/packages/cli/src/config/trustedFolders.ts @@ -137,7 +137,7 @@ export function resetTrustedFoldersForTesting(): void { } export function loadTrustedFolders(): LoadedTrustedFolders { - if (loadedTrustedFolders) { + if (loadedTrustedFolders != null) { return loadedTrustedFolders; } @@ -208,8 +208,7 @@ export function saveTrustedFolders( /** Is folder trust feature enabled per the current applied settings */ export function isFolderTrustEnabled(settings: Settings): boolean { // In llxprt, we use flat settings structure - const folderTrustSetting = settings.folderTrust ?? false; - return folderTrustSetting; + return settings.folderTrust ?? false; } function getWorkspaceTrustFromLocalConfig(): boolean | undefined { diff --git a/packages/cli/src/config/welcomeConfig.ts b/packages/cli/src/config/welcomeConfig.ts index 10daabcc13..429f0ef6df 100644 --- a/packages/cli/src/config/welcomeConfig.ts +++ b/packages/cli/src/config/welcomeConfig.ts @@ -31,7 +31,7 @@ export function resetWelcomeConfigForTesting(): void { } export function loadWelcomeConfig(): WelcomeConfig { - if (cachedConfig) { + if (cachedConfig != null) { return cachedConfig; } diff --git a/packages/cli/src/coreToolToggle.test.ts b/packages/cli/src/coreToolToggle.test.ts index 174c1bdd05..03c052a608 100644 --- a/packages/cli/src/coreToolToggle.test.ts +++ b/packages/cli/src/coreToolToggle.test.ts @@ -19,8 +19,11 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import type { Config } from '@vybestack/llxprt-code-core'; -import { DiscoveredTool, DiscoveredMCPTool } from '@vybestack/llxprt-code-core'; +import type { + Config, + DiscoveredTool, + DiscoveredMCPTool, +} from '@vybestack/llxprt-code-core'; import { generateDynamicToolSettings } from './utils/dynamicSettings.js'; // Mock the settings utilities @@ -230,8 +233,8 @@ describe('generateDynamicToolSettings', () => { }); it('should return empty object when config is undefined', () => { - const toolSettings = generateDynamicToolSettings(undefined); - expect(toolSettings).toEqual({}); + const toolSettings = generateDynamicToolSettings(); + expect(toolSettings).toStrictEqual({}); }); it('should handle empty tool registry', () => { @@ -273,7 +276,7 @@ describe('generateDynamicToolSettings', () => { } as unknown as Config; const toolSettings = generateDynamicToolSettings(errorConfig); - expect(toolSettings).toEqual({}); + expect(toolSettings).toStrictEqual({}); }); }); @@ -371,7 +374,7 @@ describe('generateDynamicToolSettings', () => { return toolName === toolKey; }); - if (!actualTool) { + if (actualTool == null) { console.error(`Tool not found for key: ${toolKey}`); } @@ -457,8 +460,8 @@ describe('generateDynamicToolSettings', () => { const excludeTools = getEffectiveValue('excludeTools', {}, {}); const allowedTools = getEffectiveValue('allowedTools', {}, {}); - expect(excludeTools).toEqual(['Tool1', 'Tool2']); - expect(allowedTools).toEqual(['Tool3']); + expect(excludeTools).toStrictEqual(['Tool1', 'Tool2']); + expect(allowedTools).toStrictEqual(['Tool3']); }); it('should handle empty arrays for excludeTools and allowedTools', async () => { @@ -476,8 +479,8 @@ describe('generateDynamicToolSettings', () => { const excludeTools = getEffectiveValue('excludeTools', {}, {}); const allowedTools = getEffectiveValue('allowedTools', {}, {}); - expect(excludeTools).toEqual([]); - expect(allowedTools).toEqual([]); + expect(excludeTools).toStrictEqual([]); + expect(allowedTools).toStrictEqual([]); }); it('should handle undefined values for excludeTools and allowedTools', async () => { @@ -495,8 +498,8 @@ describe('generateDynamicToolSettings', () => { const excludeTools = getEffectiveValue('excludeTools', {}, {}) || []; const allowedTools = getEffectiveValue('allowedTools', {}, {}) || []; - expect(excludeTools).toEqual([]); - expect(allowedTools).toEqual([]); + expect(excludeTools).toStrictEqual([]); + expect(allowedTools).toStrictEqual([]); }); }); }); diff --git a/packages/cli/src/extensions/extensionAutoUpdater.test.ts b/packages/cli/src/extensions/extensionAutoUpdater.test.ts index 1b1a915025..e327dbf1b8 100644 --- a/packages/cli/src/extensions/extensionAutoUpdater.test.ts +++ b/packages/cli/src/extensions/extensionAutoUpdater.test.ts @@ -88,10 +88,10 @@ describe('ExtensionAutoUpdater', () => { ...createUpdaterOptions({ installMode: 'immediate' }), extensionLoader: loader, updateChecker: checker, - updateExecutor, stateStore: store, notify: (message, level) => messages.push({ message, level }), now: () => 1000, + updateExecutor, }); await updater.checkNow(); @@ -128,9 +128,9 @@ describe('ExtensionAutoUpdater', () => { ...createUpdaterOptions({ installMode: 'on-restart' }), extensionLoader: loader, updateChecker: checker, - updateExecutor, stateStore: store, now: () => 2000, + updateExecutor, }); await updater.checkNow(); @@ -160,10 +160,10 @@ describe('ExtensionAutoUpdater', () => { ...createUpdaterOptions({ installMode: 'manual' }), extensionLoader: loader, updateChecker: checker, - updateExecutor, stateStore: store, notify: (message, level) => messages.push({ message, level }), now: () => 3000, + updateExecutor, }); await updater.checkNow(); diff --git a/packages/cli/src/extensions/extensionAutoUpdater.ts b/packages/cli/src/extensions/extensionAutoUpdater.ts index 22bfe38a69..9ba1002ddb 100644 --- a/packages/cli/src/extensions/extensionAutoUpdater.ts +++ b/packages/cli/src/extensions/extensionAutoUpdater.ts @@ -225,7 +225,7 @@ export class ExtensionAutoUpdater { } stop(): void { - if (this.intervalHandle) { + if (this.intervalHandle != null) { clearInterval(this.intervalHandle); this.intervalHandle = null; } @@ -286,7 +286,7 @@ export class ExtensionAutoUpdater { continue; } const extension = extensionsByName.get(name); - if (!extension || !extension.installMetadata) { + if (extension?.installMetadata == null) { entry.pendingInstall = false; entry.lastError = `Extension "${name}" is no longer installed.`; entry.state = ExtensionUpdateState.ERROR; @@ -301,7 +301,7 @@ export class ExtensionAutoUpdater { extension: GeminiCLIExtension, state: ExtensionUpdateStateFile, ): Promise { - if (!extension.installMetadata) { + if (extension.installMetadata == null) { return; } @@ -415,8 +415,8 @@ export class ExtensionAutoUpdater { }, ); - if (!info) { - throw new Error('Update returned undefined'); + if (info == null) { + throw new Error('Update returned null or undefined'); } entry.lastUpdate = this.now(); @@ -450,7 +450,7 @@ export class ExtensionAutoUpdater { return; } - if (this.notify) { + if (this.notify != null) { this.notify(message, level); return; } diff --git a/packages/cli/src/gemini.provider-init.test.ts b/packages/cli/src/gemini.provider-init.test.ts index b3db52b88b..9fe8801973 100644 --- a/packages/cli/src/gemini.provider-init.test.ts +++ b/packages/cli/src/gemini.provider-init.test.ts @@ -310,10 +310,9 @@ describe('gemini main provider initialization', () => { })), getScreenReader: vi.fn(() => false), getTerminalBackground: vi.fn(() => undefined), - - getGeminiClient, setTerminalBackground: vi.fn(), getPolicyEngine: vi.fn(() => null), + getGeminiClient, } as unknown as Config; const coreModule = await import('@vybestack/llxprt-code-core'); diff --git a/packages/cli/src/gemini.renderOptions.test.tsx b/packages/cli/src/gemini.renderOptions.test.tsx index 45208381a8..4459114091 100644 --- a/packages/cli/src/gemini.renderOptions.test.tsx +++ b/packages/cli/src/gemini.renderOptions.test.tsx @@ -120,7 +120,7 @@ describe('startInteractiveUI ink render options', () => { expect(renderSpy).toHaveBeenCalledTimes(1); const [_reactElement, options] = renderSpy.mock.calls[0]; const expectedRenderOptions = inkRenderOptions(config, settings); - expect(options).toEqual( + expect(options).toStrictEqual( expect.objectContaining({ exitOnCtrlC: false, patchConsole: false, @@ -149,7 +149,7 @@ describe('startInteractiveUI ink render options', () => { expect(renderSpy).toHaveBeenCalledTimes(1); const [_reactElement, options] = renderSpy.mock.calls[0]; - expect(options).toEqual( + expect(options).toStrictEqual( expect.objectContaining({ exitOnCtrlC: false, patchConsole: false, diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 706b09f47d..8c5fa5bcde 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -13,7 +13,7 @@ import { formatNonInteractiveError, } from './gemini.js'; import { - LoadedSettings, + type LoadedSettings, // SettingsFile, // Currently unused loadSettings, } from './config/settings.js'; @@ -203,7 +203,7 @@ describe('gemini.tsx main function', () => { (listener) => !initialUnhandledRejectionListeners.includes(listener), ); - if (addedListener) { + if (addedListener != null) { process.removeListener('unhandledRejection', addedListener); } vi.restoreAllMocks(); @@ -886,7 +886,7 @@ describe('startInteractiveUI', () => { const [reactElement, options] = renderSpy.mock.calls[0]; // Verify render options - expect(options).toEqual( + expect(options).toStrictEqual( expect.objectContaining({ exitOnCtrlC: false, }), diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 959550d58f..c5ebd76095 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -22,7 +22,7 @@ if (wantWarningSuppression && !process.env.NODE_NO_WARNINGS) { const warningCode = typeof warning === 'string' ? undefined - : typeof warning?.code === 'string' + : typeof warning.code === 'string' ? warning.code : undefined; if (warningCode && suppressedWarningCodes.has(warningCode)) { @@ -31,12 +31,12 @@ if (wantWarningSuppression && !process.env.NODE_NO_WARNINGS) { const message = typeof warning === 'string' ? warning - : (warning?.stack ?? warning?.message ?? String(warning)); + : (warning.stack ?? warning.message ?? String(warning)); debugLogger.warn(message); }); } -import React, { ErrorInfo, useState, useEffect } from 'react'; +import React, { type ErrorInfo, useState, useEffect } from 'react'; import { render, Box, Text } from 'ink'; import Spinner from 'ink-spinner'; import { AppWrapper } from './ui/App.js'; @@ -61,12 +61,12 @@ import { import { relaunchAppInChildProcess } from './utils/relaunch.js'; import chalk from 'chalk'; import { - DnsResolutionOrder, - LoadedSettings, + type DnsResolutionOrder, + type LoadedSettings, loadSettings, } from './config/settings.js'; import { - Config, + type Config, sessionId, setGitStatsService, FatalConfigError, @@ -162,7 +162,7 @@ export function formatNonInteractiveError(error: unknown): string { return error.stack ?? error.message; } - if (error && typeof error === 'object') { + if (error != null && typeof error === 'object') { try { return JSON.stringify(error, null, 2); } catch { @@ -379,7 +379,10 @@ export async function main() { const workspaceRoot = process.cwd(); const settings = loadSettings(workspaceRoot); - if (settings.merged.ui.autoConfigureMaxOldSpaceSize && !process.env.SANDBOX) { + if ( + settings.merged.ui.autoConfigureMaxOldSpaceSize === true && + !process.env.SANDBOX + ) { // Only relaunch with a larger heap when the autosizing setting is enabled. const debugMode = isDebugMode(); const memoryArgs = shouldRelaunchForMemory(debugMode); @@ -391,7 +394,7 @@ export async function main() { const argv = await parseArguments(settings.merged); - const hasPipedInput = !process.stdin.isTTY && !argv.experimentalAcp; + const hasPipedInput = !process.stdin.isTTY && argv.experimentalAcp !== true; let cachedStdinData: string | null = null; let stdinWasRead = false; @@ -404,7 +407,7 @@ export async function main() { }; const questionFromArgs = - argv.promptInteractive || argv.prompt || (argv.promptWords || []).join(' '); + argv.promptInteractive ?? argv.prompt ?? (argv.promptWords ?? []).join(' '); await cleanupCheckpoints(); @@ -428,7 +431,7 @@ export async function main() { // If we're in ACP mode, redirect console output IMMEDIATELY // before any config loading that might write to stdout - if (argv.experimentalAcp) { + if (argv.experimentalAcp === true) { // eslint-disable-next-line no-console console.log = console.error; // eslint-disable-next-line no-console @@ -501,15 +504,19 @@ export async function main() { stdinManager.enable(); // This cleanup isn't strictly needed but may help in certain situations. - process.on('SIGTERM', async () => { - stdinManager.disable(true); // Restore to wasRaw - await runExitCleanup(); - process.exit(0); + process.on('SIGTERM', () => { + void (async () => { + stdinManager.disable(true); // Restore to wasRaw + await runExitCleanup(); + process.exit(0); + })(); }); - process.on('SIGINT', async () => { - stdinManager.disable(true); // Restore to wasRaw - await runExitCleanup(); - process.exit(130); // Standard exit code for SIGINT + process.on('SIGINT', () => { + void (async () => { + stdinManager.disable(true); // Restore to wasRaw + await runExitCleanup(); + process.exit(130); // Standard exit code for SIGINT + })(); }); // Register cleanup for the stdin manager to ensure error handler is removed @@ -532,12 +539,10 @@ export async function main() { const consolePatcher = new ConsolePatcher({ stderr: !( - config.getOutputFormat?.() === OutputFormat.JSON && - !config.isInteractive() + config.getOutputFormat() === OutputFormat.JSON && !config.isInteractive() ), debugMode: - config.getOutputFormat?.() === OutputFormat.JSON && - !config.isInteractive() + config.getOutputFormat() === OutputFormat.JSON && !config.isInteractive() ? false : config.getDebugMode(), }); @@ -554,7 +559,7 @@ export async function main() { } const bootstrapProfileName = - argv.profileLoad?.trim() || + argv.profileLoad?.trim() ?? (typeof process.env.LLXPRT_BOOTSTRAP_PROFILE === 'string' ? process.env.LLXPRT_BOOTSTRAP_PROFILE.trim() : ''); @@ -703,10 +708,7 @@ export async function main() { ? cliModelFromBootstrap : config.getModel(); - if ( - (!configModel || configModel === 'placeholder-model') && - activeProvider.getDefaultModel - ) { + if (!configModel || configModel === 'placeholder-model') { // No model specified or placeholder, get the provider's default configModel = activeProvider.getDefaultModel(); } @@ -730,17 +732,15 @@ export async function main() { Object.assign(mergedModelParams, configWithParams._cliModelParams); } - if (activeProvider) { - const existingParams = getActiveModelParams(); + const existingParams = getActiveModelParams(); - for (const [key, value] of Object.entries(mergedModelParams)) { - setActiveModelParam(key, value); - } + for (const [key, value] of Object.entries(mergedModelParams)) { + setActiveModelParam(key, value); + } - for (const key of Object.keys(existingParams)) { - if (!(key in mergedModelParams)) { - clearActiveModelParam(key); - } + for (const key of Object.keys(existingParams)) { + if (!(key in mergedModelParams)) { + clearActiveModelParam(key); } } @@ -774,7 +774,7 @@ export async function main() { // computeSandboxMemoryArgs() always returns args because the sandbox starts fresh // with Node.js default ~950MB heap. let sandboxMemoryArgs: string[] = []; - if (settings.merged.ui.autoConfigureMaxOldSpaceSize) { + if (settings.merged.ui.autoConfigureMaxOldSpaceSize === true) { const containerMemoryStr = process.env.LLXPRT_SANDBOX_MEMORY ?? process.env.SANDBOX_MEMORY; let containerMemoryMB: number | undefined; @@ -919,7 +919,7 @@ export async function main() { await fsPromises.mkdir(chatsDir, { recursive: true }); // --list-sessions: display sessions and exit - if (argv.listSessions) { + if (argv.listSessions === true) { const { sessions } = await listSessions(chatsDir, projectHash); if (sessions.length === 0) { debugLogger.log('No recorded sessions for this project.'); @@ -1014,9 +1014,7 @@ export async function main() { if (resumedHistory && resumedHistory.length > 0) { try { const geminiClient = config.getGeminiClient(); - if (geminiClient) { - await geminiClient.restoreHistory(resumedHistory); - } + await geminiClient.restoreHistory(resumedHistory); } catch (err) { const messageText = err instanceof Error ? err.message : String(err); debugLogger.warn( @@ -1140,11 +1138,11 @@ export async function main() { try { await runNonInteractive({ config: nonInteractiveConfig, + runtimeMessageBus: sessionMessageBus, + deferTelemetryShutdown: true, settings, input, prompt_id, - runtimeMessageBus: sessionMessageBus, - deferTelemetryShutdown: true, }); // Fire SessionEnd hook on successful completion @@ -1180,13 +1178,13 @@ export async function main() { } function setWindowTitle(title: string, settings: LoadedSettings) { - if (!settings.merged.ui.hideWindowTitle) { + if (settings.merged.ui.hideWindowTitle !== true) { // Initial state before React loop starts const windowTitle = computeTerminalTitle({ streamingState: StreamingState.Idle, isConfirming: false, folderName: title, - showThoughts: !!settings.merged.ui.showStatusInTitle, + showThoughts: settings.merged.ui.showStatusInTitle === true, useDynamicTitle: settings.merged.ui.dynamicWindowTitle ?? true, }); writeToStdout(`\x1b]0;${windowTitle}\x07`); diff --git a/packages/cli/src/integration-tests/__tests__/oauth-buckets.integration.spec.ts b/packages/cli/src/integration-tests/__tests__/oauth-buckets.integration.spec.ts index 2bed911e4e..7a0629e963 100644 --- a/packages/cli/src/integration-tests/__tests__/oauth-buckets.integration.spec.ts +++ b/packages/cli/src/integration-tests/__tests__/oauth-buckets.integration.spec.ts @@ -564,7 +564,7 @@ describe('Phase 10: OAuth Buckets Integration Testing', () => { expect(token1).not.toBeNull(); expect(token2).not.toBeNull(); - if (token1 && token2) { + if (token1 != null && token2 != null) { const now = Math.floor(Date.now() / 1000); expect(token1.expiry).toBeLessThan(now); expect(token2.expiry).toBeLessThan(now); @@ -956,7 +956,7 @@ describe('Phase 10: OAuth Buckets Integration Testing', () => { expect(geminiWork).not.toBeNull(); // Verify expiry is tracked correctly - if (anthropicWork && geminiWork) { + if (anthropicWork != null && geminiWork != null) { const now = Math.floor(Date.now() / 1000); expect(anthropicWork.expiry).toBeGreaterThan(now); // Not expired expect(geminiWork.expiry).toBeGreaterThan(now); // Not expired @@ -968,7 +968,7 @@ describe('Phase 10: OAuth Buckets Integration Testing', () => { 'personal-gmail', ); expect(anthropicPersonal).not.toBeNull(); - if (anthropicPersonal) { + if (anthropicPersonal != null) { const now = Math.floor(Date.now() / 1000); expect(anthropicPersonal.expiry).toBeLessThan(now); // Expired } diff --git a/packages/cli/src/integration-tests/base-url-behavior.integration.test.ts b/packages/cli/src/integration-tests/base-url-behavior.integration.test.ts index 89b58514ad..2ea0f258ba 100644 --- a/packages/cli/src/integration-tests/base-url-behavior.integration.test.ts +++ b/packages/cli/src/integration-tests/base-url-behavior.integration.test.ts @@ -7,12 +7,12 @@ import { beforeEach, afterEach, describe, expect, it } from 'vitest'; import { Config, - Profile, + type Profile, ProfileManager, - ProviderManager, - IProvider, + type ProviderManager, + type IProvider, createProviderRuntimeContext, - SettingsService, + type SettingsService, MessageBus, } from '@vybestack/llxprt-code-core'; import { diff --git a/packages/cli/src/integration-tests/cli-args.integration.test.ts b/packages/cli/src/integration-tests/cli-args.integration.test.ts index 3c4c98a830..57aa819d98 100644 --- a/packages/cli/src/integration-tests/cli-args.integration.test.ts +++ b/packages/cli/src/integration-tests/cli-args.integration.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { spawn } from 'child_process'; import * as path from 'path'; import * as fs from 'fs/promises'; -import { ProfileManager, Profile } from '@vybestack/llxprt-code-core'; +import { ProfileManager, type Profile } from '@vybestack/llxprt-code-core'; import { createTempDirectory, cleanupTempDirectory, diff --git a/packages/cli/src/integration-tests/compression-settings-apply.integration.test.ts b/packages/cli/src/integration-tests/compression-settings-apply.integration.test.ts index 8c615fbcef..59318647a2 100644 --- a/packages/cli/src/integration-tests/compression-settings-apply.integration.test.ts +++ b/packages/cli/src/integration-tests/compression-settings-apply.integration.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { Config, - Profile, + type Profile, ProfileManager, SettingsService, } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/integration-tests/compression-todo.integration.test.ts b/packages/cli/src/integration-tests/compression-todo.integration.test.ts index 9b676f642e..f7077e90a8 100644 --- a/packages/cli/src/integration-tests/compression-todo.integration.test.ts +++ b/packages/cli/src/integration-tests/compression-todo.integration.test.ts @@ -27,7 +27,7 @@ import * as path from 'path'; import * as os from 'os'; import { TodoStore, - Todo, + type Todo, buildContinuationDirective, } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/integration-tests/ephemeral-settings.integration.test.ts b/packages/cli/src/integration-tests/ephemeral-settings.integration.test.ts index 3506c732b5..2cc334f9ec 100644 --- a/packages/cli/src/integration-tests/ephemeral-settings.integration.test.ts +++ b/packages/cli/src/integration-tests/ephemeral-settings.integration.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { Config, - Profile, + type Profile, ProfileManager, ProviderManager, SettingsService, @@ -83,7 +83,7 @@ describe('Ephemeral Settings Integration Tests', () => { 'https://api.example.com', ); expect(config.getEphemeralSetting('auth-key')).toBe('test-key-123'); - expect(config.getEphemeralSetting('custom-headers')).toEqual({ + expect(config.getEphemeralSetting('custom-headers')).toStrictEqual({ 'X-Custom-Header': 'test-value', Authorization: 'Bearer token123', }); @@ -122,7 +122,7 @@ describe('Ephemeral Settings Integration Tests', () => { config.setEphemeralSetting('custom-headers', customHeaders); // Verify custom headers are stored - expect(config.getEphemeralSetting('custom-headers')).toEqual( + expect(config.getEphemeralSetting('custom-headers')).toStrictEqual( customHeaders, ); @@ -275,7 +275,7 @@ describe('Ephemeral Settings Integration Tests', () => { expect(newConfig.getEphemeralSetting('base-url')).toBe( 'https://api.profile.com', ); - expect(newConfig.getEphemeralSetting('custom-headers')).toEqual({ + expect(newConfig.getEphemeralSetting('custom-headers')).toStrictEqual({ 'X-Profile-Header': 'profile-value', }); @@ -362,7 +362,7 @@ describe('Ephemeral Settings Integration Tests', () => { it('should handle complex ephemeral setting values', () => { // Arrays config.setEphemeralSetting('array-setting', ['a', 'b', 'c']); - expect(config.getEphemeralSetting('array-setting')).toEqual([ + expect(config.getEphemeralSetting('array-setting')).toStrictEqual([ 'a', 'b', 'c', @@ -377,7 +377,9 @@ describe('Ephemeral Settings Integration Tests', () => { }, }; config.setEphemeralSetting('nested-object', nestedObj); - expect(config.getEphemeralSetting('nested-object')).toEqual(nestedObj); + expect(config.getEphemeralSetting('nested-object')).toStrictEqual( + nestedObj, + ); // Numbers, booleans, null config.setEphemeralSetting('number', 42); @@ -398,7 +400,7 @@ describe('Ephemeral Settings Integration Tests', () => { // Should return new objects each time expect(settings1).not.toBe(settings2); - expect(settings1).toEqual(settings2); + expect(settings1).toStrictEqual(settings2); // Modifying returned object should not affect internal state settings1['key1'] = 'modified'; diff --git a/packages/cli/src/integration-tests/model-params-isolation.integration.test.ts b/packages/cli/src/integration-tests/model-params-isolation.integration.test.ts index fcbc1c0440..14a8dc9166 100644 --- a/packages/cli/src/integration-tests/model-params-isolation.integration.test.ts +++ b/packages/cli/src/integration-tests/model-params-isolation.integration.test.ts @@ -8,8 +8,8 @@ import { beforeEach, afterEach, describe, expect, it } from 'vitest'; import { Config, ProviderManager, - Profile, - SettingsService, + type Profile, + type SettingsService, MessageBus, type IProvider, } from '@vybestack/llxprt-code-core'; @@ -118,26 +118,26 @@ describe('Runtime model parameter isolation', () => { }); it('keeps model parameters scoped to the active provider', async () => { - expect(getActiveModelParams()).toEqual({}); + expect(getActiveModelParams()).toStrictEqual({}); setActiveModelParam('temperature', 0.8); setActiveModelParam('max_tokens', 2048); - expect(getActiveModelParams()).toEqual({ + expect(getActiveModelParams()).toStrictEqual({ temperature: 0.8, max_tokens: 2048, }); expect(settingsService.getProviderSettings('alpha').temperature).toBe(0.8); await switchActiveProvider('beta'); - expect(getActiveModelParams()).toEqual({}); + expect(getActiveModelParams()).toStrictEqual({}); expect(settingsService.getProviderSettings('alpha').temperature).toBe(0.8); setActiveModelParam('temperature', 0.35); - expect(getActiveModelParams()).toEqual({ temperature: 0.35 }); + expect(getActiveModelParams()).toStrictEqual({ temperature: 0.35 }); await switchActiveProvider('alpha'); - expect(getActiveModelParams()).toEqual({}); + expect(getActiveModelParams()).toStrictEqual({}); expect( settingsService.getProviderSettings('alpha').temperature, ).toBeUndefined(); @@ -151,7 +151,7 @@ describe('Runtime model parameter isolation', () => { const snapshot = buildRuntimeProfileSnapshot(); expect(snapshot.provider).toBe('alpha'); expect(snapshot.model).toBe('alpha-model'); - expect(snapshot.modelParams).toEqual({ + expect(snapshot.modelParams).toStrictEqual({ top_p: 0.91, response_format: { type: 'json_object' }, }); @@ -182,9 +182,9 @@ describe('Runtime model parameter isolation', () => { it('clears individual model params via helper', () => { setActiveModelParam('temperature', 0.42); - expect(getActiveModelParams()).toEqual({ temperature: 0.42 }); + expect(getActiveModelParams()).toStrictEqual({ temperature: 0.42 }); clearActiveModelParam('temperature'); - expect(getActiveModelParams()).toEqual({}); + expect(getActiveModelParams()).toStrictEqual({}); }); }); diff --git a/packages/cli/src/integration-tests/modelParams.integration.test.ts b/packages/cli/src/integration-tests/modelParams.integration.test.ts index e2fca13416..7d6efba397 100644 --- a/packages/cli/src/integration-tests/modelParams.integration.test.ts +++ b/packages/cli/src/integration-tests/modelParams.integration.test.ts @@ -8,7 +8,7 @@ import { beforeEach, afterEach, describe, expect, it } from 'vitest'; import { Config, ProviderManager, - SettingsService, + type SettingsService, MessageBus, type IProvider, } from '@vybestack/llxprt-code-core'; @@ -68,7 +68,7 @@ describe('CLI model parameter command integration', () => { let context: ReturnType; const runSetCommand = async (args: string) => { - if (!setCommand.action) { + if (setCommand.action == null) { throw new Error('setCommand.action is not defined'); } return setCommand.action(context, args); @@ -123,16 +123,16 @@ describe('CLI model parameter command integration', () => { it('sets provider-scoped model params via /set modelparam', async () => { await runSetCommand('modelparam temperature 0.9'); - expect(getActiveModelParams()).toEqual({ temperature: 0.9 }); + expect(getActiveModelParams()).toStrictEqual({ temperature: 0.9 }); expect(settingsService.getProviderSettings('alpha').temperature).toBe(0.9); }); it('clears model params using /set unset modelparam', async () => { await runSetCommand('modelparam max_tokens 4096'); - expect(getActiveModelParams()).toEqual({ max_tokens: 4096 }); + expect(getActiveModelParams()).toStrictEqual({ max_tokens: 4096 }); await runSetCommand('unset modelparam max_tokens'); - expect(getActiveModelParams()).toEqual({}); + expect(getActiveModelParams()).toStrictEqual({}); expect( settingsService.getProviderSettings('alpha').max_tokens, ).toBeUndefined(); @@ -144,7 +144,7 @@ describe('CLI model parameter command integration', () => { const snapshot = buildRuntimeProfileSnapshot(); expect(snapshot.provider).toBe('alpha'); - expect(snapshot.modelParams).toEqual({ + expect(snapshot.modelParams).toStrictEqual({ response_format: { type: 'json_object' }, top_p: 0.92, }); @@ -152,9 +152,9 @@ describe('CLI model parameter command integration', () => { it('supports clearing params directly through helper', async () => { await runSetCommand('modelparam temperature 0.7'); - expect(getActiveModelParams()).toEqual({ temperature: 0.7 }); + expect(getActiveModelParams()).toStrictEqual({ temperature: 0.7 }); clearActiveModelParam('temperature'); - expect(getActiveModelParams()).toEqual({}); + expect(getActiveModelParams()).toStrictEqual({}); }); }); diff --git a/packages/cli/src/integration-tests/oauth-timing.integration.test.ts b/packages/cli/src/integration-tests/oauth-timing.integration.test.ts index 45e1b7f888..d31b4b3c4a 100644 --- a/packages/cli/src/integration-tests/oauth-timing.integration.test.ts +++ b/packages/cli/src/integration-tests/oauth-timing.integration.test.ts @@ -24,7 +24,7 @@ import { type OAuthManager as CoreOAuthManager, SettingsService, ProfileManager, - Profile, + type Profile, SecureStore, type KeyringAdapter, } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/integration-tests/profile-keyfile.integration.test.ts b/packages/cli/src/integration-tests/profile-keyfile.integration.test.ts index 30a5517180..cb06671e72 100644 --- a/packages/cli/src/integration-tests/profile-keyfile.integration.test.ts +++ b/packages/cli/src/integration-tests/profile-keyfile.integration.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; -import { ProfileManager, Profile } from '@vybestack/llxprt-code-core'; +import { ProfileManager, type Profile } from '@vybestack/llxprt-code-core'; import { createTempDirectory, cleanupTempDirectory, @@ -466,12 +466,12 @@ describe('Profile with Keyfile Integration Tests', () => { const profile: Profile = { version: 1, - provider, model: 'test-model', modelParams: {}, ephemeralSettings: { 'auth-keyfile': keyfilePath, }, + provider, }; await profileManager.saveProfile(name, profile); diff --git a/packages/cli/src/integration-tests/profile-system.integration.test.ts b/packages/cli/src/integration-tests/profile-system.integration.test.ts index 808e3be54d..ee0c9c5e25 100644 --- a/packages/cli/src/integration-tests/profile-system.integration.test.ts +++ b/packages/cli/src/integration-tests/profile-system.integration.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { ProfileManager, Profile } from '@vybestack/llxprt-code-core'; +import { ProfileManager, type Profile } from '@vybestack/llxprt-code-core'; import { createTempDirectory, cleanupTempDirectory, @@ -85,7 +85,7 @@ describe('Profile Save/Load Cycle Integration Tests', () => { const loadedProfile = await profileManager.loadProfile('test-profile'); // Verify all fields match - expect(loadedProfile).toEqual(testProfile); + expect(loadedProfile).toStrictEqual(testProfile); expect(loadedProfile.version).toBe(1); expect(loadedProfile.provider).toBe('anthropic'); expect(loadedProfile.model).toBe('claude-3-5-sonnet-20240620'); @@ -139,7 +139,7 @@ describe('Profile Save/Load Cycle Integration Tests', () => { // Load and verify each profile for (const [name, expectedProfile] of Object.entries(profiles)) { const loadedProfile = await profileManager.loadProfile(name); - expect(loadedProfile).toEqual(expectedProfile); + expect(loadedProfile).toStrictEqual(expectedProfile); } }); }); @@ -180,7 +180,7 @@ describe('Profile Save/Load Cycle Integration Tests', () => { const parsedContent = JSON.parse(rawContent); // Verify JSON structure - expect(parsedContent).toEqual(profile); + expect(parsedContent).toStrictEqual(profile); // Verify formatting (should be pretty-printed with 2-space indentation) expect(rawContent).toContain(' "version": 1'); @@ -255,7 +255,7 @@ describe('Profile Save/Load Cycle Integration Tests', () => { it('should handle empty profile list gracefully', async () => { // Don't create any profiles const profileList = await profileManager.listProfiles(); - expect(profileList).toEqual([]); + expect(profileList).toStrictEqual([]); }); it('should create profiles directory if it does not exist', async () => { @@ -387,10 +387,10 @@ describe('Profile Save/Load Cycle Integration Tests', () => { const loaded = await profileManager.loadProfile('comprehensive'); // Verify all fields are preserved - expect(loaded).toEqual(comprehensiveProfile); + expect(loaded).toStrictEqual(comprehensiveProfile); expect(loaded.modelParams.logprobs).toBe(true); expect(loaded.modelParams.top_logprobs).toBe(3); - expect(loaded.ephemeralSettings['custom-headers']).toEqual({ + expect(loaded.ephemeralSettings['custom-headers']).toStrictEqual({ 'X-Organization-ID': 'org-123', 'X-Project-ID': 'proj-456', 'X-Custom-Tracking': 'enabled', @@ -409,9 +409,9 @@ describe('Profile Save/Load Cycle Integration Tests', () => { await profileManager.saveProfile('minimal', minimalProfile); const loaded = await profileManager.loadProfile('minimal'); - expect(loaded).toEqual(minimalProfile); - expect(loaded.modelParams).toEqual({}); - expect(loaded.ephemeralSettings).toEqual({}); + expect(loaded).toStrictEqual(minimalProfile); + expect(loaded.modelParams).toStrictEqual({}); + expect(loaded.ephemeralSettings).toStrictEqual({}); }); }); @@ -446,7 +446,7 @@ describe('Profile Save/Load Cycle Integration Tests', () => { for (const name of specialNames) { expect(profileList).toContain(name); const loaded = await profileManager.loadProfile(name); - expect(loaded).toEqual(baseProfile); + expect(loaded).toStrictEqual(baseProfile); } }); }); diff --git a/packages/cli/src/integration-tests/provider-multi-runtime.integration.test.ts b/packages/cli/src/integration-tests/provider-multi-runtime.integration.test.ts index d4dca74d56..1a7704d2fc 100644 --- a/packages/cli/src/integration-tests/provider-multi-runtime.integration.test.ts +++ b/packages/cli/src/integration-tests/provider-multi-runtime.integration.test.ts @@ -12,7 +12,7 @@ */ import { afterEach, describe, expect, it } from 'vitest'; -import { type IProvider, type Profile } from '@vybestack/llxprt-code-core'; +import type { IProvider, Profile } from '@vybestack/llxprt-code-core'; import { activateIsolatedRuntimeContext, createIsolatedRuntimeContext, @@ -42,7 +42,7 @@ afterEach(async () => { resetCliProviderInfrastructure(); while (runtimeFixtures.length > 0) { const runtime = runtimeFixtures.pop(); - if (runtime) { + if (runtime != null) { // Ensure isolated runtimes release resources even when assertions fail (Step 7, multi-runtime-baseline.md line 8). await runtime.handle.cleanup(); await cleanupTempDirectory(runtime.tempDir); diff --git a/packages/cli/src/integration-tests/provider-switching.integration.test.ts b/packages/cli/src/integration-tests/provider-switching.integration.test.ts index e4fdfaa57f..5af94e2bd2 100644 --- a/packages/cli/src/integration-tests/provider-switching.integration.test.ts +++ b/packages/cli/src/integration-tests/provider-switching.integration.test.ts @@ -7,9 +7,9 @@ import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'; import { Config, - IProvider, - ProviderManager, - SettingsService, + type IProvider, + type ProviderManager, + type SettingsService, createProviderRuntimeContext, MessageBus, } from '@vybestack/llxprt-code-core'; @@ -124,7 +124,7 @@ describe('Runtime Provider Switching Integration', () => { providerManager.setActiveProvider('providerA'); setActiveModelParam('temperature', 0.7); setActiveModelParam('top_p', 0.9); - expect(getActiveModelParams()).toEqual({ + expect(getActiveModelParams()).toStrictEqual({ temperature: 0.7, top_p: 0.9, }); @@ -133,16 +133,16 @@ describe('Runtime Provider Switching Integration', () => { expect( config.getSettingsService().getProviderSettings('providerA').temperature, ).toBeUndefined(); - expect(getActiveModelParams()).toEqual({}); + expect(getActiveModelParams()).toStrictEqual({}); setActiveModelParam('temperature', 0.3); - expect(getActiveModelParams()).toEqual({ temperature: 0.3 }); + expect(getActiveModelParams()).toStrictEqual({ temperature: 0.3 }); await switchActiveProvider('providerA'); expect( config.getSettingsService().getProviderSettings('providerB').temperature, ).toBeUndefined(); - expect(getActiveModelParams()).toEqual({}); + expect(getActiveModelParams()).toStrictEqual({}); }); it('does not clear server tools provider state when switching active provider', async () => { diff --git a/packages/cli/src/integration-tests/retry-settings.integration.test.ts b/packages/cli/src/integration-tests/retry-settings.integration.test.ts index 6b5005223e..ed03c61605 100644 --- a/packages/cli/src/integration-tests/retry-settings.integration.test.ts +++ b/packages/cli/src/integration-tests/retry-settings.integration.test.ts @@ -33,7 +33,7 @@ describe('retry settings integration tests', () => { 3, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -49,7 +49,7 @@ describe('retry settings integration tests', () => { 10000, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -60,7 +60,7 @@ describe('retry settings integration tests', () => { it('should validate retries setting', async () => { const result = await setCommand.action!(context, 'retries -1'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'retries must be a non-negative integer (e.g., 3)', @@ -70,7 +70,7 @@ describe('retry settings integration tests', () => { it('should validate retrywait setting', async () => { const result = await setCommand.action!(context, 'retrywait 0'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: diff --git a/packages/cli/src/integration-tests/security.integration.test.ts b/packages/cli/src/integration-tests/security.integration.test.ts index d77d39c3d9..a699c47eb4 100644 --- a/packages/cli/src/integration-tests/security.integration.test.ts +++ b/packages/cli/src/integration-tests/security.integration.test.ts @@ -16,7 +16,7 @@ import { readSettingsFile, writeSettingsFile, } from './test-utils.js'; -import { Config, Profile } from '@vybestack/llxprt-code-core'; +import { Config, type Profile } from '@vybestack/llxprt-code-core'; import { loadSettings } from '../config/settings.js'; describe('API Key Security Integration Tests', () => { @@ -70,7 +70,7 @@ describe('API Key Security Integration Tests', () => { // Verify expected settings are present expect(parsedSettings.theme).toBe('default'); expect(parsedSettings.sandbox).toBe(true); - expect(parsedSettings.coreTools).toEqual(['ls', 'grep']); + expect(parsedSettings.coreTools).toStrictEqual(['ls', 'grep']); }); it('should not persist providerApiKeys to settings.json', async () => { diff --git a/packages/cli/src/integration-tests/test-utils.test.ts b/packages/cli/src/integration-tests/test-utils.test.ts index 8e60601763..e778f07202 100644 --- a/packages/cli/src/integration-tests/test-utils.test.ts +++ b/packages/cli/src/integration-tests/test-utils.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, afterEach } from 'vitest'; import * as fs from 'fs/promises'; import * as path from 'path'; -import { Profile } from '@vybestack/llxprt-code-core'; +import type { Profile } from '@vybestack/llxprt-code-core'; import { createTempDirectory, cleanupTempDirectory, @@ -101,7 +101,7 @@ describe('Test Utilities', () => { const content = await fs.readFile(profilePath, 'utf8'); const savedProfile = JSON.parse(content); - expect(savedProfile).toEqual(profile); + expect(savedProfile).toStrictEqual(profile); }); }); @@ -161,7 +161,7 @@ describe('Test Utilities', () => { await writeSettingsFile(dir, settings); const readSettings = await readSettingsFile(dir); - expect(readSettings).toEqual(settings); + expect(readSettings).toStrictEqual(settings); }); it('should return empty object if settings file does not exist', async () => { @@ -169,7 +169,7 @@ describe('Test Utilities', () => { tempDirs.push(dir); const settings = await readSettingsFile(dir); - expect(settings).toEqual({}); + expect(settings).toStrictEqual({}); }); }); @@ -193,7 +193,7 @@ describe('Test Utilities', () => { const content = await fs.readFile(settingsPath, 'utf8'); const parsedSettings = JSON.parse(content); - expect(parsedSettings).toEqual(settings); + expect(parsedSettings).toStrictEqual(settings); // Check formatting (2 spaces) expect(content).toContain(' "provider"'); }); diff --git a/packages/cli/src/integration-tests/test-utils.ts b/packages/cli/src/integration-tests/test-utils.ts index 98382e91a7..942692bf12 100644 --- a/packages/cli/src/integration-tests/test-utils.ts +++ b/packages/cli/src/integration-tests/test-utils.ts @@ -8,7 +8,11 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import * as http from 'http'; -import { Config, MessageBus, Profile } from '@vybestack/llxprt-code-core'; +import { + type Config, + MessageBus, + type Profile, +} from '@vybestack/llxprt-code-core'; /** * Creates a temporary directory for tests @@ -16,8 +20,7 @@ import { Config, MessageBus, Profile } from '@vybestack/llxprt-code-core'; */ export async function createTempDirectory(): Promise { const prefix = 'llxprt-test-'; - const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); - return tempDir; + return fs.mkdtemp(path.join(os.tmpdir(), prefix)); } /** @@ -235,18 +238,18 @@ export async function createMockApiServer(): Promise { } return { - server, port: address.port, - requests, close: () => new Promise((resolve, reject) => { server.close((err) => { - if (err) { + if (err != null) { reject(err); } else { resolve(); } }); }), + server, + requests, }; } diff --git a/packages/cli/src/integration-tests/todo-continuation.integration.test.ts b/packages/cli/src/integration-tests/todo-continuation.integration.test.ts index 922debd444..0c39d886b8 100644 --- a/packages/cli/src/integration-tests/todo-continuation.integration.test.ts +++ b/packages/cli/src/integration-tests/todo-continuation.integration.test.ts @@ -9,7 +9,7 @@ import { Config, GeminiClient, TodoStore, - Todo, + type Todo, ApprovalMode, todoEvents, createRuntimeStateFromConfig, @@ -148,7 +148,7 @@ describe('Todo Continuation Integration Tests', () => { config.setEphemeralSetting(testCase.key, testCase.value); // Then: Should retrieve correctly - expect(config.getEphemeralSetting(testCase.key)).toEqual( + expect(config.getEphemeralSetting(testCase.key)).toStrictEqual( testCase.value, ); } @@ -265,8 +265,7 @@ describe('Todo Continuation Integration Tests', () => { value: 'test', } as ServerGeminiStreamEvent; // Create a mock Turn object - const mockTurn = {} as Turn; - return mockTurn; + return {} as Turn; }); // When: Send ephemeral message @@ -279,7 +278,7 @@ describe('Todo Continuation Integration Tests', () => { // Then: Should capture message and options expect(capturedMessage).toBe('Test continuation prompt'); - expect(capturedOptions).toEqual({ + expect(capturedOptions).toStrictEqual({ signal: expect.any(AbortSignal), prompt_id: 'test-prompt-id', turns: undefined, @@ -560,7 +559,9 @@ describe('Todo Continuation Integration Tests', () => { // Then: All should be retrievable for (const setting of testSettings) { - expect(config.getEphemeralSetting(setting.key)).toEqual(setting.value); + expect(config.getEphemeralSetting(setting.key)).toStrictEqual( + setting.value, + ); } // When: Get all settings at once @@ -571,7 +572,7 @@ describe('Todo Continuation Integration Tests', () => { expect(allSettings['context-limit']).toBe(150000); expect(allSettings['compression-threshold']).toBe(0.75); expect(allSettings['base-url']).toBe('https://api.example.com'); - expect(allSettings['custom-headers']).toEqual({ + expect(allSettings['custom-headers']).toStrictEqual({ 'X-Test': 'integration', }); diff --git a/packages/cli/src/integration-tests/tools-governance.integration.test.ts b/packages/cli/src/integration-tests/tools-governance.integration.test.ts index af69d6e6d9..67d9c88ab4 100644 --- a/packages/cli/src/integration-tests/tools-governance.integration.test.ts +++ b/packages/cli/src/integration-tests/tools-governance.integration.test.ts @@ -62,16 +62,16 @@ describe('tools governance integration', () => { const settings = new SettingsService(); const profileManager = new ProfileManager(); const loadedProfile = await profileManager.loadProfile(PROFILE_NAME); - expect(loadedProfile.ephemeralSettings['tools.disabled']).toEqual([ + expect(loadedProfile.ephemeralSettings['tools.disabled']).toStrictEqual([ 'code-editor', ]); const setSpy = vi.spyOn(settings, 'set'); await profileManager.load(PROFILE_NAME, settings); expect(setSpy).toHaveBeenCalledWith('tools.disabled', ['code-editor']); expect(setSpy).toHaveBeenCalledWith('tools.allowed', []); - expect(settings.get('tools.disabled')).toEqual(['code-editor']); + expect(settings.get('tools.disabled')).toStrictEqual(['code-editor']); const exported = await settings.exportForProfile(); - expect(exported.tools?.disabled).toEqual(['code-editor']); + expect(exported.tools?.disabled).toStrictEqual(['code-editor']); const mockToolRegistry = { getAllTools: () => [ @@ -105,7 +105,7 @@ describe('tools governance integration', () => { ui: { addItem: uiAddItem }, }); - if (!toolsCommand.action) { + if (toolsCommand.action == null) { throw new Error('toolsCommand action not defined'); } @@ -116,6 +116,6 @@ describe('tools governance integration', () => { expect(listMessage).toContain('Code Editor [disabled]'); await toolsCommand.action(context, 'enable code-editor'); - expect(settings.get('tools.disabled')).toEqual([]); + expect(settings.get('tools.disabled')).toStrictEqual([]); }); }); diff --git a/packages/cli/src/nonInteractiveCli.test.ts b/packages/cli/src/nonInteractiveCli.test.ts index 08f9756ad2..6ee614c1c8 100644 --- a/packages/cli/src/nonInteractiveCli.test.ts +++ b/packages/cli/src/nonInteractiveCli.test.ts @@ -5,16 +5,16 @@ */ import { - Config, + type Config, executeToolCall, - ToolRegistry, + type ToolRegistry, ToolErrorType, shutdownTelemetry, GeminiEventType, - ServerGeminiStreamEvent, + type ServerGeminiStreamEvent, DebugLogger, } from '@vybestack/llxprt-code-core'; -import { Part } from '@google/genai'; +import type { Part } from '@google/genai'; import { runNonInteractive } from './nonInteractiveCli.js'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import type { LoadedSettings } from './config/settings.js'; @@ -50,7 +50,7 @@ function createCompletedToolCallResponse(params: { suppressDisplay?: boolean; }) { return { - status: params.error ? ('error' as const) : ('success' as const), + status: params.error != null ? ('error' as const) : ('success' as const), request: { callId: params.callId, name: 'mock_tool', diff --git a/packages/cli/src/nonInteractiveCli.ts b/packages/cli/src/nonInteractiveCli.ts index f1d48eb856..0e32f3fd73 100644 --- a/packages/cli/src/nonInteractiveCli.ts +++ b/packages/cli/src/nonInteractiveCli.ts @@ -5,8 +5,8 @@ */ import { - Config, - ToolCallRequestInfo, + type Config, + type ToolCallRequestInfo, executeToolCall, shutdownTelemetry, isTelemetrySdkInitialized, @@ -27,7 +27,7 @@ import { type MessageBus, debugLogger, } from '@vybestack/llxprt-code-core'; -import { Part } from '@google/genai'; +import type { Part } from '@google/genai'; import readline from 'node:readline'; import { isSlashCommand } from './ui/utils/commandUtils.js'; import type { LoadedSettings } from './config/settings.js'; @@ -72,10 +72,10 @@ export async function runNonInteractive({ const prefix = payload.severity.toUpperCase(); process.stderr.write(`[${prefix}] ${payload.message} `); - if (payload.error && config.getDebugMode()) { + if (payload.error != null && config.getDebugMode()) { const errorToLog = payload.error instanceof Error - ? payload.error.stack || payload.error.message + ? (payload.error.stack ?? payload.error.message) : String(payload.error); process.stderr.write(`${errorToLog} `); @@ -118,7 +118,7 @@ export async function runNonInteractive({ key: { name?: string; ctrl?: boolean }, ) => { // Detect Ctrl+C: either ctrl+c key combo or raw character code 3 - if ((key && key.ctrl && key.name === 'c') || str === '\u0003') { + if ((key.ctrl === true && key.name === 'c') || str === '\u0003') { // Only handle once if (isAborting) { return; @@ -143,13 +143,13 @@ export async function runNonInteractive({ const cleanupStdinCancellation = () => { // Clear any pending cancel message timer - if (cancelMessageTimer) { + if (cancelMessageTimer != null) { clearTimeout(cancelMessageTimer); cancelMessageTimer = null; } // Cleanup readline and stdin listeners - if (rl) { + if (rl != null) { rl.close(); rl = null; } @@ -180,13 +180,13 @@ export async function runNonInteractive({ const geminiClient = config.getGeminiClient(); setActiveProviderRuntimeContext({ settingsService: config.getSettingsService(), - config, - runtimeId: config.getSessionId?.(), + runtimeId: config.getSessionId(), metadata: { source: 'nonInteractiveCli' }, + config, }); // Emit init event for streaming JSON - if (streamFormatter) { + if (streamFormatter != null) { streamFormatter.emitEvent({ type: JsonStreamEventType.INIT, timestamp: new Date().toISOString(), @@ -197,10 +197,7 @@ export async function runNonInteractive({ // Initialize emoji filter for non-interactive mode const emojiFilterMode = - typeof config.getEphemeralSetting === 'function' - ? (config.getEphemeralSetting('emojifilter') as EmojiFilterMode) || - 'auto' - : 'auto'; + (config.getEphemeralSetting('emojifilter') as EmojiFilterMode) ?? 'auto'; const emojiFilter = emojiFilterMode !== 'allowed' ? new EmojiFilter({ mode: emojiFilterMode }) @@ -221,33 +218,33 @@ export async function runNonInteractive({ // If a slash command is found and returns a prompt, use it. // Otherwise, slashCommandResult falls through to the default prompt // handling. - if (slashCommandResult) { + if (slashCommandResult != null) { query = slashCommandResult as Part[]; } } - if (!query) { + if (query == null) { const { processedQuery, error } = await handleAtCommand({ query: input, - config, addItem: (_item, _timestamp) => 0, onDebugMessage: () => {}, messageId: Date.now(), signal: abortController.signal, + config, }); - if (error || !processedQuery) { + if (error != null || processedQuery == null) { // An error occurred during @include processing (e.g., file not found). // The error message is already logged by handleAtCommand. throw new FatalInputError( - error || 'Exiting due to an error processing the @ command.', + error ?? 'Exiting due to an error processing the @ command.', ); } query = processedQuery as Part[]; } // Emit user message event for streaming JSON - if (streamFormatter) { + if (streamFormatter != null) { streamFormatter.emitEvent({ type: JsonStreamEventType.MESSAGE, timestamp: new Date().toISOString(), @@ -261,6 +258,7 @@ export async function runNonInteractive({ let jsonResponseText = ''; let turnCount = 0; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition while (true) { turnCount++; if ( @@ -303,10 +301,10 @@ export async function runNonInteractive({ let firstEventInTurn = true; const maybeEmitProfileName = () => { - if (firstEventInTurn && !jsonOutput && !streamFormatter) { + if (firstEventInTurn && !jsonOutput && streamFormatter == null) { const activeProfileName = config .getSettingsService() - .getCurrentProfileName?.(); + .getCurrentProfileName(); if (activeProfileName) { process.stdout.write(`[${activeProfileName}] `); @@ -334,7 +332,7 @@ export async function runNonInteractive({ if (thoughtText.trim()) { // Apply emoji filter if enabled - if (emojiFilter) { + if (emojiFilter != null) { const filterResult = emojiFilter.filterText(thoughtText); if (filterResult.blocked) { continue; @@ -354,7 +352,7 @@ export async function runNonInteractive({ maybeEmitProfileName(); let outputValue = event.value; - if (emojiFilter) { + if (emojiFilter != null) { const filterResult = emojiFilter.filterStreamChunk(outputValue); if (filterResult.blocked) { @@ -380,7 +378,7 @@ export async function runNonInteractive({ } } - if (streamFormatter) { + if (streamFormatter != null) { streamFormatter.emitEvent({ type: JsonStreamEventType.MESSAGE, timestamp: new Date().toISOString(), @@ -396,15 +394,13 @@ export async function runNonInteractive({ } else if (event.type === GeminiEventType.ToolCallRequest) { flushThoughtBuffer(); const toolCallRequest = event.value; - if (streamFormatter) { + if (streamFormatter != null) { streamFormatter.emitEvent({ type: JsonStreamEventType.TOOL_USE, timestamp: new Date().toISOString(), tool_name: toolCallRequest.name, - tool_id: - toolCallRequest.callId ?? - `${toolCallRequest.name}-${Date.now()}`, - parameters: toolCallRequest.args ?? {}, + tool_id: toolCallRequest.callId, + parameters: toolCallRequest.args, }); } const normalizedRequest: ToolCallRequestInfo = { @@ -413,7 +409,7 @@ export async function runNonInteractive({ }; functionCalls.push(normalizedRequest); } else if (event.type === GeminiEventType.LoopDetected) { - if (streamFormatter) { + if (streamFormatter != null) { streamFormatter.emitEvent({ type: JsonStreamEventType.ERROR, timestamp: new Date().toISOString(), @@ -422,7 +418,7 @@ export async function runNonInteractive({ }); } } else if (event.type === GeminiEventType.MaxSessionTurns) { - if (streamFormatter) { + if (streamFormatter != null) { streamFormatter.emitEvent({ type: JsonStreamEventType.ERROR, timestamp: new Date().toISOString(), @@ -433,12 +429,12 @@ export async function runNonInteractive({ } else if (event.type === GeminiEventType.Error) { throw event.value.error; } else if (event.type === GeminiEventType.AgentExecutionStopped) { - const stopMessage = `Agent execution stopped: ${event.systemMessage?.trim() || event.reason}`; + const stopMessage = `Agent execution stopped: ${event.systemMessage?.trim() ?? event.reason}`; process.stderr.write(`${stopMessage} `); return; } else if (event.type === GeminiEventType.AgentExecutionBlocked) { - const blockMessage = `Agent execution blocked: ${event.systemMessage?.trim() || event.reason}`; + const blockMessage = `Agent execution blocked: ${event.systemMessage?.trim() ?? event.reason}`; process.stderr.write(`[WARNING] ${blockMessage} `); } @@ -446,7 +442,7 @@ export async function runNonInteractive({ flushThoughtBuffer(); - const remainingBuffered = emojiFilter?.flushBuffer?.(); + const remainingBuffered = emojiFilter?.flushBuffer(); if (remainingBuffered) { if (jsonOutput) { jsonResponseText += remainingBuffered; @@ -459,15 +455,14 @@ export async function runNonInteractive({ const toolResponseParts: Part[] = []; for (const requestFromModel of functionCalls) { - const callId = - requestFromModel.callId ?? `${requestFromModel.name}-${Date.now()}`; - const rawArgs = requestFromModel.args ?? {}; + const callId = requestFromModel.callId; + const rawArgs = requestFromModel.args; let normalizedArgs: Record; if (typeof rawArgs === 'string') { try { const parsed = JSON.parse(rawArgs); normalizedArgs = - parsed && typeof parsed === 'object' + parsed != null && typeof parsed === 'object' ? (parsed as Record) : {}; } catch (error) { @@ -481,10 +476,8 @@ export async function runNonInteractive({ `Unexpected array arguments for tool ${requestFromModel.name}; coercing to empty object.`, ); normalizedArgs = {}; - } else if (rawArgs && typeof rawArgs === 'object') { - normalizedArgs = rawArgs; } else { - normalizedArgs = {}; + normalizedArgs = rawArgs; } const requestInfo: ToolCallRequestInfo = { @@ -492,7 +485,7 @@ export async function runNonInteractive({ name: requestFromModel.name, args: normalizedArgs, isClientInitiated: false, - prompt_id: requestFromModel.prompt_id ?? prompt_id, + prompt_id: requestFromModel.prompt_id, agentId: requestFromModel.agentId ?? 'primary', }; @@ -511,26 +504,27 @@ export async function runNonInteractive({ }); const toolResponse = completed.response; - if (streamFormatter) { + if (streamFormatter != null) { streamFormatter.emitEvent({ type: JsonStreamEventType.TOOL_RESULT, timestamp: new Date().toISOString(), tool_id: requestInfo.callId, - status: toolResponse.error ? 'error' : 'success', + status: toolResponse.error != null ? 'error' : 'success', output: typeof toolResponse.resultDisplay === 'string' ? toolResponse.resultDisplay : undefined, - error: toolResponse.error - ? { - type: toolResponse.errorType || 'TOOL_EXECUTION_ERROR', - message: toolResponse.error.message, - } - : undefined, + error: + toolResponse.error != null + ? { + type: toolResponse.errorType || 'TOOL_EXECUTION_ERROR', + message: toolResponse.error.message, + } + : undefined, }); } - if (toolResponse.error) { + if (toolResponse.error != null) { if (!jsonOutput && !streamJsonOutput) { debugLogger.error( `Error executing tool ${requestFromModel.name}: ${toolResponse.resultDisplay || toolResponse.error.message}`, @@ -554,7 +548,7 @@ export async function runNonInteractive({ currentMessages = toolResponseParts; } else { // Emit final result event for streaming JSON - if (streamFormatter) { + if (streamFormatter != null) { const metrics = uiTelemetryService.getMetrics(); const durationMs = Date.now() - startTime; streamFormatter.emitEvent({ diff --git a/packages/cli/src/nonInteractiveCliCommands.ts b/packages/cli/src/nonInteractiveCliCommands.ts index 0b85787bb7..5f463edd52 100644 --- a/packages/cli/src/nonInteractiveCliCommands.ts +++ b/packages/cli/src/nonInteractiveCliCommands.ts @@ -49,8 +49,8 @@ export const handleSlashCommand = async ( const { commandToExecute, args } = parseSlashCommand(trimmed, commands); - if (commandToExecute) { - if (commandToExecute.action) { + if (commandToExecute != null) { + if (commandToExecute.action != null) { // Not used by custom commands but may be in the future. const sessionStats: SessionStatsState = { sessionId: config?.getSessionId() || 'unknown', diff --git a/packages/cli/src/providers/credentialPrecedence.test.ts b/packages/cli/src/providers/credentialPrecedence.test.ts index c4f959552b..933d8a8ce4 100644 --- a/packages/cli/src/providers/credentialPrecedence.test.ts +++ b/packages/cli/src/providers/credentialPrecedence.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect } from 'vitest'; import { resolveCredentialPrecedence, - CredentialInputs, + type CredentialInputs, } from './credentialPrecedence.js'; describe('resolveCredentialPrecedence', () => { diff --git a/packages/cli/src/providers/logging/LoggingProviderWrapper.test.ts b/packages/cli/src/providers/logging/LoggingProviderWrapper.test.ts index f1e7946360..889dab4e73 100644 --- a/packages/cli/src/providers/logging/LoggingProviderWrapper.test.ts +++ b/packages/cli/src/providers/logging/LoggingProviderWrapper.test.ts @@ -5,11 +5,11 @@ */ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { +import type { IProvider, IContent, ITool, - type Config, + Config, } from '@vybestack/llxprt-code-core'; // These interfaces will be implemented in the next phase @@ -92,7 +92,7 @@ class MockLoggingProviderWrapper implements LoggingProviderWrapper { }>, ): AsyncIterableIterator { // This should log the conversation request when logging is enabled - if (this.config.getConversationLoggingEnabled?.()) { + if (this.config.getConversationLoggingEnabled()) { try { const redactedMessages = messages.map((msg) => this.redactor.redactMessage(msg, this.provider.name), @@ -445,6 +445,6 @@ describe('Multi-Provider Conversation Logging', () => { results .map((r) => r.blocks[0]) .map((block) => (block as { text: string }).text), - ).toEqual(['Chunk 1', 'Chunk 2', 'Chunk 3']); + ).toStrictEqual(['Chunk 1', 'Chunk 2', 'Chunk 3']); }); }); diff --git a/packages/cli/src/providers/logging/git-stats-service-impl.ts b/packages/cli/src/providers/logging/git-stats-service-impl.ts index d2b28d3aad..a1d1757d09 100644 --- a/packages/cli/src/providers/logging/git-stats-service-impl.ts +++ b/packages/cli/src/providers/logging/git-stats-service-impl.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { +import type { IGitStatsService, GitStats, Config, diff --git a/packages/cli/src/providers/logging/git-stats.test.ts b/packages/cli/src/providers/logging/git-stats.test.ts index 9a056dbe9b..00d61b5b06 100644 --- a/packages/cli/src/providers/logging/git-stats.test.ts +++ b/packages/cli/src/providers/logging/git-stats.test.ts @@ -82,7 +82,7 @@ describe('Git Statistics Tracking', () => { 'line1\nline2\nline3\nline4', ); - expect(stats).toEqual({ + expect(stats).toStrictEqual({ linesAdded: 2, linesRemoved: 0, filesChanged: 1, diff --git a/packages/cli/src/providers/logging/git-stats.ts b/packages/cli/src/providers/logging/git-stats.ts index 25e0bfd169..eb0099ec6e 100644 --- a/packages/cli/src/providers/logging/git-stats.ts +++ b/packages/cli/src/providers/logging/git-stats.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; export interface GitStats { linesAdded: number; @@ -43,7 +43,7 @@ export class GitStatsTracker { constructor(private config: Config) { // Handle invalid config gracefully try { - this.enabled = this.config?.getConversationLoggingEnabled?.() ?? false; + this.enabled = this.config.getConversationLoggingEnabled() ?? false; } catch (_error) { this.enabled = false; } @@ -66,10 +66,10 @@ export class GitStatsTracker { } // Handle null/undefined content gracefully - if (oldContent === null || oldContent === undefined) { + if (oldContent == null || oldContent == undefined) { oldContent = ''; } - if (newContent === null || newContent === undefined) { + if (newContent == null || newContent == undefined) { newContent = ''; } @@ -143,7 +143,7 @@ export class GitStatsTracker { isEnabled(): boolean { // Update enabled state based on current config try { - this.enabled = this.config?.getConversationLoggingEnabled?.() ?? false; + this.enabled = this.config.getConversationLoggingEnabled() ?? false; } catch (_error) { this.enabled = false; } @@ -175,7 +175,7 @@ export class GitStatsTracker { getSummary(): SessionSummary { let sessionId = ''; try { - sessionId = this.config?.getSessionId?.() ?? ''; + sessionId = this.config.getSessionId() ?? ''; } catch (_error) { sessionId = ''; } diff --git a/packages/cli/src/providers/logging/multi-provider-logging.integration.test.ts b/packages/cli/src/providers/logging/multi-provider-logging.integration.test.ts index 7b279b4bab..6cb74ec822 100644 --- a/packages/cli/src/providers/logging/multi-provider-logging.integration.test.ts +++ b/packages/cli/src/providers/logging/multi-provider-logging.integration.test.ts @@ -5,13 +5,13 @@ */ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { +import type { IProvider, IContent, GenerateChatOptions, ProviderToolset, + Config, } from '@vybestack/llxprt-code-core'; -import type { Config } from '@vybestack/llxprt-code-core'; // Interfaces that will be implemented in the next phase interface LoggingProviderWrapper { @@ -402,7 +402,7 @@ describe('Multi-Provider Conversation Logging Integration', () => { // Verify logging across providers const entries = storage.getEntries(); expect(entries).toHaveLength(3); - expect(entries.map((e) => e.provider_name)).toEqual([ + expect(entries.map((e) => e.provider_name)).toStrictEqual([ 'openai', 'anthropic', 'gemini', @@ -691,8 +691,10 @@ describe('Multi-Provider Conversation Logging Integration', () => { expect(entries).toHaveLength(3); // Verify each provider was logged correctly - const providerNames = entries.map((e) => e.provider_name).sort(); - expect(providerNames).toEqual([ + const providerNames = entries + .map((e) => e.provider_name) + .sort((a, b) => a.localeCompare(b)); + expect(providerNames).toStrictEqual([ 'concurrent-1', 'concurrent-2', 'concurrent-3', diff --git a/packages/cli/src/providers/oauth-provider-registration.ts b/packages/cli/src/providers/oauth-provider-registration.ts index 0b69c82abb..40d9c21708 100644 --- a/packages/cli/src/providers/oauth-provider-registration.ts +++ b/packages/cli/src/providers/oauth-provider-registration.ts @@ -14,8 +14,8 @@ import { QwenOAuthProvider } from '../auth/qwen-oauth-provider.js'; import { AnthropicOAuthProvider } from '../auth/anthropic-oauth-provider.js'; import { CodexOAuthProvider } from '../auth/codex-oauth-provider.js'; import type { TokenStore } from '../auth/types.js'; -import { OAuthManager } from '../auth/oauth-manager.js'; -import { HistoryItemWithoutId } from '../ui/types.js'; +import type { OAuthManager } from '../auth/oauth-manager.js'; +import type { HistoryItemWithoutId } from '../ui/types.js'; /** * Track which OAuth providers have been registered to avoid duplicate registration @@ -42,7 +42,7 @@ export function ensureOAuthProviderRegistered( } let registered = registeredProviders.get(oauthManager); - if (!registered) { + if (registered == null) { registered = new Set(); registeredProviders.set(oauthManager, registered); } @@ -50,7 +50,7 @@ export function ensureOAuthProviderRegistered( return; } - const effectiveTokenStore = tokenStore ?? oauthManager.getTokenStore?.(); + const effectiveTokenStore = tokenStore ?? oauthManager.getTokenStore(); if (!effectiveTokenStore) { oauthLogger.debug( () => @@ -79,8 +79,8 @@ export function ensureOAuthProviderRegistered( } // Note: setAddItem is still called as a fallback for providers that don't accept it in constructor - if (oauthProvider && addItem) { - oauthProvider.setAddItem?.(addItem); + if (oauthProvider && addItem != null) { + oauthProvider.setAddItem(addItem); } if (oauthProvider) { diff --git a/packages/cli/src/providers/provider-gemini-switching.test.ts b/packages/cli/src/providers/provider-gemini-switching.test.ts index a2c88b5467..d4045add4a 100644 --- a/packages/cli/src/providers/provider-gemini-switching.test.ts +++ b/packages/cli/src/providers/provider-gemini-switching.test.ts @@ -6,9 +6,9 @@ import { describe, it, expect, vi } from 'vitest'; import { createProviderManager } from './providerManagerInstance.js'; -import { IProvider, IModel } from './index.js'; +import type { IProvider, IModel } from './index.js'; import { - Config, + type Config, SettingsService, createProviderRuntimeContext, } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/providers/provider-switching.integration.test.ts b/packages/cli/src/providers/provider-switching.integration.test.ts index a79ff61359..4e6d97b8d3 100644 --- a/packages/cli/src/providers/provider-switching.integration.test.ts +++ b/packages/cli/src/providers/provider-switching.integration.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect } from 'vitest'; import { createProviderManager } from './providerManagerInstance.js'; -import { IProvider } from './index.js'; +import type { IProvider } from './index.js'; import { createProviderRuntimeContext, SettingsService, @@ -70,7 +70,7 @@ describe('Provider Switching Integration', () => { manager.registerProvider(createMockProvider('provider2')); const providers = manager.listProviders(); - expect(providers).toEqual( + expect(providers).toStrictEqual( expect.arrayContaining(['provider1', 'provider2']), ); diff --git a/packages/cli/src/providers/providerAliases.modelDefaults.test.ts b/packages/cli/src/providers/providerAliases.modelDefaults.test.ts index 0144f1ea9b..88d4f1ece8 100644 --- a/packages/cli/src/providers/providerAliases.modelDefaults.test.ts +++ b/packages/cli/src/providers/providerAliases.modelDefaults.test.ts @@ -78,9 +78,11 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { expect(entry?.config.modelDefaults).toBeInstanceOf(Array); expect(entry?.config.modelDefaults).toHaveLength(1); expect(entry?.config.modelDefaults?.[0]?.pattern).toBe('gpt-4.*'); - expect(entry?.config.modelDefaults?.[0]?.ephemeralSettings).toEqual({ - 'reasoning.enabled': true, - }); + expect(entry?.config.modelDefaults?.[0]?.ephemeralSettings).toStrictEqual( + { + 'reasoning.enabled': true, + }, + ); }); }); @@ -99,7 +101,7 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'regex-test'); expect(entry).toBeDefined(); - expect(entry?.config.modelDefaults).toEqual([]); + expect(entry?.config.modelDefaults).toStrictEqual([]); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -149,7 +151,7 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'nonstring-pattern'); expect(entry).toBeDefined(); - expect(entry?.config.modelDefaults).toEqual([]); + expect(entry?.config.modelDefaults).toStrictEqual([]); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -173,7 +175,7 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'missing-ephemeral'); expect(entry).toBeDefined(); - expect(entry?.config.modelDefaults).toEqual([]); + expect(entry?.config.modelDefaults).toStrictEqual([]); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -193,7 +195,7 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'empty-defaults'); expect(entry).toBeDefined(); - expect(entry?.config.modelDefaults).toEqual([]); + expect(entry?.config.modelDefaults).toStrictEqual([]); }); }); @@ -260,7 +262,7 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'non-objects'); expect(entry).toBeDefined(); - expect(entry?.config.modelDefaults).toEqual([]); + expect(entry?.config.modelDefaults).toStrictEqual([]); // One warning per non-object entry const nonObjectWarnings = warnSpy.mock.calls.filter( @@ -287,7 +289,7 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'pattern-number'); expect(entry).toBeDefined(); - expect(entry?.config.modelDefaults).toEqual([]); + expect(entry?.config.modelDefaults).toStrictEqual([]); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -310,7 +312,7 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'pattern-bool'); expect(entry).toBeDefined(); - expect(entry?.config.modelDefaults).toEqual([]); + expect(entry?.config.modelDefaults).toStrictEqual([]); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -335,7 +337,7 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'ephemeral-array'); expect(entry).toBeDefined(); - expect(entry?.config.modelDefaults).toEqual([]); + expect(entry?.config.modelDefaults).toStrictEqual([]); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -358,7 +360,7 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'ephemeral-string'); expect(entry).toBeDefined(); - expect(entry?.config.modelDefaults).toEqual([]); + expect(entry?.config.modelDefaults).toStrictEqual([]); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( @@ -388,11 +390,13 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'nested-values'); expect(entry).toBeDefined(); expect(entry?.config.modelDefaults).toHaveLength(1); - expect(entry?.config.modelDefaults?.[0]?.ephemeralSettings).toEqual({ - 'reasoning.enabled': true, - nested: { deeply: { value: 42 } }, - anArray: [1, 2, 3], - }); + expect(entry?.config.modelDefaults?.[0]?.ephemeralSettings).toStrictEqual( + { + 'reasoning.enabled': true, + nested: { deeply: { value: 42 } }, + anArray: [1, 2, 3], + }, + ); }); }); @@ -411,7 +415,7 @@ describe('providerAliases modelDefaults parsing (Phase 01)', () => { const entry = entries.find((e) => e.alias === 'empty-pattern'); expect(entry).toBeDefined(); - expect(entry?.config.modelDefaults).toEqual([]); + expect(entry?.config.modelDefaults).toStrictEqual([]); expect(warnSpy).toHaveBeenCalledWith( expect.stringContaining( diff --git a/packages/cli/src/providers/providerAliases.staticModels.test.ts b/packages/cli/src/providers/providerAliases.staticModels.test.ts index b6f8cf87b0..7b0edf6c97 100644 --- a/packages/cli/src/providers/providerAliases.staticModels.test.ts +++ b/packages/cli/src/providers/providerAliases.staticModels.test.ts @@ -36,7 +36,7 @@ describe('Provider Alias Static Models (Issue #1206)', () => { expect(qwenEntry?.config.defaultModel).toBeDefined(); const defaultModelInList = qwenEntry?.config.staticModels?.some( - (m) => m.id === qwenEntry?.config.defaultModel, + (m) => m.id === qwenEntry.config.defaultModel, ); expect(defaultModelInList).toBe(true); }); diff --git a/packages/cli/src/providers/providerAliases.ts b/packages/cli/src/providers/providerAliases.ts index 2a0041f5ec..865dd2bf76 100644 --- a/packages/cli/src/providers/providerAliases.ts +++ b/packages/cli/src/providers/providerAliases.ts @@ -250,7 +250,7 @@ export function loadProviderAliasEntries(): ProviderAliasEntry[] { const fullPath = path.join(directory.path, file); const entry = readAliasFile(fullPath, directory.source); - if (entry) { + if (entry != null) { aliases.push(entry); } } diff --git a/packages/cli/src/providers/providerManagerInstance.messagebus.test.ts b/packages/cli/src/providers/providerManagerInstance.messagebus.test.ts index 233cf35086..990d973f56 100644 --- a/packages/cli/src/providers/providerManagerInstance.messagebus.test.ts +++ b/packages/cli/src/providers/providerManagerInstance.messagebus.test.ts @@ -74,15 +74,17 @@ describe('getProviderManager runtime OAuth MessageBus composition', () => { expect(oauthManager).not.toBeNull(); - const registeredProviders = manager.listProviders().sort(); + const registeredProviders = manager + .listProviders() + .sort((a, b) => a.localeCompare(b)); const supportedOAuthProviders = oauthManager! .getSupportedProviders() - .sort(); + .sort((a, b) => a.localeCompare(b)); - expect(registeredProviders).toEqual( + expect(registeredProviders).toStrictEqual( expect.arrayContaining(['anthropic', 'codex', 'gemini', 'openai']), ); - expect(supportedOAuthProviders).toEqual( + expect(supportedOAuthProviders).toStrictEqual( expect.arrayContaining(['anthropic', 'codex', 'gemini', 'qwen']), ); expect( diff --git a/packages/cli/src/providers/providerManagerInstance.oauthRegistration.test.ts b/packages/cli/src/providers/providerManagerInstance.oauthRegistration.test.ts index f5c5103052..191eec2e7a 100644 --- a/packages/cli/src/providers/providerManagerInstance.oauthRegistration.test.ts +++ b/packages/cli/src/providers/providerManagerInstance.oauthRegistration.test.ts @@ -107,7 +107,7 @@ describe('Anthropic OAuth registration with environment key', () => { const ctorCalls = anthropicCtor.mock.calls; expect(ctorCalls.length).toBeGreaterThanOrEqual(1); const firstCall = ctorCalls[0] as unknown[] | undefined; - const oauthManagerArg = firstCall ? firstCall[3] : undefined; + const oauthManagerArg = firstCall != null ? firstCall[3] : undefined; expect(oauthManagerArg).toBeTruthy(); }); diff --git a/packages/cli/src/providers/providerManagerInstance.ts b/packages/cli/src/providers/providerManagerInstance.ts index 3af378f4d6..1b34fbac2a 100644 --- a/packages/cli/src/providers/providerManagerInstance.ts +++ b/packages/cli/src/providers/providerManagerInstance.ts @@ -5,7 +5,6 @@ */ import { - Config, ProviderManager, OpenAIProvider, OpenAIResponsesProvider, @@ -15,29 +14,33 @@ import { FakeProvider, sanitizeForByteString, needsSanitization, - SettingsService, getSettingsService, DebugLogger, debugLogger, +} from '@vybestack/llxprt-code-core'; +import type { + Config, + SettingsService, MessageBus, } from '@vybestack/llxprt-code-core'; const logger = new DebugLogger('llxprt:provider:manager:instance'); -import { IFileSystem, NodeFileSystem } from './IFileSystem.js'; +import type { IFileSystem } from './IFileSystem.js'; +import { NodeFileSystem } from './IFileSystem.js'; import { - Settings, LoadedSettings, USER_SETTINGS_PATH, type MergedSettings, + type Settings, } from '../config/settings.js'; import stripJsonComments from 'strip-json-comments'; import { OAuthManager } from '../auth/oauth-manager.js'; import type { OAuthManagerRuntimeMessageBusDeps } from '../auth/types.js'; import { ensureOAuthProviderRegistered } from './oauth-provider-registration.js'; import { createTokenStore } from '../auth/proxy/credential-store-factory.js'; -import { HistoryItemWithoutId } from '../ui/types.js'; +import type { HistoryItemWithoutId } from '../ui/types.js'; -import { IProviderConfig } from '@vybestack/llxprt-code-core/providers/types/IProviderConfig.js'; +import type { IProviderConfig } from '@vybestack/llxprt-code-core/providers/types/IProviderConfig.js'; import { loadProviderAliasEntries, type ProviderAliasEntry, @@ -104,9 +107,7 @@ export function setFileSystem(fs: IFileSystem): void { * Get the file system implementation to use. */ function getFileSystem(): IFileSystem { - if (!fileSystemInstance) { - fileSystemInstance = new NodeFileSystem(); - } + fileSystemInstance ??= new NodeFileSystem(); return fileSystemInstance; } @@ -210,7 +211,7 @@ function resolveAuthOnlyFlag( if (typeof getSettingsService === 'function') { try { const settingsService = getSettingsService(); - if (settingsService && typeof settingsService.get === 'function') { + if (typeof settingsService.get === 'function') { const serviceValue = settingsService.get('authOnly'); if (serviceValue !== undefined) { const coerced = coerceAuthOnly(serviceValue); @@ -286,7 +287,7 @@ export function createProviderManager( logger.debug('createProviderManager config check', { hasConfig: !!config, - configType: config?.constructor?.name, + configType: config?.constructor.name, }); if (config) { @@ -303,7 +304,7 @@ export function createProviderManager( const settingsData = loadedSettings?.merged ?? ({} as Partial); - const ephemeralSettings = config?.getEphemeralSettings?.() ?? {}; + const ephemeralSettings = config?.getEphemeralSettings() ?? {}; const effectiveOpenaiResponsesEnabled: boolean | undefined = ephemeralSettings.openaiResponsesEnabled !== undefined ? Boolean(ephemeralSettings.openaiResponsesEnabled) @@ -328,13 +329,13 @@ export function createProviderManager( | Record | undefined; const openaiProviderApiKey = - (openaiSettings?.apiKey as string | undefined) || + (openaiSettings?.apiKey as string | undefined) ?? (openaiSettings?.['auth-key'] as string | undefined); let openaiApiKey: string | undefined; if ( - ephemeralAuthKey && + ephemeralAuthKey != null && typeof ephemeralAuthKey === 'string' && ephemeralAuthKey.trim() !== '' ) { @@ -355,9 +356,9 @@ export function createProviderManager( const ephemeralBaseUrl = ephemeralSettings['base-url']; const providerBaseUrl = openaiSettings?.['base-url'] as string | undefined; const openaiBaseUrl = - ephemeralBaseUrl && typeof ephemeralBaseUrl === 'string' + ephemeralBaseUrl != null && typeof ephemeralBaseUrl === 'string' ? ephemeralBaseUrl - : providerBaseUrl && typeof providerBaseUrl === 'string' + : providerBaseUrl != null && typeof providerBaseUrl === 'string' ? providerBaseUrl : process.env.OPENAI_BASE_URL; @@ -369,10 +370,10 @@ export function createProviderManager( textToolCallModels: settingsData.textToolCallModels, providerToolFormatOverrides: settingsData.providerToolFormatOverrides, openaiResponsesEnabled: effectiveOpenaiResponsesEnabled, - allowBrowserEnvironment, getEphemeralSettings: config ? () => config.getEphemeralSettings() : undefined, + allowBrowserEnvironment, }; // All providers are now registered via alias configs @@ -604,7 +605,7 @@ export function bindOpenAIAliasIdentity( } function bindProviderAliasIdentity(provider: unknown, alias: string): void { - const aliasName = alias?.trim(); + const aliasName = alias.trim(); if (!aliasName) { return; } @@ -633,7 +634,7 @@ function createOpenAIAliasProvider( openaiProviderConfig: IProviderConfig, oauthManager: OAuthManager, ): OpenAIProvider | null { - const resolvedBaseUrl = entry.config['base-url'] || openaiBaseUrl; + const resolvedBaseUrl = entry.config['base-url'] ?? openaiBaseUrl; if (!resolvedBaseUrl) { debugLogger.warn( `[ProviderManager] Alias '${entry.alias}' is missing a baseUrl and no default is available, skipping.`, @@ -666,7 +667,7 @@ function createOpenAIAliasProvider( } const provider = new OpenAIProvider( - aliasApiKey || undefined, + aliasApiKey ?? undefined, resolvedBaseUrl, aliasProviderConfig, oauthManager, @@ -706,7 +707,7 @@ function createOpenAIResponsesAliasProvider( openaiProviderConfig: IProviderConfig, oauthManager: OAuthManager, ): OpenAIResponsesProvider | null { - const resolvedBaseUrl = entry.config['base-url'] || openaiBaseUrl; + const resolvedBaseUrl = entry.config['base-url'] ?? openaiBaseUrl; if (!resolvedBaseUrl) { debugLogger.warn( `[ProviderManager] Alias '${entry.alias}' is missing a baseUrl and no default is available, skipping.`, @@ -739,7 +740,7 @@ function createOpenAIResponsesAliasProvider( } const provider = new OpenAIResponsesProvider( - aliasApiKey || undefined, + aliasApiKey ?? undefined, resolvedBaseUrl, aliasProviderConfig, oauthManager, @@ -784,7 +785,7 @@ function createOpenAIVercelAliasProvider( openaiProviderConfig: IProviderConfig, oauthManager: OAuthManager, ): OpenAIVercelProvider | null { - const resolvedBaseUrl = entry.config['base-url'] || openaiBaseUrl; + const resolvedBaseUrl = entry.config['base-url'] ?? openaiBaseUrl; if (!resolvedBaseUrl) { debugLogger.warn( `[ProviderManager] Alias '${entry.alias}' is missing a baseUrl and no default is available, skipping.`, @@ -817,7 +818,7 @@ function createOpenAIVercelAliasProvider( } const provider = new OpenAIVercelProvider( - aliasApiKey || undefined, + aliasApiKey ?? undefined, resolvedBaseUrl, aliasProviderConfig, oauthManager, @@ -865,7 +866,7 @@ function createGeminiAliasProvider( const resolvedBaseUrl = entry.config['base-url']; const provider = new GeminiProvider( - aliasApiKey || undefined, + aliasApiKey ?? undefined, resolvedBaseUrl, config, oauthManager, @@ -897,7 +898,7 @@ function createAnthropicAliasProvider( ): AnthropicProvider | null { let aliasApiKey: string | undefined; // Only use environment variable API key if authOnly is not enabled - if (!authOnlyEnabled && entry.config.apiKeyEnv) { + if (authOnlyEnabled !== true && entry.config.apiKeyEnv) { const envValue = process.env[entry.config.apiKeyEnv]; if (envValue && envValue.trim() !== '') { aliasApiKey = sanitizeApiKey(envValue); @@ -912,7 +913,7 @@ function createAnthropicAliasProvider( } const provider = new AnthropicProvider( - aliasApiKey || undefined, + aliasApiKey ?? undefined, resolvedBaseUrl, providerConfig, oauthManager, diff --git a/packages/cli/src/runtime/__tests__/authKeyName.test.ts b/packages/cli/src/runtime/__tests__/authKeyName.test.ts index 17e82a2e84..948857b3a9 100644 --- a/packages/cli/src/runtime/__tests__/authKeyName.test.ts +++ b/packages/cli/src/runtime/__tests__/authKeyName.test.ts @@ -61,8 +61,8 @@ function createTestStorage( ): ProviderKeyStorage { const secureStore = new SecureStore('llxprt-code-provider-keys', { keyringLoader: async () => mockKeyring, - fallbackDir, fallbackPolicy: 'allow', + fallbackDir, }); return new ProviderKeyStorage({ secureStore }); } @@ -213,7 +213,7 @@ describe('API key precedence and named key resolution @plan:PLAN-20260211-SECURE }); afterEach(async () => { - if (cleanupHandle) { + if (cleanupHandle != null) { await Promise.resolve(cleanupHandle()).catch(() => {}); cleanupHandle = null; } @@ -482,7 +482,7 @@ describe('API key precedence and named key resolution @plan:PLAN-20260211-SECURE }); afterEach(async () => { - if (cleanupHandle) { + if (cleanupHandle != null) { await Promise.resolve(cleanupHandle()).catch(() => {}); cleanupHandle = null; } diff --git a/packages/cli/src/runtime/__tests__/profileApplication.test.ts b/packages/cli/src/runtime/__tests__/profileApplication.test.ts index 3e03eb4028..8bda7cda9c 100644 --- a/packages/cli/src/runtime/__tests__/profileApplication.test.ts +++ b/packages/cli/src/runtime/__tests__/profileApplication.test.ts @@ -483,7 +483,7 @@ describe('Profile application basics', () => { providerName: string, options?: { preserveEphemerals?: string[]; autoOAuth?: boolean }, ) => { - if (options && 'autoOAuth' in options) { + if (options != null && 'autoOAuth' in options) { switchWasCalledWithAutoOAuth = options.autoOAuth === true; } providerManagerStub.activeProviderName = providerName; @@ -1050,7 +1050,7 @@ describe('Phase 3: Profile loading auth timing (OAuth lazy loading)', () => { expect(switchIndex).toBeGreaterThan(-1); // Should include warning about keyfile failure - expect(result.warnings).toEqual( + expect(result.warnings).toStrictEqual( expect.arrayContaining([ expect.stringMatching(/Failed to load keyfile/i), ]), @@ -1255,7 +1255,7 @@ describe('STEP 2 workflow: pre-switch auth wiring', () => { const result = await applyProfileWithGuards(profile); - expect(result.warnings).toEqual( + expect(result.warnings).toStrictEqual( expect.arrayContaining([ expect.stringContaining( "Key 'missing-key' not found in secure storage", diff --git a/packages/cli/src/runtime/__tests__/runtimeIsolation.test.ts b/packages/cli/src/runtime/__tests__/runtimeIsolation.test.ts index aa78b8cd10..b9a9344162 100644 --- a/packages/cli/src/runtime/__tests__/runtimeIsolation.test.ts +++ b/packages/cli/src/runtime/__tests__/runtimeIsolation.test.ts @@ -47,7 +47,7 @@ afterEach(async () => { resetCliProviderInfrastructure(); while (runtimeFixtures.length > 0) { const fixture = runtimeFixtures.pop(); - if (!fixture) { + if (fixture == null) { continue; } try { @@ -213,7 +213,7 @@ describe('CLI runtime isolation', () => { ); expect( runtimeA.handle.config.getEphemeralSetting('custom-headers'), - ).toEqual({ 'x-runtime': runtimeA.id }); + ).toStrictEqual({ 'x-runtime': runtimeA.id }); expect(aSecondarySettings.temperature).toBe(0.6); expect(aSecondarySettings.apiKey).toBe('alpha-updated-key'); expect(aSecondarySettings['base-url']).toBe( @@ -239,7 +239,7 @@ describe('CLI runtime isolation', () => { ); expect( runtimeB.handle.config.getEphemeralSetting('custom-headers'), - ).toEqual({ 'x-runtime': runtimeB.id }); + ).toStrictEqual({ 'x-runtime': runtimeB.id }); expect(bSecondarySettings.temperature).toBe(0.4); expect(bSecondarySettings.apiKey).toBe('beta-updated-key'); expect(bSecondarySettings['base-url']).toBe( diff --git a/packages/cli/src/runtime/agentRuntimeAdapter.ts b/packages/cli/src/runtime/agentRuntimeAdapter.ts index 1a75d47a38..a6ad6b09da 100644 --- a/packages/cli/src/runtime/agentRuntimeAdapter.ts +++ b/packages/cli/src/runtime/agentRuntimeAdapter.ts @@ -100,9 +100,10 @@ export class AgentRuntimeAdapter { model: snapshot.model, baseUrl: snapshot.baseUrl, proxyUrl: snapshot.proxyUrl, - modelParams: snapshot.modelParams - ? Object.freeze({ ...snapshot.modelParams }) - : undefined, + modelParams: + snapshot.modelParams != null + ? Object.freeze({ ...snapshot.modelParams }) + : undefined, sessionId: snapshot.sessionId, updatedAt: snapshot.updatedAt, }) as AgentRuntimeState; @@ -214,7 +215,7 @@ export class AgentRuntimeAdapter { // Validate provider exists const providerManager = this.legacyConfig.getProviderManager(); - if (!providerManager) { + if (providerManager == null) { throw new Error('Provider manager not initialized'); } @@ -225,7 +226,7 @@ export class AgentRuntimeAdapter { ) => { getDefaultModel: () => string } | undefined; } ).getProviderByName(providerName); - if (!provider) { + if (provider == null) { const available = providerManager.listProviders().join(', '); throw new Error( `Provider '${providerName}' not found. Available providers: ${available}`, @@ -304,7 +305,7 @@ export class AgentRuntimeAdapter { // Validate provider const providerManager = this.legacyConfig.getProviderManager(); - if (!providerManager) { + if (providerManager == null) { throw new Error('Provider manager not initialized'); } @@ -315,7 +316,7 @@ export class AgentRuntimeAdapter { ) => { getDefaultModel: () => string } | undefined; } ).getProviderByName(providerName); - if (!provider) { + if (provider == null) { const available = providerManager.listProviders().join(', '); throw new Error( `Provider '${providerName}' not found. Available providers: ${available}`, @@ -372,7 +373,7 @@ export class AgentRuntimeAdapter { // Note: Config doesn't have setProxy, proxy is read-only // Mirror model params as ephemeral settings - if (state.modelParams) { + if (state.modelParams != null) { for (const [key, value] of Object.entries(state.modelParams)) { this.legacyConfig.setEphemeralSetting(key, value); } @@ -398,7 +399,7 @@ export class AgentRuntimeAdapter { // @pseudocode cli-runtime-adapter.md lines 597-609 // Unsubscribe from runtime state events - if (this.unsubscribe) { + if (this.unsubscribe != null) { this.unsubscribe(); this.unsubscribe = null; } @@ -441,7 +442,7 @@ export function setRuntimeAdapter(adapter: AgentRuntimeAdapter): void { export function getRuntimeAdapter(): AgentRuntimeAdapter { // @plan PLAN-20251027-STATELESS5.P08 // @pseudocode cli-runtime-adapter.md lines 369-375 - if (!globalRuntimeAdapter) { + if (globalRuntimeAdapter == null) { throw new Error( 'Runtime adapter not initialized. Call setRuntimeAdapter() during bootstrap.', ); @@ -512,7 +513,7 @@ export function resolveRuntimeStateFromFlags( params.model = flags.model; } - if (flags.set) { + if (flags.set != null) { // Process --set flags (e.g., --set base-url=...) for (const [key, value] of flags.set) { if (key === 'base-url') { diff --git a/packages/cli/src/runtime/anthropic-oauth-defaults.test.ts b/packages/cli/src/runtime/anthropic-oauth-defaults.test.ts index 1fb92447c2..8a9686ff2c 100644 --- a/packages/cli/src/runtime/anthropic-oauth-defaults.test.ts +++ b/packages/cli/src/runtime/anthropic-oauth-defaults.test.ts @@ -56,7 +56,7 @@ const { ) => { if ( typeof providerOrChanges === 'string' && - maybeChanges && + maybeChanges != null && typeof maybeChanges === 'object' ) { for (const [key, value] of Object.entries(maybeChanges)) { @@ -203,7 +203,7 @@ vi.mock('@vybestack/llxprt-code-core', async (importOriginal) => { return context; }, getActiveProviderRuntimeContext: () => { - if (!activeContext) { + if (activeContext == null) { throw new Error( 'MissingProviderRuntimeError(provider-runtime): runtime registration missing', ); diff --git a/packages/cli/src/runtime/bucketFailover.ts b/packages/cli/src/runtime/bucketFailover.ts index 4ac1d417f7..3fdeda266d 100644 --- a/packages/cli/src/runtime/bucketFailover.ts +++ b/packages/cli/src/runtime/bucketFailover.ts @@ -188,8 +188,7 @@ export async function executeWithBucketFailover( const bucket = buckets[i]; try { - const response = await executor(request, bucket); - return response; + return await executor(request, bucket); } catch (error) { const err = error as Error; lastError = err; diff --git a/packages/cli/src/runtime/profileApplication.ts b/packages/cli/src/runtime/profileApplication.ts index 09cc33a6d2..8507a9bb5e 100644 --- a/packages/cli/src/runtime/profileApplication.ts +++ b/packages/cli/src/runtime/profileApplication.ts @@ -111,17 +111,15 @@ export function selectAvailableProvider( availableProviders: readonly string[], ): ProviderSelectionResult { const trimmedRequested = - typeof requestedProvider === 'string' - ? requestedProvider.trim() - : (requestedProvider ?? ''); + typeof requestedProvider === 'string' ? requestedProvider.trim() : ''; const warnings: string[] = []; if (trimmedRequested && availableProviders.includes(trimmedRequested)) { return { providerName: trimmedRequested, - warnings, didFallback: false, requestedProvider: trimmedRequested, + warnings, }; } @@ -140,9 +138,9 @@ export function selectAvailableProvider( return { providerName: fallbackProvider, - warnings, didFallback: Boolean(trimmedRequested), requestedProvider: trimmedRequested || null, + warnings, }; } @@ -184,7 +182,7 @@ async function wireAuthBeforeSwitch( let authKeyApplied = false; let authKeyNameApplied = false; let resolvedAuthKeyfilePath: string | null = null; - const authKeyName = sanitizedProfile.ephemeralSettings?.['auth-key-name']; + const authKeyName = sanitizedProfile.ephemeralSettings['auth-key-name']; if ( authKeyName && typeof authKeyName === 'string' && @@ -219,7 +217,7 @@ async function wireAuthBeforeSwitch( } } - const authKeyfile = sanitizedProfile.ephemeralSettings?.['auth-keyfile']; + const authKeyfile = sanitizedProfile.ephemeralSettings['auth-keyfile']; if ( authKeyfile && typeof authKeyfile === 'string' && @@ -265,7 +263,7 @@ async function wireAuthBeforeSwitch( } } - if (!authKeyApplied && sanitizedProfile.ephemeralSettings?.['auth-key']) { + if (!authKeyApplied && sanitizedProfile.ephemeralSettings['auth-key']) { const authKey = sanitizedProfile.ephemeralSettings['auth-key']; setEphemeralSetting('auth-key', authKey); setProviderApiKey(authKey); @@ -275,7 +273,7 @@ async function wireAuthBeforeSwitch( ); } - if (sanitizedProfile.ephemeralSettings?.['base-url']) { + if (sanitizedProfile.ephemeralSettings['base-url']) { const baseUrl = sanitizedProfile.ephemeralSettings['base-url']; setEphemeralSetting('base-url', baseUrl); setProviderBaseUrl(baseUrl); @@ -316,7 +314,7 @@ const PRE_APPLIED_EPHEMERAL_KEYS = new Set([ function applyNonAuthEphemerals(sanitizedProfile: Profile): void { const otherEphemerals = Object.entries( - sanitizedProfile.ephemeralSettings ?? {}, + sanitizedProfile.ephemeralSettings, ).filter(([key]) => !PRE_APPLIED_EPHEMERAL_KEYS.has(key)); for (const [key, value] of otherEphemerals) { @@ -382,7 +380,7 @@ async function applyModelAndParams( const modelToSet = isLB ? 'load-balancer' : requestedModel || fallbackModel; const modelResult = await setActiveModel(modelToSet); - const profileParams = sanitizedProfile.modelParams ?? {}; + const profileParams = sanitizedProfile.modelParams; const existingParams = getActiveModelParams(); for (const [key, value] of Object.entries(profileParams)) { @@ -397,7 +395,7 @@ async function applyModelAndParams( const provider = providerManager.getActiveProvider(); - if (!provider) { + if (provider == null) { throw new Error( `[oauth-manager] Active provider "${targetProviderName}" is not registered.`, ); @@ -431,7 +429,8 @@ export async function applyProfileWithGuards( // Get ProfileManager from runtime services (allows for dependency injection in tests) const profileManagerInstance = - 'profileManager' in runtimeServices && runtimeServices.profileManager + 'profileManager' in runtimeServices && + runtimeServices.profileManager != null ? runtimeServices.profileManager : new ProfileManager(); @@ -458,8 +457,8 @@ export async function applyProfileWithGuards( // Extract full config from loaded sub-profile // Resolve authToken from either auth-key or auth-keyfile - let authToken = subProfile.ephemeralSettings?.['auth-key']; - const authKeyfile = subProfile.ephemeralSettings?.['auth-keyfile']; + let authToken = subProfile.ephemeralSettings['auth-key']; + const authKeyfile = subProfile.ephemeralSettings['auth-keyfile']; // If auth-key not provided but auth-keyfile is, read the key from file if (!authToken && authKeyfile) { @@ -484,14 +483,14 @@ export async function applyProfileWithGuards( name: profileName, providerName: subProfile.provider, model: subProfile.model, - baseURL: subProfile.ephemeralSettings?.['base-url'], - authToken, - authKeyfile, - ephemeralSettings: (subProfile.ephemeralSettings ?? {}) as Record< + baseURL: subProfile.ephemeralSettings['base-url'], + ephemeralSettings: subProfile.ephemeralSettings as Record< string, unknown >, - modelParams: (subProfile.modelParams ?? {}) as Record, + modelParams: subProfile.modelParams as Record, + authToken, + authKeyfile, }; resolvedSubProfiles.push(resolved); @@ -547,12 +546,7 @@ export async function applyProfileWithGuards( : actualProfile.provider; logger.debug(() => { - const requested = - typeof requestedProvider === 'string' - ? requestedProvider - : requestedProvider === null - ? 'null' - : 'unset'; + const requested = requestedProvider; return `[profile] applying profile provider='${requested}' available=[${availableProviders.join( ', ', )}]`; @@ -572,8 +566,8 @@ export async function applyProfileWithGuards( const sanitizedProfile: Profile = { ...actualProfile, - modelParams: { ...(actualProfile.modelParams ?? {}) }, - ephemeralSettings: { ...(actualProfile.ephemeralSettings ?? {}) }, + modelParams: { ...actualProfile.modelParams }, + ephemeralSettings: { ...actualProfile.ephemeralSettings }, }; const setProviderApiKey = (apiKey: string | undefined): void => { @@ -611,7 +605,7 @@ export async function applyProfileWithGuards( ) { for (const alias of aliases) { const candidate = - sanitizedProfile.modelParams?.[alias as keyof ModelParams]; + sanitizedProfile.modelParams[alias as keyof ModelParams]; if (typeof candidate === 'string' && candidate.trim() !== '') { sanitizedProfile.ephemeralSettings[targetKey] = candidate; break; @@ -634,7 +628,7 @@ export async function applyProfileWithGuards( 'auth-keyfile', ); propagateModelParamToEphemeral(['base-url'], 'base-url'); - if (sanitizedProfile.modelParams) { + { const extraSensitiveKeys = [ 'apiKey', 'api-key', @@ -656,19 +650,17 @@ export async function applyProfileWithGuards( } logger.debug( () => - `[profile] target provider '${targetProviderName}' (requested='${requestedProvider ?? 'none'}')`, + `[profile] target provider '${targetProviderName}' (requested='${requestedProvider}')`, ); const providerRecord = providerManager.getProviderByName(targetProviderName); - if (!providerRecord) { + if (providerRecord == null) { warnings.push( `Provider '${targetProviderName}' not registered; skipping provider-specific updates.`, ); } const previousEphemerals = config.getEphemeralSettings(); - const previousEphemeralEntries = new Map( - Object.entries(previousEphemerals ?? {}), - ); + const previousEphemeralEntries = new Map(Object.entries(previousEphemerals)); const previousEphemeralKeys = Array.from(previousEphemeralEntries.keys()); // STEP 1: Clear ALL ephemerals first (except activeProvider) @@ -836,11 +828,11 @@ export async function applyProfileWithGuards( return { providerName: provider.name, modelName: appliedModelName, - infoMessages, - warnings, providerChanged: providerSwitch.changed, didFallback: selection.didFallback, - requestedProvider, baseUrl: resolvedBaseUrl, + infoMessages, + warnings, + requestedProvider, }; } diff --git a/packages/cli/src/runtime/profileSnapshot.ts b/packages/cli/src/runtime/profileSnapshot.ts index 28d50466ca..1fcf862ada 100644 --- a/packages/cli/src/runtime/profileSnapshot.ts +++ b/packages/cli/src/runtime/profileSnapshot.ts @@ -86,18 +86,13 @@ export function buildRuntimeProfileSnapshot(): Profile { const { config, settingsService, providerManager } = getCliRuntimeServices(); const providerName = resolveActiveProviderName(settingsService, config) ?? - providerManager.getActiveProviderName() ?? - config.getProvider() ?? - 'openai'; + providerManager.getActiveProviderName(); const providerSettings = getProviderSettingsSnapshot( settingsService, providerName, ); const currentModel = - (providerSettings.model as string | undefined) ?? - config.getModel() ?? - providerManager.getProviderByName(providerName)?.getDefaultModel?.() ?? - 'unknown'; + (providerSettings.model as string | undefined) ?? config.getModel(); const ephemeralSettings = config.getEphemeralSettings(); const snapshot: Record = {}; @@ -153,8 +148,8 @@ export function buildRuntimeProfileSnapshot(): Profile { version: 1, provider: providerName, model: currentModel, - modelParams, ephemeralSettings: snapshot as Profile['ephemeralSettings'], + modelParams, }; } @@ -208,8 +203,8 @@ function hasBucketSetChanged( } function getFailoverBuckets(config: CliRuntimeConfig): string[] { - const handler = config.getBucketFailoverHandler?.(); - return handler?.getBuckets?.() ?? []; + const handler = config.getBucketFailoverHandler(); + return handler?.getBuckets() ?? []; } function getOAuthBuckets(authConfig: ProfileAuthConfig | undefined): string[] { @@ -275,7 +270,7 @@ function clearProfileFailoverOnBucketChanges( 'Clearing session bucket and failover handler.', ); oauthManager.clearSessionBucket(profile.provider); - config.setBucketFailoverHandler?.(undefined); + config.setBucketFailoverHandler(undefined); } function wireStandardProfileFailover( @@ -365,7 +360,7 @@ async function wireLoadBalancerFailover( config: CliRuntimeConfig, profile: LoadBalancerProfile, ): Promise { - const subProfileNames = profile.profiles || []; + const subProfileNames = profile.profiles; logger.debug( () => `[issue1250] LoadBalancer profile detected with ${subProfileNames.length} sub-profile(s)`, @@ -377,7 +372,7 @@ async function wireLoadBalancerFailover( for (const subProfileName of subProfileNames) { const subProfile = await loadSubProfileOrNull(subProfileName, manager); - if (!subProfile) { + if (subProfile == null) { continue; } shouldClearHandler = @@ -397,7 +392,7 @@ async function wireLoadBalancerFailover( () => '[issue1467] Clearing failover handler after LB sub-profile bucket changes', ); - config.setBucketFailoverHandler?.(undefined); + config.setBucketFailoverHandler(undefined); } function buildProfileLoadResult( @@ -427,7 +422,7 @@ export async function applyProfileSnapshot( setCurrentProfileName(settingsService, options.profileName); const oauthManager = getCliOAuthManager(); - if (oauthManager) { + if (oauthManager != null) { scheduleProactiveRenewals(oauthManager, profile); clearProfileFailoverOnBucketChanges(oauthManager, config, profile); wireStandardProfileFailover(oauthManager, profile); @@ -447,7 +442,7 @@ export async function saveProfileSnapshot( const snapshot = buildRuntimeProfileSnapshot(); let finalProfile: Profile = snapshot; - if (additionalConfig) { + if (additionalConfig != null) { finalProfile = { ...snapshot, ...additionalConfig } as Profile; } @@ -522,13 +517,12 @@ export function getRuntimeDiagnosticsSnapshot(): RuntimeDiagnosticsSnapshot { const providerName = resolveActiveProviderName(settingsService, config) ?? - providerManager.getActiveProviderName() ?? - null; + providerManager.getActiveProviderName(); const modelValue = getActiveModelName(); const modelName = - modelValue && modelValue.trim() !== '' + modelValue !== '' && modelValue.trim() !== '' ? modelValue - : (config.getModel() ?? null); + : config.getModel(); const profileName = getActiveProfileName(); diff --git a/packages/cli/src/runtime/provider-alias-defaults.test.ts b/packages/cli/src/runtime/provider-alias-defaults.test.ts index 6fbdaac837..3f639d6086 100644 --- a/packages/cli/src/runtime/provider-alias-defaults.test.ts +++ b/packages/cli/src/runtime/provider-alias-defaults.test.ts @@ -55,7 +55,7 @@ const { providerOrChanges?: string | Record, changes?: Record, ): Promise { - if (typeof providerOrChanges === 'string' && changes) { + if (typeof providerOrChanges === 'string' && changes != null) { for (const [key, value] of Object.entries(changes)) { this.setProviderSetting(providerOrChanges, key, value); } @@ -206,7 +206,7 @@ vi.mock('@vybestack/llxprt-code-core', async (importOriginal) => { return context; }, getActiveProviderRuntimeContext: () => { - if (!activeContext) { + if (activeContext == null) { throw new Error( 'MissingProviderRuntimeError(provider-runtime): runtime registration missing', ); diff --git a/packages/cli/src/runtime/providerConfigUtils.test.ts b/packages/cli/src/runtime/providerConfigUtils.test.ts index 7d23db5103..6d7b323c8b 100644 --- a/packages/cli/src/runtime/providerConfigUtils.test.ts +++ b/packages/cli/src/runtime/providerConfigUtils.test.ts @@ -51,7 +51,7 @@ describe('providerConfigUtils runtime wrappers', () => { expect(updateActiveProviderApiKeyMock).toHaveBeenCalledWith( 'sanitized-api-key', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ success: true, message: 'API key set', isPaidMode: false, diff --git a/packages/cli/src/runtime/providerMutations.spec.ts b/packages/cli/src/runtime/providerMutations.spec.ts index 904db876f8..e47a408147 100644 --- a/packages/cli/src/runtime/providerMutations.spec.ts +++ b/packages/cli/src/runtime/providerMutations.spec.ts @@ -86,7 +86,7 @@ describe('providerMutations', () => { it('returns undefined for null/undefined input', () => { expect(normalizeProviderBaseUrl(null)).toBeUndefined(); - expect(normalizeProviderBaseUrl(undefined)).toBeUndefined(); + expect(normalizeProviderBaseUrl()).toBeUndefined(); }); it('returns undefined for empty string', () => { diff --git a/packages/cli/src/runtime/providerMutations.ts b/packages/cli/src/runtime/providerMutations.ts index 8b4d3194ed..7efa88d301 100644 --- a/packages/cli/src/runtime/providerMutations.ts +++ b/packages/cli/src/runtime/providerMutations.ts @@ -11,7 +11,7 @@ * Depends on runtimeAccessors.ts for runtime services access. */ -import { Config, DebugLogger } from '@vybestack/llxprt-code-core'; +import { type Config, DebugLogger } from '@vybestack/llxprt-code-core'; import type { ModelDefaultRule } from '../providers/providerAliases.js'; import { getCliRuntimeServices } from './runtimeAccessors.js'; @@ -102,7 +102,7 @@ function extractConfiguredProviderBaseUrl( providerConfig?: { baseUrl?: string; baseURL?: string }; } ).providerConfig; - if (!configCandidate) { + if (configCandidate == null) { return undefined; } return normalizeProviderBaseUrl( @@ -118,7 +118,7 @@ function extractBaseProviderConfigUrl( baseProviderConfig?: { baseURL?: string; baseUrl?: string }; } ).baseProviderConfig; - if (!baseProviderConfig) { + if (baseProviderConfig == null) { return undefined; } return normalizeProviderBaseUrl( @@ -271,12 +271,12 @@ export async function updateActiveProviderApiKey( ); return { changed: true, - providerName, message: `API key removed for provider '${providerName}'` + (providerName === 'gemini' && isPaidMode === false ? '\n✓ You are now using OAuth (no paid usage).' : ''), + providerName, isPaidMode, }; } @@ -295,12 +295,12 @@ export async function updateActiveProviderApiKey( ); return { changed: true, - providerName, message: `API key updated for provider '${providerName}'` + (providerName === 'gemini' && isPaidMode !== false ? '\nWARNING: Gemini now runs in paid mode.' : ''), + providerName, isPaidMode, }; } @@ -321,8 +321,8 @@ export async function updateActiveProviderBaseUrl( config.setEphemeralSetting('base-url', undefined); return { changed: true, - providerName, message: `Base URL cleared; provider '${providerName}' now uses the default endpoint.`, + providerName, }; } @@ -334,9 +334,9 @@ export async function updateActiveProviderBaseUrl( config.setEphemeralSetting('base-url', normalizedBaseUrl); return { changed: true, - providerName, message: `Base URL updated to '${normalizedBaseUrl}' for provider '${providerName}'.`, baseUrl: normalizedBaseUrl, + providerName, }; } @@ -438,7 +438,7 @@ export async function setActiveModel( } // Stateless recomputation of model defaults. - if (aliasConfig?.modelDefaults) { + if (aliasConfig?.modelDefaults != null) { recomputeAndApplyModelDefaultsDiff( config, previousModel, @@ -449,8 +449,8 @@ export async function setActiveModel( return { providerName: activeProvider.name, - previousModel, nextModel: modelName, + previousModel, authRefreshed, }; } diff --git a/packages/cli/src/runtime/providerSwitch.ts b/packages/cli/src/runtime/providerSwitch.ts index 7b14359c94..4765257991 100644 --- a/packages/cli/src/runtime/providerSwitch.ts +++ b/packages/cli/src/runtime/providerSwitch.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Config, DebugLogger } from '@vybestack/llxprt-code-core'; +import { type Config, DebugLogger } from '@vybestack/llxprt-code-core'; import type { HistoryItemWithoutId } from '../ui/types.js'; import { getCliOAuthManager, @@ -376,7 +376,7 @@ async function handleAnthropicOAuth( } const oauthManager = getCliOAuthManager(); - if (!oauthManager) { + if (oauthManager == null) { return; } @@ -487,7 +487,7 @@ function applyAnthropicOAuthDefaults(context: ProviderSwitchContext): void { function applyAliasEphemeralSettings(context: ProviderSwitchContext): void { const aliasEphemeralSettings = context.aliasConfig?.ephemeralSettings; if ( - !aliasEphemeralSettings || + aliasEphemeralSettings == null || typeof aliasEphemeralSettings !== 'object' || Array.isArray(aliasEphemeralSettings) ) { @@ -561,7 +561,7 @@ function applyModelDefaults(context: ProviderSwitchContext): void { if ( context.skipModelDefaults || !context.modelToApply || - !context.aliasConfig?.modelDefaults + context.aliasConfig?.modelDefaults == null ) { return; } @@ -653,14 +653,9 @@ function createProviderSwitchContext( if (currentProvider === name) { return { - name, - currentProvider, autoOAuth: false, skipModelDefaults: true, preserveEphemerals: [], - config, - settingsService, - providerManager, activeProvider: providerManager.getActiveProvider(), baseProvider: {}, aliasConfig: undefined, @@ -676,6 +671,11 @@ function createProviderSwitchContext( maxOutputTokensBeforeSwitch: undefined, infoMessages: [], addItem: options.addItem, + name, + currentProvider, + config, + settingsService, + providerManager, }; } @@ -685,14 +685,8 @@ function createProviderSwitchContext( ]; const context: ProviderSwitchContext = { - name, - currentProvider, autoOAuth: options.autoOAuth ?? false, skipModelDefaults: options.skipModelDefaults ?? false, - preserveEphemerals, - config, - settingsService, - providerManager, activeProvider: providerManager.getActiveProvider(), baseProvider: {}, aliasConfig: getAliasConfig(name), @@ -708,6 +702,12 @@ function createProviderSwitchContext( maxOutputTokensBeforeSwitch: undefined, infoMessages: [], addItem: options.addItem, + name, + currentProvider, + preserveEphemerals, + config, + settingsService, + providerManager, }; const ephemeralSnapshot = clearEphemeralsForSwitch(context); diff --git a/packages/cli/src/runtime/runtimeAccessors.spec.ts b/packages/cli/src/runtime/runtimeAccessors.spec.ts index 20bcf7021a..5534274dca 100644 --- a/packages/cli/src/runtime/runtimeAccessors.spec.ts +++ b/packages/cli/src/runtime/runtimeAccessors.spec.ts @@ -12,9 +12,9 @@ import { import { configureCliStatelessHardening } from './statelessHardening.js'; import { setCliRuntimeContext } from './runtimeLifecycle.js'; import { - Config, - SettingsService, - ProviderManager, + type Config, + type SettingsService, + type ProviderManager, clearActiveProviderRuntimeContext, } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/runtime/runtimeAccessors.ts b/packages/cli/src/runtime/runtimeAccessors.ts index ad150d2f35..9b8c0abe6f 100644 --- a/packages/cli/src/runtime/runtimeAccessors.ts +++ b/packages/cli/src/runtime/runtimeAccessors.ts @@ -18,17 +18,17 @@ */ import { - Config, DebugLogger, - SettingsService, - type ProviderManager, - type ProfileManager, createProviderRuntimeContext, peekActiveProviderRuntimeContext, getProviderConfigKeys, + type SettingsService, + type Config, + type ProviderManager, + type ProfileManager, type HydratedModel, } from '@vybestack/llxprt-code-core'; -import { OAuthManager } from '../auth/oauth-manager.js'; +import type { OAuthManager } from '../auth/oauth-manager.js'; import type { HistoryItemWithoutId } from '../ui/types.js'; import { getCurrentRuntimeScope } from './runtimeContextFactory.js'; import { @@ -73,22 +73,22 @@ export function getCliRuntimeContext() { const identity = resolveActiveRuntimeIdentity(); const entry = runtimeRegistry.get(identity.runtimeId); - if (!entry || !entry.config) { + if (entry?.config == null) { const registeredIds = Array.from(runtimeRegistry.keys()); const scope = getCurrentRuntimeScope(); const activeCtx = peekActiveProviderRuntimeContext(); logger.debug( () => - `[getCliRuntimeContext] MISS: runtimeId=${identity.runtimeId}, hasEntry=${!!entry}, hasConfig=${!!(entry && entry.config)}, registered=[${registeredIds.join(', ')}], scope=${JSON.stringify(scope)}, activeCtx.runtimeId=${activeCtx?.runtimeId}`, + `[getCliRuntimeContext] MISS: runtimeId=${identity.runtimeId}, hasEntry=${!!entry}, hasConfig=${!!entry?.config}, registered=[${registeredIds.join(', ')}], scope=${JSON.stringify(scope)}, activeCtx.runtimeId=${activeCtx?.runtimeId}`, ); } - if (entry && entry.config) { + if (entry?.config != null) { // @plan:PLAN-20251023-STATELESS-HARDENING.P08 // @requirement:REQ-SP4-004 - Remove singleton fallbacks when stateless hardening is enabled const settingsService = entry.settingsService; - if (isStatelessProviderIntegrationEnabled() && !settingsService) { + if (isStatelessProviderIntegrationEnabled() && settingsService == null) { throw new Error( formatMissingRuntimeMessage({ runtimeId: identity.runtimeId, @@ -100,9 +100,7 @@ export function getCliRuntimeContext() { // Fallback path for legacy compatibility (disabled under stateless hardening) const resolvedSettings = - settingsService ?? - entry.config.getSettingsService() ?? - new SettingsService(); + settingsService ?? entry.config.getSettingsService(); return createProviderRuntimeContext({ settingsService: resolvedSettings, config: entry.config, @@ -138,7 +136,7 @@ export function getCliRuntimeServices(): CliRuntimeServices { const entry = requireRuntimeEntry(runtimeId); const context = getCliRuntimeContext(); const config = entry.config ?? context.config; - if (!config) { + if (config == null) { throw new Error( formatNormalizationFailureMessage({ runtimeId, @@ -148,17 +146,8 @@ export function getCliRuntimeServices(): CliRuntimeServices { ); } const settingsService = entry.settingsService ?? context.settingsService; - if (!settingsService) { - throw new Error( - formatMissingRuntimeMessage({ - runtimeId, - missingFields: ['SettingsService'], - hint: 'Call activateIsolatedRuntimeContext() or inject a runtime-specific SettingsService for tests.', - }), - ); - } const providerManager = entry.providerManager; - if (!providerManager) { + if (providerManager == null) { throw new Error( formatMissingRuntimeMessage({ runtimeId, @@ -186,7 +175,7 @@ export function getCliProviderManager( const entry = requireRuntimeEntry(runtimeId); const oauthManager = entry.oauthManager; - if (options.addItem && oauthManager) { + if (options.addItem != null && oauthManager != null) { const providersMap = ( oauthManager as unknown as { providers?: Map; @@ -203,7 +192,7 @@ export function getCliProviderManager( ) => number, ) => void; }; - if (p.name && p.setAddItem) { + if (p.name && p.setAddItem != null) { p.setAddItem(options.addItem); } } @@ -218,7 +207,7 @@ export function isCliRuntimeStatelessReady(): boolean { } const { runtimeId } = resolveActiveRuntimeIdentity(); const entry = runtimeRegistry.get(runtimeId); - if (!entry) { + if (entry == null) { return false; } return Boolean( @@ -253,13 +242,13 @@ export function ensureStatelessProviderReady(): void { const providerManager = entry.providerManager; const missingFields: string[] = []; - if (!settingsService) { + if (settingsService == null) { missingFields.push('SettingsService'); } - if (!config) { + if (config == null) { missingFields.push('Config'); } - if (!providerManager) { + if (providerManager == null) { missingFields.push('ProviderManager'); } @@ -378,7 +367,7 @@ export function getActiveModelName(): string { try { const provider = providerManager.getActiveProvider(); - return provider.getDefaultModel?.() ?? ''; + return provider.getDefaultModel(); } catch { return ''; } @@ -422,9 +411,9 @@ export function getActiveProviderStatus(): ProviderRuntimeStatus { return { providerName: provider.name, + isPaidMode: provider.isPaidMode?.(), modelName, displayLabel, - isPaidMode: provider.isPaidMode?.(), baseURL, }; } catch { diff --git a/packages/cli/src/runtime/runtimeContextFactory.ts b/packages/cli/src/runtime/runtimeContextFactory.ts index 7a128eee3e..82df9a2e76 100644 --- a/packages/cli/src/runtime/runtimeContextFactory.ts +++ b/packages/cli/src/runtime/runtimeContextFactory.ts @@ -14,7 +14,7 @@ import * as path from 'node:path'; import { Config, - KeyringTokenStore, + type KeyringTokenStore, ProviderManager, SettingsService, MessageBus, @@ -201,7 +201,7 @@ export function registerIsolatedRuntimeBindings( export function createIsolatedRuntimeContext( options: IsolatedRuntimeContextOptions = {}, ): IsolatedRuntimeContextHandle { - if (!activationBindings) { + if (activationBindings == null) { throw new Error( 'Isolated runtime activation bindings must be registered before creating contexts.', ); @@ -229,8 +229,8 @@ export function createIsolatedRuntimeContext( new Config({ sessionId: runtimeId, targetDir: workspaceDir, - debugMode, cwd: workspaceDir, + debugMode, model, settingsService, }); @@ -250,22 +250,21 @@ export function createIsolatedRuntimeContext( const llxprtDir = path.join(os.homedir(), '.llxprt'); const resolvedProfileManager = - optionsConfigWithManagers?.getProfileManager?.() ?? - configWithManagers.getProfileManager?.() ?? + optionsConfigWithManagers?.getProfileManager() ?? + configWithManagers.getProfileManager() ?? new ProfileManager(path.join(llxprtDir, 'profiles')); const resolvedSubagentManager = - optionsConfigWithManagers?.getSubagentManager?.() ?? - configWithManagers.getSubagentManager?.() ?? + optionsConfigWithManagers?.getSubagentManager() ?? + configWithManagers.getSubagentManager() ?? new SubagentManager( path.join(llxprtDir, 'subagents'), resolvedProfileManager, ); - configWithManagers.setProfileManager?.(resolvedProfileManager); - configWithManagers.setSubagentManager?.(resolvedSubagentManager); + configWithManagers.setProfileManager(resolvedProfileManager); + configWithManagers.setSubagentManager(resolvedSubagentManager); - const resolvedSettingsService = - config.getSettingsService() ?? settingsService; + const resolvedSettingsService = config.getSettingsService(); const sessionMessageBus = new MessageBus( config.getPolicyEngine(), config.getDebugMode(), @@ -282,9 +281,9 @@ export function createIsolatedRuntimeContext( const initialRuntimeContext = createProviderRuntimeContext({ settingsService: resolvedSettingsService, + metadata: baseMetadata, config, runtimeId, - metadata: baseMetadata, }); const providerManager = new ProviderManager({ @@ -306,7 +305,7 @@ export function createIsolatedRuntimeContext( const activate = async ( activationOptions?: IsolatedRuntimeActivationOptions, ): Promise => { - if (!activationBindings) { + if (activationBindings == null) { throw new Error( 'Isolated runtime activation bindings must be registered before activation.', ); @@ -330,9 +329,9 @@ export function createIsolatedRuntimeContext( await runWithRuntimeScope(scope, async () => { const scopedRuntime = createProviderRuntimeContext({ settingsService: resolvedSettingsService, - config, runtimeId: currentRuntimeId, metadata: currentMetadata, + config, }); ( providerManager as unknown as { runtime?: ProviderRuntimeContext } @@ -346,14 +345,14 @@ export function createIsolatedRuntimeContext( }), ); - if (options.prepare) { + if (options.prepare != null) { await options.prepare({ - config, settingsService: resolvedSettingsService, - providerManager, - oauthManager, runtimeId: currentRuntimeId, metadata: currentMetadata, + config, + providerManager, + oauthManager, }); } @@ -377,7 +376,7 @@ export function createIsolatedRuntimeContext( * Clear activation state and invoke onCleanup hooks in the reverse order detailed in Step 7. */ const cleanup = async (): Promise => { - if (!active && !options.onCleanup) { + if (!active && options.onCleanup == null) { return; } @@ -388,7 +387,7 @@ export function createIsolatedRuntimeContext( const bindings = activationBindings; await runWithRuntimeScope(scope, async () => { - if (bindings) { + if (bindings != null) { await Promise.resolve(bindings.resetInfrastructure()); } clearActiveProviderRuntimeContext(); @@ -396,17 +395,17 @@ export function createIsolatedRuntimeContext( const revocation: RuntimeAuthScopeFlushResult = flushRuntimeAuthScope(currentRuntimeId); - if (options.onCleanup) { + if (options.onCleanup != null) { await options.onCleanup({ - config, settingsService: resolvedSettingsService, - providerManager, runtimeId: currentRuntimeId, metadata: currentMetadata, + config, + providerManager, }); } - if (bindings?.disposeRuntime) { + if (bindings?.disposeRuntime != null) { await Promise.resolve( bindings.disposeRuntime(currentRuntimeId, revocation), ); diff --git a/packages/cli/src/runtime/runtimeLifecycle.spec.ts b/packages/cli/src/runtime/runtimeLifecycle.spec.ts index 009d060d27..ab90d69f4f 100644 --- a/packages/cli/src/runtime/runtimeLifecycle.spec.ts +++ b/packages/cli/src/runtime/runtimeLifecycle.spec.ts @@ -17,13 +17,13 @@ import { activateIsolatedRuntimeContext, } from './runtimeLifecycle.js'; import { - Config, - SettingsService, - ProviderManager, + type Config, + type SettingsService, + type ProviderManager, clearActiveProviderRuntimeContext, - MessageBus, + type MessageBus, } from '@vybestack/llxprt-code-core'; -import { OAuthManager } from '../auth/oauth-manager.js'; +import type { OAuthManager } from '../auth/oauth-manager.js'; import { getCliProviderManager } from './runtimeAccessors.js'; /** diff --git a/packages/cli/src/runtime/runtimeLifecycle.ts b/packages/cli/src/runtime/runtimeLifecycle.ts index aed168a156..4f8a2e5d63 100644 --- a/packages/cli/src/runtime/runtimeLifecycle.ts +++ b/packages/cli/src/runtime/runtimeLifecycle.ts @@ -17,7 +17,7 @@ */ import { - Config, + type Config, DebugLogger, type SettingsService, type ProfileManager, @@ -26,7 +26,7 @@ import { createProviderRuntimeContext, setActiveProviderRuntimeContext, } from '@vybestack/llxprt-code-core'; -import { OAuthManager } from '../auth/oauth-manager.js'; +import type { OAuthManager } from '../auth/oauth-manager.js'; import { registerProviderManagerSingleton } from '../providers/providerManagerInstance.js'; import { type IsolatedRuntimeActivationOptions, @@ -53,13 +53,13 @@ export async function activateIsolatedRuntimeContext( ): Promise { const runtimeId = options.runtimeId ?? handle.runtimeId; const mergedMetadata = { - ...(handle.metadata ?? {}), + ...handle.metadata, ...(options.metadata ?? {}), }; const overrides: IsolatedRuntimeActivationOptions = { ...options, - runtimeId, metadata: mergedMetadata, + runtimeId, }; enterRuntimeScope({ runtimeId, metadata: mergedMetadata }); @@ -90,13 +90,13 @@ export function registerCliProviderInfrastructure( registerProviderManagerSingleton(manager, oauthManager); const config = entry.config ?? null; - if (config) { + if (config != null) { config.setProviderManager(manager); manager.setConfig(config); logger.debug( () => - `[cli-runtime] ProviderManager#setConfig applied (loggingEnabled=${config.getConversationLoggingEnabled?.() ?? false})`, + `[cli-runtime] ProviderManager#setConfig applied (loggingEnabled=${config.getConversationLoggingEnabled()})`, ); upsertRuntimeEntry(runtimeId, { config }); } @@ -152,7 +152,7 @@ export function setCliRuntimeContext( }); logger.debug(() => { const providerLabel = - config && typeof config.getProvider === 'function' + config != null && typeof config.getProvider === 'function' ? ` (provider=${config.getProvider() ?? 'unset'})` : ''; return `[cli-runtime] Registering runtime context ${runtimeId}${providerLabel}`; @@ -160,9 +160,9 @@ export function setCliRuntimeContext( setActiveProviderRuntimeContext(nextContext); upsertRuntimeEntry(runtimeId, { - settingsService, config: config ?? null, - metadata, profileManager: options.profileManager, + settingsService, + metadata, }); } diff --git a/packages/cli/src/runtime/runtimeRegistry.ts b/packages/cli/src/runtime/runtimeRegistry.ts index 1f9901566a..50950d3dff 100644 --- a/packages/cli/src/runtime/runtimeRegistry.ts +++ b/packages/cli/src/runtime/runtimeRegistry.ts @@ -12,16 +12,16 @@ */ import { - Config, + type Config, DebugLogger, - SettingsService, - ProfileManager, + type SettingsService, + type ProfileManager, clearActiveProviderRuntimeContext, peekActiveProviderRuntimeContext, type ProviderManager, type RuntimeAuthScopeFlushResult, } from '@vybestack/llxprt-code-core'; -import { OAuthManager } from '../auth/oauth-manager.js'; +import type { OAuthManager } from '../auth/oauth-manager.js'; import { resetProviderManager } from '../providers/providerManagerInstance.js'; import { getCurrentRuntimeScope } from './runtimeContextFactory.js'; import { formatMissingRuntimeMessage } from './messages.js'; @@ -46,12 +46,12 @@ export function resolveActiveRuntimeIdentity(): { metadata: Record; } { const scope = getCurrentRuntimeScope(); - if (scope) { + if (scope != null) { return scope; } const context = peekActiveProviderRuntimeContext(); - if (context) { + if (context != null) { const candidateId = typeof context.runtimeId === 'string' && context.runtimeId.trim() !== '' ? context.runtimeId @@ -123,7 +123,7 @@ export function upsertRuntimeEntry( export function requireRuntimeEntry(runtimeId: string): RuntimeRegistryEntry { const entry = runtimeRegistry.get(runtimeId); - if (entry) { + if (entry != null) { return entry; } diff --git a/packages/cli/src/runtime/settingsResolver.ts b/packages/cli/src/runtime/settingsResolver.ts index 0424fb27a2..5bb7d15098 100644 --- a/packages/cli/src/runtime/settingsResolver.ts +++ b/packages/cli/src/runtime/settingsResolver.ts @@ -12,7 +12,7 @@ import { homedir } from 'node:os'; import { readFile } from 'node:fs/promises'; -import { Config, SettingsService } from '@vybestack/llxprt-code-core'; +import type { Config, SettingsService } from '@vybestack/llxprt-code-core'; import { applyCliSetArguments } from '../config/cliEphemeralSettings.js'; import { getCliRuntimeServices } from './runtimeAccessors.js'; import { @@ -58,7 +58,11 @@ export async function applyCliArgumentOverrides( // Apply --set arguments const setArgsToUse = bootstrapArgs?.setOverrides ?? argv.set; - if (setArgsToUse && Array.isArray(setArgsToUse) && setArgsToUse.length > 0) { + if ( + setArgsToUse != null && + Array.isArray(setArgsToUse) && + setArgsToUse.length > 0 + ) { applyCliSetArguments(config, setArgsToUse); } diff --git a/packages/cli/src/runtime/statelessHardening.ts b/packages/cli/src/runtime/statelessHardening.ts index be1a8a4d83..3b4d96ab58 100644 --- a/packages/cli/src/runtime/statelessHardening.ts +++ b/packages/cli/src/runtime/statelessHardening.ts @@ -64,7 +64,7 @@ function normalizeStatelessPreference( function readStatelessPreferenceFromMetadata( metadata: Record | undefined, ): StatelessHardeningPreference | null { - if (!metadata) { + if (metadata == null) { return null; } for (const key of STATELESS_METADATA_KEYS) { diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index e674588394..e8db9664d1 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -80,7 +80,7 @@ export class BuiltinCommandLoader implements ICommandLoader { constructor(private config: Config | null) { // Access extensionEnablementManager if available on config - if (config && 'extensionEnablementManager' in config) { + if (config != null && 'extensionEnablementManager' in config) { this.extensionEnablementManager = ( config as Config & { extensionEnablementManager?: ExtensionEnablementManager; @@ -102,7 +102,7 @@ export class BuiltinCommandLoader implements ICommandLoader { const allCommands = this.registerBuiltinCommands(); // Filter out commands from disabled extensions - if (this.extensionEnablementManager) { + if (this.extensionEnablementManager != null) { return allCommands.filter((cmd) => { // Built-in commands (no extensionName) are always included if (!cmd.extensionName) { diff --git a/packages/cli/src/services/CommandService.test.ts b/packages/cli/src/services/CommandService.test.ts index 1bdbb814b4..f567f64b98 100644 --- a/packages/cli/src/services/CommandService.test.ts +++ b/packages/cli/src/services/CommandService.test.ts @@ -6,15 +6,15 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { CommandService } from './CommandService.js'; -import { type ICommandLoader } from './types.js'; +import type { ICommandLoader } from './types.js'; import { CommandKind, type SlashCommand } from '../ui/commands/types.js'; import { DebugLogger } from '@vybestack/llxprt-code-core'; const createMockCommand = (name: string, kind: CommandKind): SlashCommand => ({ - name, description: `Description for ${name}`, - kind, action: vi.fn(), + name, + kind, }); const mockCommandA = createMockCommand('command-a', CommandKind.BUILT_IN); @@ -54,7 +54,7 @@ describe('CommandService', () => { expect(mockLoader.loadCommands).toHaveBeenCalledTimes(1); expect(commands).toHaveLength(2); - expect(commands).toEqual( + expect(commands).toStrictEqual( expect.arrayContaining([mockCommandA, mockCommandB]), ); }); @@ -72,7 +72,7 @@ describe('CommandService', () => { expect(loader1.loadCommands).toHaveBeenCalledTimes(1); expect(loader2.loadCommands).toHaveBeenCalledTimes(1); expect(commands).toHaveLength(2); - expect(commands).toEqual( + expect(commands).toStrictEqual( expect.arrayContaining([mockCommandA, mockCommandC]), ); }); @@ -96,10 +96,10 @@ describe('CommandService', () => { const commandB = commands.find((cmd) => cmd.name === 'command-b'); expect(commandB).toBeDefined(); expect(commandB?.kind).toBe(CommandKind.FILE); // Verify it's the overridden version. - expect(commandB).toEqual(mockCommandB_Override); + expect(commandB).toStrictEqual(mockCommandB_Override); // Ensure the other commands are still present. - expect(commands).toEqual( + expect(commands).toStrictEqual( expect.arrayContaining([ mockCommandA, mockCommandC, @@ -121,7 +121,7 @@ describe('CommandService', () => { expect(emptyLoader.loadCommands).toHaveBeenCalledTimes(1); expect(commands).toHaveLength(2); - expect(commands).toEqual( + expect(commands).toStrictEqual( expect.arrayContaining([mockCommandA, mockCommandB]), ); }); @@ -143,7 +143,7 @@ describe('CommandService', () => { const commands = service.getCommands(); expect(commands).toHaveLength(1); - expect(commands).toEqual([mockCommandA]); + expect(commands).toStrictEqual([mockCommandA]); expect(debugSpy).toHaveBeenCalledWith('A command loader failed:', error); debugSpy.mockRestore(); diff --git a/packages/cli/src/services/CommandService.ts b/packages/cli/src/services/CommandService.ts index 3bcabea4fa..cb3d019c57 100644 --- a/packages/cli/src/services/CommandService.ts +++ b/packages/cli/src/services/CommandService.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SlashCommand } from '../ui/commands/types.js'; -import { ICommandLoader } from './types.js'; +import type { SlashCommand } from '../ui/commands/types.js'; +import type { ICommandLoader } from './types.js'; import { debugLogger } from '@vybestack/llxprt-code-core'; /** diff --git a/packages/cli/src/services/FileCommandLoader.test.ts b/packages/cli/src/services/FileCommandLoader.test.ts index 19799b6e93..d60fb01bed 100644 --- a/packages/cli/src/services/FileCommandLoader.test.ts +++ b/packages/cli/src/services/FileCommandLoader.test.ts @@ -6,7 +6,7 @@ import * as glob from 'glob'; import * as path from 'node:path'; -import { Config, Storage } from '@vybestack/llxprt-code-core'; +import { type Config, Storage } from '@vybestack/llxprt-code-core'; import mock from 'mock-fs'; import { FileCommandLoader } from './FileCommandLoader.js'; import { assert, vi } from 'vitest'; @@ -20,7 +20,7 @@ import { ShellProcessor, } from './prompt-processors/shellProcessor.js'; import { DefaultArgumentProcessor } from './prompt-processors/argumentProcessor.js'; -import { CommandContext } from '../ui/commands/types.js'; +import type { CommandContext } from '../ui/commands/types.js'; const mockShellProcess = vi.hoisted(() => vi.fn()); const mockAtFileProcess = vi.hoisted(() => vi.fn()); @@ -519,7 +519,7 @@ describe('FileCommandLoader', () => { expect(commands).toHaveLength(3); const commandNames = commands.map((cmd) => cmd.name); - expect(commandNames).toEqual(['user', 'project', 'ext']); + expect(commandNames).toStrictEqual(['user', 'project', 'ext']); const extCommand = commands.find((cmd) => cmd.name === 'ext'); expect(extCommand?.extensionName).toBe('test-ext'); @@ -760,7 +760,7 @@ describe('FileCommandLoader', () => { expect(commands).toHaveLength(3); const commandNames = commands.map((cmd) => cmd.name).sort(); - expect(commandNames).toEqual(['b:c', 'b:d:e', 'simple']); + expect(commandNames).toStrictEqual(['b:c', 'b:d:e', 'simple']); const nestedCmd = commands.find((cmd) => cmd.name === 'b:c'); expect(nestedCmd?.extensionName).toBe('a'); @@ -945,7 +945,7 @@ describe('FileCommandLoader', () => { result?.type === 'confirm_shell_commands', 'Incorrect action type', ); - expect(result.commandsToConfirm).toEqual(['rm -rf /']); + expect(result.commandsToConfirm).toStrictEqual(['rm -rf /']); expect(result.originalInvocation.raw).toBe(rawInvocation); }); @@ -1090,7 +1090,7 @@ describe('FileCommandLoader', () => { assert(result?.type === 'submit_prompt', 'Incorrect action type'); // AtFileProcessor is not actually used by FileCommandLoader // so the @{} syntax is not processed - expect(result.content).toEqual('Context from file: @{./test.txt}'); + expect(result.content).toStrictEqual('Context from file: @{./test.txt}'); }); }); diff --git a/packages/cli/src/services/FileCommandLoader.ts b/packages/cli/src/services/FileCommandLoader.ts index 9179e37b96..26b298f530 100644 --- a/packages/cli/src/services/FileCommandLoader.ts +++ b/packages/cli/src/services/FileCommandLoader.ts @@ -9,17 +9,17 @@ import path from 'path'; import toml from '@iarna/toml'; import { glob } from 'glob'; import { z } from 'zod'; -import { Config, Storage, debugLogger } from '@vybestack/llxprt-code-core'; -import { ICommandLoader } from './types.js'; +import { type Config, Storage, debugLogger } from '@vybestack/llxprt-code-core'; +import type { ICommandLoader } from './types.js'; import { - CommandContext, + type CommandContext, CommandKind, - SlashCommand, - SlashCommandActionReturn, + type SlashCommand, + type SlashCommandActionReturn, } from '../ui/commands/types.js'; import { DefaultArgumentProcessor } from './prompt-processors/argumentProcessor.js'; import { - IPromptProcessor, + type IPromptProcessor, SHORTHAND_ARGS_PLACEHOLDER, SHELL_INJECTION_TRIGGER, } from './prompt-processors/types.js'; @@ -61,9 +61,9 @@ export class FileCommandLoader implements ICommandLoader { private readonly isTrustedFolder: boolean; constructor(private readonly config: Config | null) { - this.folderTrustEnabled = !!config?.getFolderTrust(); - this.isTrustedFolder = !!config?.isTrustedFolder(); - this.projectRoot = config?.getProjectRoot() || process.cwd(); + this.folderTrustEnabled = config?.getFolderTrust() === true; + this.isTrustedFolder = config?.isTrustedFolder() === true; + this.projectRoot = config?.getProjectRoot() ?? process.cwd(); } /** @@ -86,8 +86,8 @@ export class FileCommandLoader implements ICommandLoader { const globOptions = { nodir: true, dot: true, - signal, follow: true, + signal, }; // Load commands from each directory @@ -114,10 +114,7 @@ export class FileCommandLoader implements ICommandLoader { // Add all commands without deduplication allCommands.push(...commands); } catch (error) { - if ( - !signal.aborted && - (error as { code?: string })?.code !== 'ENOENT' - ) { + if (!signal.aborted && (error as { code?: string }).code !== 'ENOENT') { debugLogger.error( `[FileCommandLoader] Error loading commands from ${dirInfo.path}:`, error, @@ -146,7 +143,7 @@ export class FileCommandLoader implements ICommandLoader { dirs.push({ path: storage.getProjectCommandsDir() }); // 3. Extension commands (processed last to detect all conflicts) - if (this.config) { + if (this.config != null) { const activeExtensions = this.config .getExtensions() .filter((ext) => ext.isActive) @@ -224,7 +221,7 @@ export class FileCommandLoader implements ICommandLoader { // Add extension name tag for extension commands const defaultDescription = `Custom command from ${path.basename(filePath)}`; - let description = validDef.description || defaultDescription; + let description = validDef.description ?? defaultDescription; if (extensionName) { description = `[${extensionName}] ${description}`; } @@ -250,14 +247,12 @@ export class FileCommandLoader implements ICommandLoader { return { name: baseCommandName, - description, kind: CommandKind.FILE, - extensionName, action: async ( context: CommandContext, _args: string, ): Promise => { - if (!context.invocation) { + if (context.invocation == null) { debugLogger.error( `[FileCommandLoader] Critical error: Command '${baseCommandName}' was executed without invocation context.`, ); @@ -293,6 +288,8 @@ export class FileCommandLoader implements ICommandLoader { throw e; } }, + description, + extensionName, }; } } diff --git a/packages/cli/src/services/McpPromptLoader.test.ts b/packages/cli/src/services/McpPromptLoader.test.ts index 73318fcb8e..71ede2148b 100644 --- a/packages/cli/src/services/McpPromptLoader.test.ts +++ b/packages/cli/src/services/McpPromptLoader.test.ts @@ -5,9 +5,9 @@ */ import { McpPromptLoader } from './McpPromptLoader.js'; -import { Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; import * as cliCore from '@vybestack/llxprt-code-core'; -import { PromptArgument } from '@modelcontextprotocol/sdk/types.js'; +import type { PromptArgument } from '@modelcontextprotocol/sdk/types.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { CommandKind, type CommandContext } from '../ui/commands/types.js'; diff --git a/packages/cli/src/services/McpPromptLoader.ts b/packages/cli/src/services/McpPromptLoader.ts index 0b9fa0582e..34c1647eac 100644 --- a/packages/cli/src/services/McpPromptLoader.ts +++ b/packages/cli/src/services/McpPromptLoader.ts @@ -5,18 +5,18 @@ */ import { - Config, + type Config, getErrorMessage, getMCPServerPrompts, } from '@vybestack/llxprt-code-core'; import { - CommandContext, + type CommandContext, CommandKind, - SlashCommand, - SlashCommandActionReturn, + type SlashCommand, + type SlashCommandActionReturn, } from '../ui/commands/types.js'; -import { ICommandLoader } from './types.js'; -import { +import type { ICommandLoader } from './types.js'; +import type { PromptArgument, PromptMessage, } from '@modelcontextprotocol/sdk/types.js'; @@ -37,7 +37,7 @@ export class McpPromptLoader implements ICommandLoader { */ loadCommands(_signal: AbortSignal): Promise { const promptCommands: SlashCommand[] = []; - if (!this.config) { + if (this.config == null) { return Promise.resolve([]); } const mcpServers = this.config.getMcpServers() || {}; @@ -57,7 +57,7 @@ export class McpPromptLoader implements ICommandLoader { description: 'Show help for this prompt', kind: CommandKind.MCP_PROMPT, action: async (): Promise => { - if (!prompt.arguments || prompt.arguments.length === 0) { + if (prompt.arguments == null || prompt.arguments.length === 0) { return { type: 'message', messageType: 'info', @@ -88,10 +88,10 @@ export class McpPromptLoader implements ICommandLoader { }, ], action: async ( - context: CommandContext, + _context: CommandContext, args: string, ): Promise => { - if (!this.config) { + if (this.config == null) { return { type: 'message', messageType: 'error', @@ -202,7 +202,7 @@ export class McpPromptLoader implements ICommandLoader { messages?: PromptMessage[], ): string | null { const firstContent = messages?.[0]?.content; - if (firstContent && firstContent.type === 'text') { + if (firstContent != null && firstContent.type === 'text') { return firstContent.text; } return null; @@ -256,7 +256,7 @@ export class McpPromptLoader implements ICommandLoader { positionalArgs.push((match[1] ?? match[2]).replace(/\\(.)/g, '$1')); } - if (!promptArgs) { + if (promptArgs == null) { return promptInputs; } for (const arg of promptArgs) { diff --git a/packages/cli/src/services/__tests__/performResume.spec.ts b/packages/cli/src/services/__tests__/performResume.spec.ts index b742a85556..7e497e6fea 100644 --- a/packages/cli/src/services/__tests__/performResume.spec.ts +++ b/packages/cli/src/services/__tests__/performResume.spec.ts @@ -25,7 +25,7 @@ import * as os from 'node:os'; import { SessionRecordingService, SessionLockManager, - RecordingIntegration, + type RecordingIntegration, type LockHandle, type SessionRecordingServiceConfig, type SessionRecordLine, @@ -50,10 +50,10 @@ function makeConfig( return { sessionId: overrides.sessionId ?? crypto.randomUUID(), projectHash: overrides.projectHash ?? PROJECT_HASH, - chatsDir, workspaceDirs: overrides.workspaceDirs ?? ['/test/workspace'], provider: overrides.provider ?? 'anthropic', model: overrides.model ?? 'claude-4', + chatsDir, }; } @@ -204,8 +204,8 @@ async function countFileEvents(filePath: string): Promise { */ function extractSessionId(filePath: string): string { const basename = path.basename(filePath); - const match = basename.match(/^session-(.+)\.jsonl$/); - if (!match) throw new Error(`Invalid session file path: ${filePath}`); + const match = RegExp(/^session-(.+)\.jsonl$/).exec(basename); + if (match == null) throw new Error(`Invalid session file path: ${filePath}`); return match[1]; } @@ -286,7 +286,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Track new lock for cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -324,7 +324,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Track new lock for cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -360,7 +360,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Track new lock for cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -389,7 +389,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Track new lock for cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); }); @@ -730,7 +730,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Track new lock for cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -757,7 +757,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Track new lock for cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -797,7 +797,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Track new lock for cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -844,7 +844,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Track new lock for cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); }); @@ -966,7 +966,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); }); @@ -1014,7 +1014,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -1043,7 +1043,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -1072,7 +1072,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -1103,7 +1103,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -1132,7 +1132,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -1177,7 +1177,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) lockHandles.push(newLock); + if (newLock != null) lockHandles.push(newLock); }); /** @@ -1275,7 +1275,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) await newLock.release(); + if (newLock != null) await newLock.release(); } finally { await fs.rm(localTempDir, { recursive: true, force: true }); } @@ -1379,7 +1379,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup lock const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) await newLock.release(); + if (newLock != null) await newLock.release(); } } finally { await fs.rm(localTempDir, { recursive: true, force: true }); @@ -1486,7 +1486,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup lock const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) await newLock.release(); + if (newLock != null) await newLock.release(); } } finally { await fs.rm(localTempDir, { recursive: true, force: true }); @@ -1537,7 +1537,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { if (result.ok) { const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) await newLock.release(); + if (newLock != null) await newLock.release(); } } finally { await fs.rm(localTempDir, { recursive: true, force: true }); @@ -1585,7 +1585,7 @@ describe('performResume @plan:PLAN-20260214-SESSIONBROWSER.P10', () => { // Cleanup lock const newLock = context.recordingCallbacks.getCurrentLockHandle(); - if (newLock) await newLock.release(); + if (newLock != null) await newLock.release(); } } finally { await fs.rm(localTempDir, { recursive: true, force: true }); diff --git a/packages/cli/src/services/performResume.ts b/packages/cli/src/services/performResume.ts index 5089951e85..5266cbfff3 100644 --- a/packages/cli/src/services/performResume.ts +++ b/packages/cli/src/services/performResume.ts @@ -120,7 +120,7 @@ export async function performResume( targetSession = session; break; } - if (!targetSession) { + if (targetSession == null) { return { ok: false, error: 'No resumable sessions found (all locked, empty, or current).', @@ -151,11 +151,11 @@ export async function performResume( // 5. Phase 1: Acquire new session (before disposing old) const resumeResult = await resumeSession({ continueRef: targetSession.sessionId, - projectHash, - chatsDir, currentProvider: context.currentProvider, currentModel: context.currentModel, workspaceDirs: context.workspaceDirs, + projectHash, + chatsDir, }); if (resumeResult.ok === false) { @@ -169,7 +169,7 @@ export async function performResume( // Dispose in order: integration -> recording -> lock. // Failures are warnings so newly acquired infrastructure can still be installed. - if (oldIntegration) { + if (oldIntegration != null) { try { oldIntegration.dispose(); } catch (e) { @@ -178,7 +178,7 @@ export async function performResume( ); } } - if (oldRecording) { + if (oldRecording != null) { try { await oldRecording.dispose(); } catch (e) { @@ -187,7 +187,7 @@ export async function performResume( ); } } - if (oldLock) { + if (oldLock != null) { try { await oldLock.release(); } catch (e) { @@ -213,6 +213,6 @@ export async function performResume( ok: true, history: resumeResult.history, metadata: resumeResult.metadata, - warnings: resumeResult.warnings ?? [], + warnings: resumeResult.warnings, }; } diff --git a/packages/cli/src/services/prompt-processors/argumentProcessor.ts b/packages/cli/src/services/prompt-processors/argumentProcessor.ts index 18e67216e9..aaa6f9c872 100644 --- a/packages/cli/src/services/prompt-processors/argumentProcessor.ts +++ b/packages/cli/src/services/prompt-processors/argumentProcessor.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { IPromptProcessor } from './types.js'; -import { CommandContext } from '../../ui/commands/types.js'; +import type { IPromptProcessor } from './types.js'; +import type { CommandContext } from '../../ui/commands/types.js'; /** * Appends the user's full command invocation to the prompt if arguments are diff --git a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts index 95a487dbf2..5dd51f5afd 100644 --- a/packages/cli/src/services/prompt-processors/shellProcessor.test.ts +++ b/packages/cli/src/services/prompt-processors/shellProcessor.test.ts @@ -7,10 +7,10 @@ import { describe, it, expect, beforeEach, vi, type Mock } from 'vitest'; import { ConfirmationRequiredError, ShellProcessor } from './shellProcessor.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; -import { CommandContext } from '../../ui/commands/types.js'; +import type { CommandContext } from '../../ui/commands/types.js'; import { ApprovalMode, - Config, + type Config, escapeShellArg, getShellConfiguration, } from '@vybestack/llxprt-code-core'; @@ -237,7 +237,7 @@ describe('ShellProcessor', () => { const promise = processor.process(prompt, context); await expect(promise).rejects.toBeInstanceOf(ConfirmationRequiredError); const error = await promise.catch((e) => e); - expect(error.commandsToConfirm).toEqual(['rm -rf /']); + expect(error.commandsToConfirm).toStrictEqual(['rm -rf /']); expect(mockShellExecute).not.toHaveBeenCalled(); }); @@ -258,7 +258,7 @@ describe('ShellProcessor', () => { const promise = processor.process(prompt, context); await expect(promise).rejects.toBeInstanceOf(ConfirmationRequiredError); const error = await promise.catch((e) => e); - expect(error.commandsToConfirm).toEqual(['cmd1', 'cmd2']); + expect(error.commandsToConfirm).toStrictEqual(['cmd1', 'cmd2']); }); it('should not execute any commands if at least one requires confirmation', async () => { @@ -292,7 +292,7 @@ describe('ShellProcessor', () => { const promise = processor.process(prompt, context); await expect(promise).rejects.toBeInstanceOf(ConfirmationRequiredError); const error = await promise.catch((e) => e); - expect(error.commandsToConfirm).toEqual(['rm -rf /']); + expect(error.commandsToConfirm).toStrictEqual(['rm -rf /']); }); it('should execute all commands if they are on the session allowlist', async () => { diff --git a/packages/cli/src/services/prompt-processors/shellProcessor.ts b/packages/cli/src/services/prompt-processors/shellProcessor.ts index 02c2f992c8..1f617fc4db 100644 --- a/packages/cli/src/services/prompt-processors/shellProcessor.ts +++ b/packages/cli/src/services/prompt-processors/shellProcessor.ts @@ -12,9 +12,9 @@ import { ShellExecutionService, } from '@vybestack/llxprt-code-core'; -import { CommandContext } from '../../ui/commands/types.js'; +import type { CommandContext } from '../../ui/commands/types.js'; import { - IPromptProcessor, + type IPromptProcessor, SHELL_INJECTION_TRIGGER, SHORTHAND_ARGS_PLACEHOLDER, } from './types.js'; @@ -64,7 +64,7 @@ export class ShellProcessor implements IPromptProcessor { } const config = context.services.config; - if (!config) { + if (config == null) { throw new Error( `Security configuration not loaded. Cannot verify shell command permissions for '${this.commandName}'. Aborting.`, ); @@ -149,7 +149,7 @@ export class ShellProcessor implements IPromptProcessor { const executionResult = await result; // Handle Spawn Errors - if (executionResult.error && !executionResult.aborted) { + if (executionResult.error != null && !executionResult.aborted) { throw new Error( `Failed to start shell command in '${this.commandName}': ${executionResult.error.message}. Command: ${injection.resolvedCommand}`, ); diff --git a/packages/cli/src/services/prompt-processors/types.ts b/packages/cli/src/services/prompt-processors/types.ts index ffdcfa2309..b30834a731 100644 --- a/packages/cli/src/services/prompt-processors/types.ts +++ b/packages/cli/src/services/prompt-processors/types.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CommandContext } from '../../ui/commands/types.js'; +import type { CommandContext } from '../../ui/commands/types.js'; /** * Defines the interface for a prompt processor, a module that can transform diff --git a/packages/cli/src/services/todo-continuation/todoContinuationService.spec.ts b/packages/cli/src/services/todo-continuation/todoContinuationService.spec.ts index 6c25c11aa5..917b909953 100644 --- a/packages/cli/src/services/todo-continuation/todoContinuationService.spec.ts +++ b/packages/cli/src/services/todo-continuation/todoContinuationService.spec.ts @@ -5,8 +5,7 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; -import { Config } from '@vybestack/llxprt-code-core'; -import type { Todo } from '@vybestack/llxprt-code-core'; +import type { Config, Todo } from '@vybestack/llxprt-code-core'; import { TodoContinuationService, type ContinuationPromptConfig, diff --git a/packages/cli/src/services/todo-continuation/todoContinuationService.ts b/packages/cli/src/services/todo-continuation/todoContinuationService.ts index 53d0fc3731..572a4a7434 100644 --- a/packages/cli/src/services/todo-continuation/todoContinuationService.ts +++ b/packages/cli/src/services/todo-continuation/todoContinuationService.ts @@ -4,8 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Config } from '@vybestack/llxprt-code-core'; -import type { Todo } from '@vybestack/llxprt-code-core'; +import type { Config, Todo } from '@vybestack/llxprt-code-core'; /** * Test mock config interface for unit tests @@ -111,6 +110,7 @@ export class TodoContinuationService { * @returns Formatted continuation prompt string */ generateContinuationPrompt(config: ContinuationPromptConfig): string { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions -- runtime guard if (!config) { throw new Error('Configuration is required'); } @@ -125,13 +125,12 @@ export class TodoContinuationService { config.attemptCount, config.previousFailure, ); - } else { - return this.generateStandardPrompt( - taskDescription, - config.attemptCount, - config.previousFailure, - ); } + return this.generateStandardPrompt( + taskDescription, + config.attemptCount, + config.previousFailure, + ); } /** @@ -142,6 +141,7 @@ export class TodoContinuationService { checkContinuationConditions( context: ContinuationContext, ): ContinuationEvaluation { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions -- runtime guard if (!context) { throw new Error('Context is required'); } @@ -207,7 +207,7 @@ export class TodoContinuationService { // Find the active todo to continue const activeTodo = this.findBestActiveTodo(context.todos); - if (!activeTodo) { + if (activeTodo == null) { return { shouldContinue: false, reason: 'No suitable active todo found', @@ -230,7 +230,7 @@ export class TodoContinuationService { * @returns Formatted task description string */ formatTaskDescription(todo: Todo): string { - if (!todo || !todo.content) { + if (!todo?.content) { return 'Complete task'; } @@ -285,7 +285,7 @@ export class TodoContinuationService { } // Check time constraints - if (state.lastPromptTime) { + if (state.lastPromptTime != null) { const timeSinceLastPrompt = Date.now() - state.lastPromptTime.getTime(); if ( timeSinceLastPrompt < @@ -357,9 +357,8 @@ export class TodoContinuationService { if (isYoloMode) { return this.generateYoloModePrompt(truncatedDescription); - } else { - return this.generateStandardPrompt(truncatedDescription); } + return this.generateStandardPrompt(truncatedDescription); } /** @@ -391,15 +390,14 @@ export class TodoContinuationService { ) { // Good word boundary found return truncated.substring(0, lastSpaceIndex) + '...'; - } else { - // No good word boundary, hard truncate - return ( - truncated.substring( - 0, - TodoContinuationService.MAX_TASK_DESCRIPTION_LENGTH - 3, - ) + '...' - ); } + // No good word boundary, hard truncate + return ( + truncated.substring( + 0, + TodoContinuationService.MAX_TASK_DESCRIPTION_LENGTH - 3, + ) + '...' + ); } /** @@ -559,7 +557,7 @@ export class TodoContinuationService { * @returns Whether time constraints are satisfied */ private checkTimeConstraints(lastPromptTime?: Date): boolean { - if (!lastPromptTime) { + if (lastPromptTime == null) { return true; // No previous attempt, allowed } diff --git a/packages/cli/src/services/types.ts b/packages/cli/src/services/types.ts index 3e235bd1f6..3a74f40fbd 100644 --- a/packages/cli/src/services/types.ts +++ b/packages/cli/src/services/types.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SlashCommand } from '../ui/commands/types.js'; +import type { SlashCommand } from '../ui/commands/types.js'; /** * Defines the contract for any class that can load and provide slash commands. diff --git a/packages/cli/src/settings/ephemeralSettings.reasoningSummary.test.ts b/packages/cli/src/settings/ephemeralSettings.reasoningSummary.test.ts index c44a03b426..6d68469ace 100644 --- a/packages/cli/src/settings/ephemeralSettings.reasoningSummary.test.ts +++ b/packages/cli/src/settings/ephemeralSettings.reasoningSummary.test.ts @@ -21,7 +21,7 @@ describe('reasoning.summary ephemeral setting @issue:922', () => { it('should mention OpenAI in the description', () => { const description = ephemeralSettingHelp['reasoning.summary']; - expect(description?.toLowerCase()).toContain('openai'); + expect(description.toLowerCase()).toContain('openai'); }); it('should mention valid values in the description', () => { diff --git a/packages/cli/src/storage/ConversationStorage.test.ts b/packages/cli/src/storage/ConversationStorage.test.ts index fcf9fe12c7..70d261e443 100644 --- a/packages/cli/src/storage/ConversationStorage.test.ts +++ b/packages/cli/src/storage/ConversationStorage.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as path from 'path'; -import { IContent } from '@vybestack/llxprt-code-core'; +import type { IContent } from '@vybestack/llxprt-code-core'; // Interface for conversation log entries interface ConversationLogEntry { @@ -105,7 +105,7 @@ class MockConversationStorage implements ConversationStorage { // Archive current log const currentFile = this.getCurrentLogPath(); const fileInfo = this.logFiles.get(currentFile); - if (fileInfo) { + if (fileInfo != null) { this.logFiles.set(newLogFile, { ...fileInfo }); } diff --git a/packages/cli/src/test-utils/createExtension.ts b/packages/cli/src/test-utils/createExtension.ts index 33550fd8a5..2447239868 100644 --- a/packages/cli/src/test-utils/createExtension.ts +++ b/packages/cli/src/test-utils/createExtension.ts @@ -10,9 +10,9 @@ import { EXTENSIONS_CONFIG_FILENAME, INSTALL_METADATA_FILENAME, } from '../config/extension.js'; -import { - type MCPServerConfig, - type ExtensionInstallMetadata, +import type { + MCPServerConfig, + ExtensionInstallMetadata, } from '@vybestack/llxprt-code-core'; export function createExtension({ @@ -39,7 +39,7 @@ export function createExtension({ fs.writeFileSync(path.join(extDir, contextFileName), 'context'); } - if (installMetadata) { + if (installMetadata != null) { fs.writeFileSync( path.join(extDir, INSTALL_METADATA_FILENAME), JSON.stringify(installMetadata), diff --git a/packages/cli/src/test-utils/customMatchers.ts b/packages/cli/src/test-utils/customMatchers.ts index ba9de6f50b..724067ac45 100644 --- a/packages/cli/src/test-utils/customMatchers.ts +++ b/packages/cli/src/test-utils/customMatchers.ts @@ -6,7 +6,7 @@ /// -import { Assertion, expect } from 'vitest'; +import { type Assertion, expect } from 'vitest'; import type { TextBuffer } from '../ui/components/shared/text-buffer.js'; // RegExp to detect invalid characters: backspace, and ANSI escape codes diff --git a/packages/cli/src/test-utils/mockCommandContext.ts b/packages/cli/src/test-utils/mockCommandContext.ts index 3169207d80..2c6f022928 100644 --- a/packages/cli/src/test-utils/mockCommandContext.ts +++ b/packages/cli/src/test-utils/mockCommandContext.ts @@ -5,10 +5,10 @@ */ import { vi } from 'vitest'; -import { CommandContext } from '../ui/commands/types.js'; -import { LoadedSettings } from '../config/settings.js'; -import { GitService, Config } from '@vybestack/llxprt-code-core'; -import { SessionStatsState } from '../ui/contexts/SessionContext.js'; +import type { CommandContext } from '../ui/commands/types.js'; +import type { LoadedSettings } from '../config/settings.js'; +import type { GitService, Config } from '@vybestack/llxprt-code-core'; +import type { SessionStatsState } from '../ui/contexts/SessionContext.js'; // A utility type to make all properties of an object, and its nested objects, partial. type DeepPartial = T extends object diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx index bdceeb9342..74fe976bd0 100644 --- a/packages/cli/src/test-utils/render.tsx +++ b/packages/cli/src/test-utils/render.tsx @@ -149,7 +149,7 @@ const MockRuntimeContextProvider: React.FC = ({ // Export mock hooks that tests can use export const useMockRuntimeApi = (): MockRuntimeApi => { const context = useContext(MockRuntimeContext); - if (!context) { + if (context == null) { throw new Error('MockRuntimeContext not found'); } return context.api; diff --git a/packages/cli/src/test-utils/responsive-testing.test.tsx b/packages/cli/src/test-utils/responsive-testing.test.tsx index dd58a1a610..272e2bdc1f 100644 --- a/packages/cli/src/test-utils/responsive-testing.test.tsx +++ b/packages/cli/src/test-utils/responsive-testing.test.tsx @@ -5,8 +5,8 @@ */ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render } from 'ink-testing-library'; -import React from 'react'; +import type { render } from 'ink-testing-library'; +import type React from 'react'; import { renderAtWidth, testResponsiveBehavior } from './responsive-testing.js'; import { useResponsive } from '../ui/hooks/useResponsive.js'; diff --git a/packages/cli/src/test-utils/responsive-testing.tsx b/packages/cli/src/test-utils/responsive-testing.tsx index 37c3e895c4..3150a3af19 100644 --- a/packages/cli/src/test-utils/responsive-testing.tsx +++ b/packages/cli/src/test-utils/responsive-testing.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { render } from 'ink-testing-library'; import { vi } from 'vitest'; import { useTerminalSize } from '../ui/hooks/useTerminalSize.js'; @@ -29,26 +29,26 @@ export function renderAtWidth( } export function testResponsiveBehavior( - name: string, + _name: string, component: React.ReactElement, assertions: ResponsiveAssertions, ): void { // Test narrow behavior if assertion is provided - if (assertions.narrow) { + if (assertions.narrow != null) { const narrowResult = renderAtWidth(component, 60); assertions.narrow(narrowResult); narrowResult.unmount(); } // Test standard behavior if assertion is provided - if (assertions.standard) { + if (assertions.standard != null) { const standardResult = renderAtWidth(component, 100); assertions.standard(standardResult); standardResult.unmount(); } // Test wide behavior if assertion is provided - if (assertions.wide) { + if (assertions.wide != null) { const wideResult = renderAtWidth(component, 180); assertions.wide(wideResult); wideResult.unmount(); diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index 167400b8b7..ed5febe229 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -9,29 +9,33 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { Text } from 'ink'; import { renderWithProviders } from '../test-utils/render.js'; import { AppWrapper as App } from './App.js'; -import { - Config as ServerConfig, +import type { MCPServerConfig, + Config as ServerConfig, ApprovalMode, - ToolRegistry, - AccessibilitySettings, - SandboxConfig, - LLxprtClient, + type ToolRegistry, + type AccessibilitySettings, + type SandboxConfig, + type LLxprtClient, ideContext, DEFAULT_AGENT_ID, } from '@vybestack/llxprt-code-core'; -import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js'; +import { + LoadedSettings, + type SettingsFile, + type Settings, +} from '../config/settings.js'; import process from 'node:process'; import { useGeminiStream } from './hooks/geminiStream/index.js'; import { useConsoleMessages } from './hooks/useConsoleMessages.js'; import { StreamingState, - ConsoleMessageItem, + type ConsoleMessageItem, MessageType, - HistoryItem, + type HistoryItem, } from './types.js'; import { Tips } from './components/Tips.js'; -import { checkForUpdates, UpdateObject } from './utils/updateCheck.js'; +import { checkForUpdates, type UpdateObject } from './utils/updateCheck.js'; import { EventEmitter } from 'events'; import { updateEventEmitter } from '../utils/updateEventEmitter.js'; import * as useTerminalSize from './hooks/useTerminalSize.js'; @@ -425,7 +429,7 @@ describe('App UI', () => { }); afterEach(() => { - if (currentUnmount) { + if (currentUnmount != null) { currentUnmount(); currentUnmount = undefined; } diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 5727c04932..3cc8d24590 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -19,7 +19,7 @@ * under the original `AppContainer` name used by App.tsx and the public API. */ -import React from 'react'; +import type React from 'react'; import type { Config, MessageBus, diff --git a/packages/cli/src/ui/AppContainerRuntime.tsx b/packages/cli/src/ui/AppContainerRuntime.tsx index 010e011aef..e0f8cf27e6 100644 --- a/packages/cli/src/ui/AppContainerRuntime.tsx +++ b/packages/cli/src/ui/AppContainerRuntime.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import type { Config, MessageBus, diff --git a/packages/cli/src/ui/IdeIntegrationNudge.tsx b/packages/cli/src/ui/IdeIntegrationNudge.tsx index 885fb5f1a4..92f7009643 100644 --- a/packages/cli/src/ui/IdeIntegrationNudge.tsx +++ b/packages/cli/src/ui/IdeIntegrationNudge.tsx @@ -8,7 +8,7 @@ import type { IdeInfo } from '@vybestack/llxprt-code-core'; import { Box, Text } from 'ink'; import { RadioButtonSelect, - RadioSelectItem, + type RadioSelectItem, } from './components/shared/RadioButtonSelect.js'; import { useKeypress } from './hooks/useKeypress.js'; import { Colors } from './colors.js'; diff --git a/packages/cli/src/ui/__tests__/AppContainer.mount.test.tsx b/packages/cli/src/ui/__tests__/AppContainer.mount.test.tsx index b4d3360e19..59d0495339 100644 --- a/packages/cli/src/ui/__tests__/AppContainer.mount.test.tsx +++ b/packages/cli/src/ui/__tests__/AppContainer.mount.test.tsx @@ -544,9 +544,9 @@ describe('AppContainer.mount', () => { config: mockConfig as unknown as Config, settings: mockSettings, version: '1.0.0-test', - resumedHistory, appState: initialAppState, appDispatch: vi.fn(), + resumedHistory, }; // Act & Assert: Should not throw when rendering with resumed history @@ -564,9 +564,9 @@ describe('AppContainer.mount', () => { config: mockConfig as unknown as Config, settings: mockSettings, version: '1.0.0-test', - startupWarnings, appState: initialAppState, appDispatch: vi.fn(), + startupWarnings, }; // Act & Assert: Should not throw when rendering with startup warnings diff --git a/packages/cli/src/ui/colors.ts b/packages/cli/src/ui/colors.ts index d22dff6c37..e04fc83951 100644 --- a/packages/cli/src/ui/colors.ts +++ b/packages/cli/src/ui/colors.ts @@ -5,8 +5,8 @@ */ import { themeManager } from './themes/theme-manager.js'; -import { ColorsTheme } from './themes/theme.js'; -import { SemanticColors as SemanticColorsInterface } from './themes/semantic-tokens.js'; +import type { ColorsTheme } from './themes/theme.js'; +import type { SemanticColors as SemanticColorsInterface } from './themes/semantic-tokens.js'; import chalk from 'chalk'; export const Colors: ColorsTheme = { diff --git a/packages/cli/src/ui/commands/__tests__/continueCommand.spec.ts b/packages/cli/src/ui/commands/__tests__/continueCommand.spec.ts index 019353b4a0..d9c7ce221e 100644 --- a/packages/cli/src/ui/commands/__tests__/continueCommand.spec.ts +++ b/packages/cli/src/ui/commands/__tests__/continueCommand.spec.ts @@ -272,9 +272,9 @@ describe('continueCommand @plan:PLAN-20260214-SESSIONBROWSER.P19', () => { const schema = continueCommand.schema; expect(schema).toBeDefined(); - if (schema && schema.length > 0) { + if (schema != null && schema.length > 0) { const firstArg = schema[0]; - if (firstArg.kind === 'value' && firstArg.completer) { + if (firstArg.kind === 'value' && firstArg.completer != null) { const completions = await firstArg.completer( ctx, '', @@ -303,9 +303,9 @@ describe('continueCommand @plan:PLAN-20260214-SESSIONBROWSER.P19', () => { expect(schema).toBeDefined(); // The completer should be able to return session-related completions - if (schema && schema.length > 0) { + if (schema != null && schema.length > 0) { const firstArg = schema[0]; - if (firstArg.kind === 'value' && firstArg.completer) { + if (firstArg.kind === 'value' && firstArg.completer != null) { const completions = await firstArg.completer( ctx, '', @@ -328,9 +328,9 @@ describe('continueCommand @plan:PLAN-20260214-SESSIONBROWSER.P19', () => { // When non-interactive, completion may return empty or limited results const schema = continueCommand.schema; - if (schema && schema.length > 0) { + if (schema != null && schema.length > 0) { const firstArg = schema[0]; - if (firstArg.kind === 'value' && firstArg.completer) { + if (firstArg.kind === 'value' && firstArg.completer != null) { const completions = await firstArg.completer( ctx, '', diff --git a/packages/cli/src/ui/commands/__tests__/profileCommand.failover.test.ts b/packages/cli/src/ui/commands/__tests__/profileCommand.failover.test.ts index cc0b0613c1..9466182e67 100644 --- a/packages/cli/src/ui/commands/__tests__/profileCommand.failover.test.ts +++ b/packages/cli/src/ui/commands/__tests__/profileCommand.failover.test.ts @@ -189,7 +189,7 @@ describe('profileCommand - failover policy parsing', () => { const savedProfile = runtimeMocks.saveLoadBalancerProfile.mock .calls[0][1] as LoadBalancerProfile; - expect(savedProfile).toEqual({ + expect(savedProfile).toStrictEqual({ version: 1, type: 'loadbalancer', policy: 'failover', @@ -214,7 +214,7 @@ describe('profileCommand - failover policy parsing', () => { const savedProfile = runtimeMocks.saveLoadBalancerProfile.mock .calls[0][1] as LoadBalancerProfile; - expect(savedProfile).toEqual({ + expect(savedProfile).toStrictEqual({ version: 1, type: 'loadbalancer', policy: 'roundrobin', @@ -239,7 +239,7 @@ describe('profileCommand - failover policy parsing', () => { const savedProfile = runtimeMocks.saveLoadBalancerProfile.mock .calls[0][1] as LoadBalancerProfile; - expect(savedProfile.profiles).toEqual([ + expect(savedProfile.profiles).toStrictEqual([ 'profile1', 'profile2', 'profile3', diff --git a/packages/cli/src/ui/commands/__tests__/profileCommand.lb.test.ts b/packages/cli/src/ui/commands/__tests__/profileCommand.lb.test.ts index dd0a318bf6..733519e093 100644 --- a/packages/cli/src/ui/commands/__tests__/profileCommand.lb.test.ts +++ b/packages/cli/src/ui/commands/__tests__/profileCommand.lb.test.ts @@ -148,7 +148,7 @@ describe('profileCommand - load balancer save with protected settings', () => { const savedProfile = runtimeMocks.saveLoadBalancerProfile.mock .calls[0][1] as LoadBalancerProfile; - expect(savedProfile.ephemeralSettings).toEqual({}); + expect(savedProfile.ephemeralSettings).toStrictEqual({}); }); it('handles ephemeral settings with only protected values', async () => { @@ -167,7 +167,7 @@ describe('profileCommand - load balancer save with protected settings', () => { const savedProfile = runtimeMocks.saveLoadBalancerProfile.mock .calls[0][1] as LoadBalancerProfile; // All settings were protected, so ephemeralSettings should be empty - expect(savedProfile.ephemeralSettings).toEqual({}); + expect(savedProfile.ephemeralSettings).toStrictEqual({}); }); it('preserves multiple profile names correctly', async () => { @@ -183,7 +183,7 @@ describe('profileCommand - load balancer save with protected settings', () => { const savedProfile = runtimeMocks.saveLoadBalancerProfile.mock .calls[0][1] as LoadBalancerProfile; - expect(savedProfile.profiles).toEqual([ + expect(savedProfile.profiles).toStrictEqual([ 'profile1', 'profile2', 'profile3', @@ -216,7 +216,7 @@ describe('profileCommand - load balancer save with protected settings', () => { .calls[0][1] as LoadBalancerProfile; // Only streaming should remain (provider should be stripped) - expect(savedProfile.ephemeralSettings).toEqual({ + expect(savedProfile.ephemeralSettings).toStrictEqual({ streaming: false, }); }); diff --git a/packages/cli/src/ui/commands/__tests__/setCommand.lb.test.ts b/packages/cli/src/ui/commands/__tests__/setCommand.lb.test.ts index 3e52ff344f..ac7bd2d058 100644 --- a/packages/cli/src/ui/commands/__tests__/setCommand.lb.test.ts +++ b/packages/cli/src/ui/commands/__tests__/setCommand.lb.test.ts @@ -40,7 +40,7 @@ describe('setCommand - load balancer settings', () => { 'tpm_threshold', 1000, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -52,7 +52,7 @@ describe('setCommand - load balancer settings', () => { const result = await setCommand.action!(context, 'tpm_threshold 0'); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'tpm_threshold must be a positive integer', @@ -63,7 +63,7 @@ describe('setCommand - load balancer settings', () => { const result = await setCommand.action!(context, 'tpm_threshold -100'); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'tpm_threshold must be a positive integer', @@ -74,7 +74,7 @@ describe('setCommand - load balancer settings', () => { const result = await setCommand.action!(context, 'tpm_threshold 100.5'); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'tpm_threshold must be a positive integer', @@ -85,7 +85,7 @@ describe('setCommand - load balancer settings', () => { const result = await setCommand.action!(context, 'tpm_threshold'); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -102,7 +102,7 @@ describe('setCommand - load balancer settings', () => { 'timeout_ms', 30000, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -114,7 +114,7 @@ describe('setCommand - load balancer settings', () => { const result = await setCommand.action!(context, 'timeout_ms 0'); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'timeout_ms must be a positive integer', @@ -125,7 +125,7 @@ describe('setCommand - load balancer settings', () => { const result = await setCommand.action!(context, 'timeout_ms -5000'); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'timeout_ms must be a positive integer', @@ -136,7 +136,7 @@ describe('setCommand - load balancer settings', () => { const result = await setCommand.action!(context, 'timeout_ms 30000.5'); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'timeout_ms must be a positive integer', @@ -147,7 +147,7 @@ describe('setCommand - load balancer settings', () => { const result = await setCommand.action!(context, 'timeout_ms'); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -167,7 +167,7 @@ describe('setCommand - load balancer settings', () => { 'circuit_breaker_enabled', true, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -185,7 +185,7 @@ describe('setCommand - load balancer settings', () => { 'circuit_breaker_enabled', false, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -200,7 +200,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: "circuit_breaker_enabled must be either 'true' or 'false'", @@ -214,7 +214,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -234,7 +234,7 @@ describe('setCommand - load balancer settings', () => { 'circuit_breaker_failure_threshold', 3, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -249,7 +249,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'circuit_breaker_failure_threshold must be a positive integer', @@ -263,7 +263,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'circuit_breaker_failure_threshold must be a positive integer', @@ -277,7 +277,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'circuit_breaker_failure_threshold must be a positive integer', @@ -291,7 +291,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -311,7 +311,7 @@ describe('setCommand - load balancer settings', () => { 'circuit_breaker_failure_window_ms', 60000, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -326,7 +326,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'circuit_breaker_failure_window_ms must be a positive integer', @@ -340,7 +340,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'circuit_breaker_failure_window_ms must be a positive integer', @@ -354,7 +354,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'circuit_breaker_failure_window_ms must be a positive integer', @@ -368,7 +368,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -388,7 +388,7 @@ describe('setCommand - load balancer settings', () => { 'circuit_breaker_recovery_timeout_ms', 30000, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -403,7 +403,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -418,7 +418,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -433,7 +433,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -448,7 +448,7 @@ describe('setCommand - load balancer settings', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -465,7 +465,7 @@ describe('setCommand - load balancer settings', () => { 'tpm_threshold', undefined, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: "Ephemeral setting 'tpm_threshold' cleared", @@ -479,7 +479,7 @@ describe('setCommand - load balancer settings', () => { 'timeout_ms', undefined, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: "Ephemeral setting 'timeout_ms' cleared", @@ -496,7 +496,7 @@ describe('setCommand - load balancer settings', () => { 'circuit_breaker_enabled', undefined, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: "Ephemeral setting 'circuit_breaker_enabled' cleared", @@ -513,7 +513,7 @@ describe('setCommand - load balancer settings', () => { 'circuit_breaker_failure_threshold', undefined, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -531,7 +531,7 @@ describe('setCommand - load balancer settings', () => { 'circuit_breaker_failure_window_ms', undefined, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -549,7 +549,7 @@ describe('setCommand - load balancer settings', () => { 'circuit_breaker_recovery_timeout_ms', undefined, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: diff --git a/packages/cli/src/ui/commands/__tests__/statsCommand.lb.test.ts b/packages/cli/src/ui/commands/__tests__/statsCommand.lb.test.ts index cd7138a60a..e15d53466f 100644 --- a/packages/cli/src/ui/commands/__tests__/statsCommand.lb.test.ts +++ b/packages/cli/src/ui/commands/__tests__/statsCommand.lb.test.ts @@ -9,7 +9,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { statsCommand } from '../statsCommand.js'; -import { type CommandContext } from '../types.js'; +import type { CommandContext } from '../types.js'; import { createMockCommandContext } from '../../../test-utils/mockCommandContext.js'; import { MessageType } from '../../types.js'; @@ -25,7 +25,8 @@ describe('statsCommand - load balancer stats', () => { const lbSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'lb', ); - if (!lbSubCommand?.action) throw new Error('lb subcommand has no action'); + if (lbSubCommand?.action == null) + throw new Error('lb subcommand has no action'); // eslint-disable-next-line @typescript-eslint/no-floating-promises lbSubCommand.action(mockContext, ''); @@ -67,7 +68,7 @@ describe('statsCommand - load balancer stats', () => { expect(lbSubCommand).toBeDefined(); expect(lbSubCommand?.name).toBe('lb'); - expect(lbSubCommand?.altNames).toEqual(['loadbalancer']); + expect(lbSubCommand?.altNames).toStrictEqual(['loadbalancer']); expect(lbSubCommand?.description).toContain('load balancer'); }); }); @@ -80,7 +81,7 @@ describe('statsCommand - load balancer stats', () => { expect(lbSubCommand).toBeDefined(); expect(lbSubCommand?.name).toBe('lb'); - expect(lbSubCommand?.altNames).toEqual(['loadbalancer']); + expect(lbSubCommand?.altNames).toStrictEqual(['loadbalancer']); }); it('should use MessageType.LB_STATS when invoked', () => { @@ -88,7 +89,7 @@ describe('statsCommand - load balancer stats', () => { (sc) => sc.name === 'lb', ); - if (!lbSubCommand?.action) { + if (lbSubCommand?.action == null) { throw new Error('lb subcommand has no action'); } diff --git a/packages/cli/src/ui/commands/aboutCommand.test.ts b/packages/cli/src/ui/commands/aboutCommand.test.ts index fe77a31790..77f8983d56 100644 --- a/packages/cli/src/ui/commands/aboutCommand.test.ts +++ b/packages/cli/src/ui/commands/aboutCommand.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { aboutCommand } from './aboutCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import * as versionUtils from '../../utils/version.js'; import { MessageType } from '../types.js'; @@ -17,7 +17,7 @@ import { } from '../../providers/providerManagerInstance.js'; import { USER_SETTINGS_PATH } from '../../config/settings.js'; -import { IdeClient } from '../../../../core/src/ide/ide-client.js'; +import type { IdeClient } from '../../../../core/src/ide/ide-client.js'; const runtimeMocks = vi.hoisted(() => ({ getRuntimeApiMock: vi.fn(), @@ -106,7 +106,7 @@ describe('aboutCommand', () => { it('should call addItem with all version info', async () => { process.env.SANDBOX = ''; - if (!aboutCommand.action) { + if (aboutCommand.action == null) { throw new Error('The about command must have an action.'); } @@ -129,7 +129,7 @@ describe('aboutCommand', () => { it('should show the correct sandbox environment variable', async () => { process.env.SANDBOX = 'gemini-sandbox'; - if (!aboutCommand.action) { + if (aboutCommand.action == null) { throw new Error('The about command must have an action.'); } @@ -145,7 +145,7 @@ describe('aboutCommand', () => { it('should show sandbox-exec profile when applicable', async () => { process.env.SANDBOX = 'sandbox-exec'; process.env.SEATBELT_PROFILE = 'test-profile'; - if (!aboutCommand.action) { + if (aboutCommand.action == null) { throw new Error('The about command must have an action.'); } @@ -164,7 +164,7 @@ describe('aboutCommand', () => { } as Partial as IdeClient); process.env.SANDBOX = ''; - if (!aboutCommand.action) { + if (aboutCommand.action == null) { throw new Error('The about command must have an action.'); } diff --git a/packages/cli/src/ui/commands/aboutCommand.ts b/packages/cli/src/ui/commands/aboutCommand.ts index 14ac1d5848..f5fd53ee0b 100644 --- a/packages/cli/src/ui/commands/aboutCommand.ts +++ b/packages/cli/src/ui/commands/aboutCommand.ts @@ -5,7 +5,7 @@ */ import { getCliVersion } from '../../utils/version.js'; -import { CommandKind, SlashCommand } from './types.js'; +import { CommandKind, type SlashCommand } from './types.js'; import process from 'node:process'; import { MessageType, type HistoryItemAbout } from '../types.js'; import { getRuntimeApi } from '../contexts/RuntimeContext.js'; @@ -22,7 +22,7 @@ export const aboutCommand: SlashCommand = { sandboxEnv = process.env.SANDBOX; } else if (process.env.SANDBOX === 'sandbox-exec') { sandboxEnv = `sandbox-exec (${ - process.env.SEATBELT_PROFILE || 'unknown' + process.env.SEATBELT_PROFILE ?? 'unknown' })`; } // Determine the currently selected model/provider using runtime diagnostics @@ -36,7 +36,7 @@ export const aboutCommand: SlashCommand = { runtimeApi = null; } - if (runtimeApi) { + if (runtimeApi != null) { try { const snapshot = runtimeApi.getRuntimeDiagnosticsSnapshot(); if (snapshot.modelName) { @@ -47,7 +47,7 @@ export const aboutCommand: SlashCommand = { provider = snapshot.providerName; } - const activeProviderName = runtimeApi.getActiveProviderName?.(); + const activeProviderName = runtimeApi.getActiveProviderName(); if ( activeProviderName && snapshot.modelName && @@ -56,49 +56,45 @@ export const aboutCommand: SlashCommand = { modelVersion = `${activeProviderName}:${snapshot.modelName}`; } - const providerManager = runtimeApi.getCliProviderManager?.(); - if ( - providerManager && - typeof providerManager.getActiveProvider === 'function' - ) { + const providerManager = runtimeApi.getCliProviderManager(); + if (typeof providerManager.getActiveProvider === 'function') { const activeProvider = providerManager.getActiveProvider(); - if (activeProvider) { - provider = activeProvider.name ?? provider; - let finalProvider: unknown = activeProvider; - if ( - 'wrappedProvider' in activeProvider && - activeProvider.wrappedProvider - ) { - finalProvider = activeProvider.wrappedProvider; - } - const providerWithGetBaseURL = finalProvider as { - getBaseURL?: () => string | undefined; - }; - if (typeof providerWithGetBaseURL.getBaseURL === 'function') { - baseURL = providerWithGetBaseURL.getBaseURL?.() ?? ''; - } + provider = activeProvider.name; + let finalProvider: unknown = activeProvider; + if ( + 'wrappedProvider' in activeProvider && + (activeProvider as Record).wrappedProvider != null + ) { + finalProvider = (activeProvider as Record) + .wrappedProvider; + } + const providerWithGetBaseURL = finalProvider as { + getBaseURL?: () => string | undefined; + }; + if (typeof providerWithGetBaseURL.getBaseURL === 'function') { + baseURL = providerWithGetBaseURL.getBaseURL() ?? ''; } } if (!baseURL) { - const runtimeBaseUrl = runtimeApi.getEphemeralSetting?.('base-url'); + const runtimeBaseUrl = runtimeApi.getEphemeralSetting('base-url'); if (typeof runtimeBaseUrl === 'string') { baseURL = runtimeBaseUrl; } } } catch { - modelVersion = context.services.config?.getModel() || modelVersion; + modelVersion = context.services.config?.getModel() ?? modelVersion; } if (modelVersion === 'Unknown') { - modelVersion = context.services.config?.getModel() || modelVersion; + modelVersion = context.services.config?.getModel() ?? modelVersion; } } else { - modelVersion = context.services.config?.getModel() || modelVersion; + modelVersion = context.services.config?.getModel() ?? modelVersion; } if (!baseURL) { - const fallbackBaseUrl = context.services.config?.getEphemeralSetting?.( + const fallbackBaseUrl = context.services.config?.getEphemeralSetting( 'base-url', ) as string | undefined; if (fallbackBaseUrl) { @@ -108,17 +104,17 @@ export const aboutCommand: SlashCommand = { if (provider === 'Unknown') { const fallbackProvider = - context.services.config?.getProvider?.() ?? undefined; + context.services.config?.getProvider() ?? undefined; if (fallbackProvider) { provider = fallbackProvider; } } const cliVersion = await getCliVersion(); - const gcpProject = process.env.GOOGLE_CLOUD_PROJECT || ''; + const gcpProject = process.env.GOOGLE_CLOUD_PROJECT ?? ''; const ideClient = - (context.services.config?.getIdeMode() && - context.services.config?.getIdeClient()?.getDetectedIdeDisplayName()) || + (context.services.config?.getIdeMode() === true && + context.services.config.getIdeClient()?.getDetectedIdeDisplayName()) || ''; // Determine keyfile path and key status for the active provider (if any) @@ -132,7 +128,7 @@ export const aboutCommand: SlashCommand = { const providerName = providerManager.getActiveProviderName(); if (providerName) { keyfilePath = - context.services.settings.getProviderKeyfile?.(providerName) || ''; + context.services.settings.getProviderKeyfile(providerName) ?? ''; // We don't check for API keys anymore - they're only in profiles } } catch { @@ -141,13 +137,13 @@ export const aboutCommand: SlashCommand = { const aboutItem: Omit = { type: MessageType.ABOUT, + keyfile: keyfilePath, + key: keyStatus, cliVersion, osVersion, sandboxEnv, modelVersion, gcpProject, - keyfile: keyfilePath, - key: keyStatus, ideClient, provider, baseURL, diff --git a/packages/cli/src/ui/commands/authCommand.codex.test.ts b/packages/cli/src/ui/commands/authCommand.codex.test.ts index c508a02bf2..9c00a874bc 100644 --- a/packages/cli/src/ui/commands/authCommand.codex.test.ts +++ b/packages/cli/src/ui/commands/authCommand.codex.test.ts @@ -12,8 +12,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { AuthCommandExecutor } from './authCommand.js'; -import { OAuthManager } from '../../auth/oauth-manager.js'; -import { CommandContext } from './types.js'; +import type { OAuthManager } from '../../auth/oauth-manager.js'; +import type { CommandContext } from './types.js'; describe('AuthCommand Codex OAuth Integration', () => { let mockOAuthManager: OAuthManager; @@ -98,7 +98,7 @@ describe('AuthCommand Codex OAuth Integration', () => { // Then: Should enable OAuth expect(mockToggleOAuth).toHaveBeenCalledWith('codex'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth enabled for codex', @@ -120,7 +120,7 @@ describe('AuthCommand Codex OAuth Integration', () => { // Then: Should disable OAuth expect(mockToggleOAuth).toHaveBeenCalledWith('codex'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth disabled for codex', @@ -147,7 +147,7 @@ describe('AuthCommand Codex OAuth Integration', () => { // Then: Should show status expect(mockIsEnabled).toHaveBeenCalledWith('codex'); expect(mockIsAuthenticated).toHaveBeenCalledWith('codex'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth for codex: ENABLED (authenticated)', @@ -168,7 +168,7 @@ describe('AuthCommand Codex OAuth Integration', () => { const result = await executor.execute(mockContext, 'codex'); // Then: Should show status - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth for codex: ENABLED (not authenticated)', @@ -189,7 +189,7 @@ describe('AuthCommand Codex OAuth Integration', () => { // Then: Should logout (undefined bucket means default/no bucket) expect(mockLogout).toHaveBeenCalledWith('codex', undefined); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'Successfully logged out of codex', diff --git a/packages/cli/src/ui/commands/authCommand.test.ts b/packages/cli/src/ui/commands/authCommand.test.ts index 72e3b9a60a..c528882bc6 100644 --- a/packages/cli/src/ui/commands/authCommand.test.ts +++ b/packages/cli/src/ui/commands/authCommand.test.ts @@ -6,8 +6,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; import { AuthCommandExecutor } from './authCommand.js'; -import { OAuthManager } from '../../auth/oauth-manager.js'; -import { CommandContext } from './types.js'; +import type { OAuthManager } from '../../auth/oauth-manager.js'; +import type { CommandContext } from './types.js'; // Mock OAuth manager and dependencies const peekStoredTokenMock = vi.fn(); @@ -55,7 +55,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext); // Then: Should show OAuth dialog - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'dialog', dialog: 'auth', }); @@ -66,7 +66,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext); // Then: Should return dialog action (OAuth-only architecture) - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'dialog', dialog: 'auth', }); @@ -91,7 +91,7 @@ describe('AuthCommandExecutor OAuth Support', () => { // Then: Should show provider status expect(mockIsEnabled).toHaveBeenCalledWith('gemini'); expect(mockIsAuthenticated).toHaveBeenCalledWith('gemini'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth for gemini: ENABLED (authenticated)', @@ -122,7 +122,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext, 'gemini'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -153,7 +153,7 @@ describe('AuthCommandExecutor OAuth Support', () => { // Then: Should toggle OAuth enablement and return success expect(mockToggleOAuth).toHaveBeenCalledWith('gemini'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth enabled for gemini', @@ -175,7 +175,7 @@ describe('AuthCommandExecutor OAuth Support', () => { // Then: Should toggle OAuth enablement and return success expect(mockToggleOAuth).toHaveBeenCalledWith('qwen'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth disabled for qwen', @@ -198,7 +198,7 @@ describe('AuthCommandExecutor OAuth Support', () => { // Then: Should trim and show provider status expect(mockIsEnabled).toHaveBeenCalledWith('gemini'); expect(mockIsAuthenticated).toHaveBeenCalledWith('gemini'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth for gemini: DISABLED', @@ -210,7 +210,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext, 'gemini invalid'); // Then: Should return error message - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -228,7 +228,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext); // Then: Should return dialog action (OAuth-only, no API key options) - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'dialog', dialog: 'auth', }); @@ -247,7 +247,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext, 'unknown-provider'); // Then: Should return error message - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -260,7 +260,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext, ' '); // Then: Should show OAuth menu - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'dialog', dialog: 'auth', }); @@ -291,7 +291,7 @@ describe('AuthCommandExecutor OAuth Support', () => { // Then: Should enable OAuth and show success message expect(mockToggleOAuth).toHaveBeenCalledWith('gemini'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth enabled for gemini', @@ -313,7 +313,7 @@ describe('AuthCommandExecutor OAuth Support', () => { // Then: Should disable OAuth and show success message expect(mockToggleOAuth).toHaveBeenCalledWith('qwen'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth disabled for qwen', @@ -332,7 +332,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext, 'gemini enable'); // Then: Should show already enabled message - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth for gemini is already enabled', @@ -351,7 +351,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext, 'qwen disable'); // Then: Should show already disabled message - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'OAuth for qwen is already disabled', @@ -374,7 +374,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext, 'gemini enable'); // Then: Should show warning about precedence - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -411,7 +411,9 @@ describe('AuthCommandExecutor OAuth Support', () => { (mockOAuthManager.getAuthStatus as unknown) = mockGetAuthStatus; const result = await executor.getAuthStatus(); - expect(result).toEqual(['[] gemini: not authenticated [OAuth enabled]']); + expect(result).toStrictEqual([ + '[] gemini: not authenticated [OAuth enabled]', + ]); }); }); @@ -437,7 +439,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.getAuthStatus(); // Then: Should return formatted status indicators with enablement info - expect(result).toEqual([ + expect(result).toStrictEqual([ '[✓] gemini: authenticated (expires in 60m) [OAuth enabled]', '[] qwen: not authenticated [OAuth disabled]', ]); @@ -461,7 +463,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext, 'gemini enable'); // Then: Should return error message - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Failed to enable OAuth for gemini: Toggle failed', @@ -484,7 +486,7 @@ describe('AuthCommandExecutor OAuth Support', () => { const result = await executor.execute(mockContext, 'qwen disable'); // Then: Should return user-friendly error message - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Failed to disable OAuth for qwen: Cannot save configuration', diff --git a/packages/cli/src/ui/commands/authCommand.ts b/packages/cli/src/ui/commands/authCommand.ts index 0fe5a3e815..7d2f0eecda 100644 --- a/packages/cli/src/ui/commands/authCommand.ts +++ b/packages/cli/src/ui/commands/authCommand.ts @@ -11,10 +11,10 @@ import { CommandKind, - SlashCommand, - CommandContext, - SlashCommandActionReturn, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type SlashCommandActionReturn, + type MessageActionReturn, } from './types.js'; import { OAuthManager } from '../../auth/oauth-manager.js'; import { DebugLogger, MessageBus } from '@vybestack/llxprt-code-core'; @@ -23,10 +23,7 @@ import { GeminiOAuthProvider } from '../../auth/gemini-oauth-provider.js'; import { AnthropicOAuthProvider } from '../../auth/anthropic-oauth-provider.js'; import { CodexOAuthProvider } from '../../auth/codex-oauth-provider.js'; import { getRuntimeApi } from '../contexts/RuntimeContext.js'; -import { - type CommandArgumentSchema, - type CompleterFn, -} from './schema/types.js'; +import type { CommandArgumentSchema, CompleterFn } from './schema/types.js'; import { withFuzzyFilter } from '../utils/fuzzyFilter.js'; import { createTokenStore } from '../../auth/proxy/credential-store-factory.js'; @@ -40,7 +37,7 @@ function getOAuthManager(): OAuthManager { const runtime = getRuntimeApi(); let oauthManager = runtime.getCliOAuthManager(); - if (!oauthManager) { + if (oauthManager == null) { const tokenStore = createTokenStore(); oauthManager = new OAuthManager(tokenStore); oauthManager.registerProvider(new GeminiOAuthProvider()); @@ -99,14 +96,13 @@ const logoutCompleter: CompleterFn = withFuzzyFilter( const oauthManager = getOAuthManager(); const buckets = await oauthManager.listBuckets(provider); - const options = [ + return [ { value: '--all', description: 'Logout from all buckets' }, ...buckets.map((bucket) => ({ value: bucket, description: `Logout from bucket: ${bucket}`, })), ]; - return options; } catch { return []; } @@ -185,7 +181,7 @@ export class AuthCommandExecutor { constructor(private oauthManager: OAuthManager) {} async execute( - context: CommandContext, + _context: CommandContext, args?: string, ): Promise { // Parse args while preserving original parts for error messages @@ -280,7 +276,7 @@ export class AuthCommandExecutor { ); } - if (token && typeof token.expiry === 'number') { + if (token != null && typeof token.expiry === 'number') { // Lines 72-76: Calculate time until expiry const expiryDate = new Date(token.expiry * 1000); const timeUntilExpiry = Math.max(0, token.expiry - Date.now() / 1000); @@ -298,9 +294,8 @@ export class AuthCommandExecutor { messageType: 'info', content: status, }; - } else { - status += ' (authenticated)'; } + status += ' (authenticated)'; } else if (isEnabled && !isAuthenticated) { status += ' (not authenticated)'; } @@ -389,7 +384,7 @@ export class AuthCommandExecutor { // Get the provider instance const providerInstance = providerManager.getProviderByName(provider); - if (!providerInstance) return; + if (providerInstance == null) return; // If it's an OpenAI provider (which Qwen uses), clear its cache if ( @@ -663,11 +658,11 @@ export const authCommand: SlashCommand = { // If for some reason it doesn't exist yet, create it // @plan:PLAN-20250214-CREDPROXY.P33 - if (!oauthManager) { + if (oauthManager == null) { // This should rarely happen, but handle it as a fallback const tokenStore = createTokenStore(); const config = context.services.config; - if (!config) { + if (config == null) { throw new Error('Auth command requires an initialized Config service.'); } const runtimeMessageBus = new MessageBus( diff --git a/packages/cli/src/ui/commands/baseurlCommand.ts b/packages/cli/src/ui/commands/baseurlCommand.ts index 145f28eac8..eb6cba10cc 100644 --- a/packages/cli/src/ui/commands/baseurlCommand.ts +++ b/packages/cli/src/ui/commands/baseurlCommand.ts @@ -5,9 +5,9 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, CommandKind, } from './types.js'; import { getRuntimeApi } from '../contexts/RuntimeContext.js'; @@ -17,7 +17,7 @@ export const baseurlCommand: SlashCommand = { description: 'set base URL for the current provider', kind: CommandKind.BUILT_IN, action: async ( - context: CommandContext, + _context: CommandContext, args: string, ): Promise => { const baseUrl = args?.trim(); diff --git a/packages/cli/src/ui/commands/bugCommand.test.ts b/packages/cli/src/ui/commands/bugCommand.test.ts index 4271c12a5c..ddd6d8ea23 100644 --- a/packages/cli/src/ui/commands/bugCommand.test.ts +++ b/packages/cli/src/ui/commands/bugCommand.test.ts @@ -59,7 +59,7 @@ describe('bugCommand', () => { }, }); - if (!bugCommand.action) throw new Error('Action is not defined'); + if (bugCommand.action == null) throw new Error('Action is not defined'); await bugCommand.action(mockContext, 'A test bug'); const expectedInfo = ` @@ -96,7 +96,7 @@ describe('bugCommand', () => { }, }, }); - if (!bugCommand.action) throw new Error('Action is not defined'); + if (bugCommand.action == null) throw new Error('Action is not defined'); await bugCommand.action(mockContext, 'A custom bug'); const expectedInfo = ` diff --git a/packages/cli/src/ui/commands/chatCommand.test.ts b/packages/cli/src/ui/commands/chatCommand.test.ts index 450127cfd9..1fc012b28a 100644 --- a/packages/cli/src/ui/commands/chatCommand.test.ts +++ b/packages/cli/src/ui/commands/chatCommand.test.ts @@ -11,16 +11,16 @@ import { expect, beforeEach, afterEach, - Mocked, + type Mocked, } from 'vitest'; import type { CommandContext, SlashCommand } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; -import { GeminiClient } from '@vybestack/llxprt-code-core'; +import type { GeminiClient } from '@vybestack/llxprt-code-core'; import * as fsPromises from 'fs/promises'; import { chatCommand } from './chatCommand.js'; -import { Stats } from 'fs'; +import type { Stats } from 'fs'; import { createCompletionHandler } from './schema/index.js'; vi.mock('fs/promises', () => ({ @@ -58,7 +58,7 @@ describe('chatCommand', () => { const subCommand = chatCommand.subCommands?.find( (cmd) => cmd.name === name, ); - if (!subCommand) { + if (subCommand == null) { throw new Error(`/chat ${name} command not found.`); } return subCommand; @@ -165,7 +165,7 @@ describe('chatCommand', () => { it('should return an error if tag is missing', async () => { const result = await saveCommand?.action?.(mockContext, ' '); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Missing tag. Usage: /chat save ', @@ -175,7 +175,7 @@ describe('chatCommand', () => { it('should inform if conversation history is empty or only contains system context', async () => { mockGetHistory.mockReturnValue([]); let result = await saveCommand?.action?.(mockContext, tag); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'No conversation found to save.', @@ -185,7 +185,7 @@ describe('chatCommand', () => { { role: 'user', parts: [{ text: 'context for our chat' }] }, ]); result = await saveCommand?.action?.(mockContext, tag); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'No conversation found to save.', @@ -197,7 +197,7 @@ describe('chatCommand', () => { { role: 'model', parts: [{ text: 'I am doing well!' }] }, ]); result = await saveCommand?.action?.(mockContext, tag); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: `Conversation checkpoint saved with tag: ${tag}.`, @@ -237,7 +237,7 @@ describe('chatCommand', () => { expect(mockCheckpointExists).not.toHaveBeenCalled(); // Should skip existence check expect(mockSaveCheckpoint).toHaveBeenCalledWith(history, tag); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: `Conversation checkpoint saved with tag: ${tag}.`, @@ -334,7 +334,7 @@ describe('chatCommand', () => { it('should return an error if tag is missing', async () => { const result = await resumeCommand?.action?.(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Missing tag. Usage: /chat resume ', @@ -346,7 +346,7 @@ describe('chatCommand', () => { const result = await resumeCommand?.action?.(mockContext, badTag); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: `No saved checkpoint found with tag: ${badTag}.`, @@ -363,7 +363,7 @@ describe('chatCommand', () => { const result = await resumeCommand?.action?.(mockContext, goodTag); // Now returns LoadHistoryActionReturn to properly sync UI and client history - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'load_history', history: [ { type: 'user', text: 'hello gemini' }, @@ -403,7 +403,7 @@ describe('chatCommand', () => { }) as Stats) as unknown as typeof fsPromises.stat, ); - expect(await runCompletion('a')).toEqual(['alpha']); + expect(await runCompletion('a')).toStrictEqual(['alpha']); }); it('should suggest filenames sorted by modified time (newest first)', async () => { @@ -422,7 +422,7 @@ describe('chatCommand', () => { return { mtime: new Date(date.getTime() + 1000) } as Stats; }) as unknown as typeof fsPromises.stat); - expect(await runCompletion('')).toEqual(['test2', 'test1']); + expect(await runCompletion('')).toStrictEqual(['test2', 'test1']); }); }); }); @@ -436,7 +436,7 @@ describe('chatCommand', () => { it('should return an error if tag is missing', async () => { const result = await deleteCommand?.action?.(mockContext, ' '); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Missing tag. Usage: /chat delete ', @@ -466,7 +466,7 @@ describe('chatCommand', () => { const result = await deleteCommand?.action?.(mockContext, tag); expect(mockDeleteCheckpoint).toHaveBeenCalledWith(tag); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: `Deleted checkpoint: ${tag}`, @@ -500,7 +500,7 @@ describe('chatCommand', () => { '/chat delete a', ); - expect(result.suggestions.map((option) => option.value)).toEqual([ + expect(result.suggestions.map((option) => option.value)).toStrictEqual([ 'alpha', ]); }); diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts index bf4e0af9a4..87780eafde 100644 --- a/packages/cli/src/ui/commands/chatCommand.ts +++ b/packages/cli/src/ui/commands/chatCommand.ts @@ -9,11 +9,11 @@ import React from 'react'; import { Text } from 'ink'; import { Colors } from '../colors.js'; import { - CommandContext, - SlashCommand, - MessageActionReturn, + type CommandContext, + type SlashCommand, + type MessageActionReturn, CommandKind, - SlashCommandActionReturn, + type SlashCommandActionReturn, } from './types.js'; import { decodeTagName, @@ -28,8 +28,8 @@ import type { HistoryItemWithoutId, } from '../types.js'; import { MessageType } from '../types.js'; -import { type CommandArgumentSchema } from './schema/types.js'; -import { type Part } from '@google/genai'; +import type { CommandArgumentSchema } from './schema/types.js'; +import type { Part } from '@google/genai'; import { withFuzzyFilter } from '../utils/fuzzyFilter.js'; const getSavedChatTags = async ( @@ -127,7 +127,7 @@ const saveCommand: SlashCommand = { await logger.initialize(); // Check for overwrite confirmation first - if (!context.overwriteConfirmed) { + if (context.overwriteConfirmed !== true) { const exists = await logger.checkpointExists(tag); if (exists) { return { @@ -140,7 +140,7 @@ const saveCommand: SlashCommand = { ' already exists. Do you want to overwrite it?', ), originalInvocation: { - raw: context.invocation?.raw || `/chat save ${tag}`, + raw: context.invocation?.raw ?? `/chat save ${tag}`, }, }; } @@ -148,7 +148,7 @@ const saveCommand: SlashCommand = { const client = config?.getGeminiClient(); // Check if chat is initialized before accessing it - if (!client?.hasChatInitialized()) { + if (client?.hasChatInitialized() !== true) { return { type: 'message', messageType: 'error', @@ -165,13 +165,12 @@ const saveCommand: SlashCommand = { messageType: 'info', content: `Conversation checkpoint saved with tag: ${decodeTagName(tag)}.`, }; - } else { - return { - type: 'message', - messageType: 'info', - content: 'No conversation found to save.', - }; } + return { + type: 'message', + messageType: 'info', + content: 'No conversation found to save.', + }; }, }; @@ -198,7 +197,9 @@ const resumeCommand: SlashCommand = { // Get emoji filter mode from settings const emojiFilterMode = - (config?.getEphemeralSetting('emojifilter') as EmojiFilterMode) || 'auto'; + (config?.getEphemeralSetting('emojifilter') as + | EmojiFilterMode + | undefined) ?? 'auto'; // Create emoji filter if not in 'allowed' mode const emojiFilter = @@ -210,7 +211,7 @@ const resumeCommand: SlashCommand = { let conversation = checkpoint.history; // Apply emoji filtering if needed - if (emojiFilter) { + if (emojiFilter != null) { conversation = conversation.map((item) => { const filteredItem = { ...item }; if (Array.isArray(filteredItem.parts)) { @@ -238,9 +239,7 @@ const resumeCommand: SlashCommand = { // Use LoadHistoryActionReturn to properly sync both UI and client history const uiHistory: HistoryItemWithoutId[] = conversation.map((content) => { const text = - content.parts - ?.map((part: Part) => (part.text ? part.text : '')) - .join('') || ''; + content.parts?.map((part: Part) => part.text ?? '').join('') ?? ''; return { type: content.role === 'user' ? MessageType.USER : MessageType.GEMINI, text, @@ -278,7 +277,7 @@ const deleteCommand: SlashCommand = { const { logger } = context.services; await logger.initialize(); - if (!force && !context.overwriteConfirmed) { + if (!force && context.overwriteConfirmed !== true) { return { type: 'confirm_action', prompt: React.createElement( @@ -289,7 +288,7 @@ const deleteCommand: SlashCommand = { '?', ), originalInvocation: { - raw: context.invocation?.raw || `/chat delete ${tag}`, + raw: context.invocation?.raw ?? `/chat delete ${tag}`, }, }; } @@ -341,7 +340,7 @@ const renameCommand: SlashCommand = { } if (await logger.checkpointExists(newTag)) { - if (!context.overwriteConfirmed) { + if (context.overwriteConfirmed !== true) { return { type: 'confirm_action', prompt: React.createElement( @@ -352,7 +351,7 @@ const renameCommand: SlashCommand = { ' already exists. Do you want to overwrite it?', ), originalInvocation: { - raw: context.invocation?.raw || `/chat rename ${oldTag} ${newTag}`, + raw: context.invocation?.raw ?? `/chat rename ${oldTag} ${newTag}`, }, }; } @@ -380,7 +379,7 @@ const clearCommand: SlashCommand = { action: async (context): Promise => { const client = context.services.config?.getGeminiClient(); // Check if chat is initialized before clearing - if (!client?.hasChatInitialized()) { + if (client?.hasChatInitialized() !== true) { return { type: 'message', messageType: 'info', @@ -418,7 +417,7 @@ const restoreHistory = async ( turns: number, ): Promise => { const client = context.services.config?.getGeminiClient(); - if (!client?.hasChatInitialized()) { + if (client?.hasChatInitialized() !== true) { return { type: 'message', messageType: 'error', @@ -457,9 +456,7 @@ const restoreHistory = async ( // Convert to UI history items for display const uiHistory: HistoryItemWithoutId[] = newHistory.map((content) => { const text = - content.parts - ?.map((part: Part) => (part.text ? part.text : '')) - .join('') || ''; + content.parts?.map((part: Part) => part.text ?? '').join('') ?? ''; return { type: content.role === 'user' ? MessageType.USER : MessageType.GEMINI, text, @@ -518,7 +515,7 @@ const debugCommand: SlashCommand = { debugInfo.push(`Chat initialized: ${chatInitialized}`); // History information - if (chatInitialized && client) { + if (chatInitialized && client != null) { try { const chat = client.getChat(); const history = chat.getHistory(); @@ -531,7 +528,7 @@ const debugCommand: SlashCommand = { } // Model information - if (config) { + if (config != null) { try { const model = config.getModel(); debugInfo.push(`Current model: ${model}`); diff --git a/packages/cli/src/ui/commands/clearCommand.test.ts b/packages/cli/src/ui/commands/clearCommand.test.ts index cf565c8245..ddfed1a365 100644 --- a/packages/cli/src/ui/commands/clearCommand.test.ts +++ b/packages/cli/src/ui/commands/clearCommand.test.ts @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { vi, describe, it, expect, beforeEach, Mock } from 'vitest'; +import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest'; import { clearCommand } from './clearCommand'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import { getCliRuntimeServices } from '../../runtime/runtimeSettings.js'; // Mock the telemetry service @@ -27,8 +27,8 @@ vi.mock('../../runtime/runtimeSettings.js', () => ({ })); import { - Config, - GeminiClient, + type Config, + type GeminiClient, uiTelemetryService, triggerSessionEndHook, triggerSessionStartHook, diff --git a/packages/cli/src/ui/commands/clearCommand.ts b/packages/cli/src/ui/commands/clearCommand.ts index 6bb0c1a23a..b569c72fa0 100644 --- a/packages/cli/src/ui/commands/clearCommand.ts +++ b/packages/cli/src/ui/commands/clearCommand.ts @@ -5,20 +5,24 @@ */ import { - GeminiClient, + type GeminiClient, uiTelemetryService, triggerSessionEndHook, triggerSessionStartHook, SessionEndReason, SessionStartSource, } from '@vybestack/llxprt-code-core'; -import { CommandKind, SlashCommand, type CommandContext } from './types.js'; +import { + CommandKind, + type SlashCommand, + type CommandContext, +} from './types.js'; import { getCliRuntimeServices } from '../../runtime/runtimeSettings.js'; function resolveForegroundGeminiClient( context: CommandContext, ): GeminiClient | null { - if (context.services.config) { + if (context.services.config != null) { return context.services.config.getGeminiClient(); } @@ -37,11 +41,11 @@ export const clearCommand: SlashCommand = { action: async (context, _args) => { const geminiClient = resolveForegroundGeminiClient(context); - if (geminiClient) { + if (geminiClient != null) { context.ui.setDebugMessage('Clearing terminal and resetting chat.'); // Trigger SessionEnd hook before clearing (fail-open) - if (context.services.config) { + if (context.services.config != null) { try { await triggerSessionEndHook( context.services.config, @@ -55,7 +59,7 @@ export const clearCommand: SlashCommand = { await geminiClient.resetChat(); // Trigger SessionStart hook after clearing (fail-open) - if (context.services.config) { + if (context.services.config != null) { try { const sessionStartOutput = await triggerSessionStartHook( context.services.config, diff --git a/packages/cli/src/ui/commands/compressCommand.test.ts b/packages/cli/src/ui/commands/compressCommand.test.ts index 7432f7f716..519e89f498 100644 --- a/packages/cli/src/ui/commands/compressCommand.test.ts +++ b/packages/cli/src/ui/commands/compressCommand.test.ts @@ -5,9 +5,9 @@ */ import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { GeminiClient } from '@vybestack/llxprt-code-core'; import { CompressionStatus, - GeminiClient, PerformCompressionResult, } from '@vybestack/llxprt-code-core'; import { compressCommand } from './compressCommand.js'; diff --git a/packages/cli/src/ui/commands/compressCommand.ts b/packages/cli/src/ui/commands/compressCommand.ts index 1b23c23660..15a6b14f52 100644 --- a/packages/cli/src/ui/commands/compressCommand.ts +++ b/packages/cli/src/ui/commands/compressCommand.ts @@ -8,8 +8,10 @@ import { CompressionStatus, PerformCompressionResult, } from '@vybestack/llxprt-code-core'; -import { HistoryItemCompression, MessageType } from '../types.js'; -import { CommandKind, SlashCommand } from './types.js'; +import type { HistoryItemCompression } from '../types.js'; +import { MessageType } from '../types.js'; +import type { SlashCommand } from './types.js'; +import { CommandKind } from './types.js'; export const compressCommand: SlashCommand = { name: 'compress', @@ -44,7 +46,7 @@ export const compressCommand: SlashCommand = { ui.setPendingItem(pendingMessage); const promptId = `compress-${Date.now()}`; const geminiClient = context.services.config?.getGeminiClient(); - if (!geminiClient || !geminiClient.hasChatInitialized()) { + if (!geminiClient?.hasChatInitialized()) { ui.addItem( { type: MessageType.ERROR, diff --git a/packages/cli/src/ui/commands/continueCommand.ts b/packages/cli/src/ui/commands/continueCommand.ts index e477703789..d393569da3 100644 --- a/packages/cli/src/ui/commands/continueCommand.ts +++ b/packages/cli/src/ui/commands/continueCommand.ts @@ -124,8 +124,8 @@ export const continueCommand: SlashCommand = { // Interactive mode with active conversation requires confirmation (REQ-RC-010) return { type: 'perform_resume', - sessionRef, requiresConfirmation: true, + sessionRef, }; } diff --git a/packages/cli/src/ui/commands/copyCommand.test.ts b/packages/cli/src/ui/commands/copyCommand.test.ts index 57af82bd3c..114f2304a6 100644 --- a/packages/cli/src/ui/commands/copyCommand.test.ts +++ b/packages/cli/src/ui/commands/copyCommand.test.ts @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { vi, describe, it, expect, beforeEach, Mock } from 'vitest'; +import { vi, describe, it, expect, beforeEach, type Mock } from 'vitest'; import { copyCommand } from './copyCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import { copyToClipboard } from '../utils/commandUtils.js'; @@ -45,7 +45,7 @@ describe('copyCommand', () => { }); it('should return info message when no history is available', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); // Mock no chat initialized mockContext = createMockCommandContext({ @@ -62,7 +62,7 @@ describe('copyCommand', () => { const result = await copyCommand.action(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'No chat history available yet', @@ -72,13 +72,13 @@ describe('copyCommand', () => { }); it('should return info message when history is empty', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); mockGetHistory.mockReturnValue([]); const result = await copyCommand.action(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'No output in history', @@ -88,7 +88,7 @@ describe('copyCommand', () => { }); it('should return info message when no AI messages are found in history', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); const historyWithUserOnly = [ { @@ -101,7 +101,7 @@ describe('copyCommand', () => { const result = await copyCommand.action(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'No output in history', @@ -111,7 +111,7 @@ describe('copyCommand', () => { }); it('should copy last AI message to clipboard successfully', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); const historyWithAiMessage = [ { @@ -129,7 +129,7 @@ describe('copyCommand', () => { const result = await copyCommand.action(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'Last output copied to the clipboard', @@ -141,7 +141,7 @@ describe('copyCommand', () => { }); it('should handle multiple text parts in AI message', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); const historyWithMultipleParts = [ { @@ -156,7 +156,7 @@ describe('copyCommand', () => { const result = await copyCommand.action(mockContext, ''); expect(mockCopyToClipboard).toHaveBeenCalledWith('Part 1: Part 2: Part 3'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'Last output copied to the clipboard', @@ -164,7 +164,7 @@ describe('copyCommand', () => { }); it('should filter out non-text parts', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); const historyWithMixedParts = [ { @@ -183,7 +183,7 @@ describe('copyCommand', () => { const result = await copyCommand.action(mockContext, ''); expect(mockCopyToClipboard).toHaveBeenCalledWith('Text part more text'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'Last output copied to the clipboard', @@ -191,7 +191,7 @@ describe('copyCommand', () => { }); it('should get the last AI message when multiple AI messages exist', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); const historyWithMultipleAiMessages = [ { @@ -214,7 +214,7 @@ describe('copyCommand', () => { const result = await copyCommand.action(mockContext, ''); expect(mockCopyToClipboard).toHaveBeenCalledWith('Second AI response'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'Last output copied to the clipboard', @@ -222,7 +222,7 @@ describe('copyCommand', () => { }); it('should handle clipboard copy error', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); const historyWithAiMessage = [ { @@ -237,7 +237,7 @@ describe('copyCommand', () => { const result = await copyCommand.action(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: `Failed to copy to the clipboard. ${clipboardError.message}`, @@ -245,7 +245,7 @@ describe('copyCommand', () => { }); it('should handle non-Error clipboard errors', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); const historyWithAiMessage = [ { @@ -260,7 +260,7 @@ describe('copyCommand', () => { const result = await copyCommand.action(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: `Failed to copy to the clipboard. ${rejectedValue}`, @@ -268,7 +268,7 @@ describe('copyCommand', () => { }); it('should return info message when no text parts found in AI message', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); const historyWithEmptyParts = [ { @@ -281,7 +281,7 @@ describe('copyCommand', () => { const result = await copyCommand.action(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'Last AI output contains no text to copy.', @@ -291,7 +291,7 @@ describe('copyCommand', () => { }); it('should handle unavailable config service', async () => { - if (!copyCommand.action) throw new Error('Command has no action'); + if (copyCommand.action == null) throw new Error('Command has no action'); const nullConfigContext = createMockCommandContext({ services: { config: null }, @@ -299,7 +299,7 @@ describe('copyCommand', () => { const result = await copyCommand.action(nullConfigContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'No chat history available yet', diff --git a/packages/cli/src/ui/commands/copyCommand.ts b/packages/cli/src/ui/commands/copyCommand.ts index f79e5c3802..d91f234d68 100644 --- a/packages/cli/src/ui/commands/copyCommand.ts +++ b/packages/cli/src/ui/commands/copyCommand.ts @@ -7,10 +7,10 @@ import { copyToClipboard } from '../utils/commandUtils.js'; import { CommandKind, - SlashCommand, - SlashCommandActionReturn, + type SlashCommand, + type SlashCommandActionReturn, } from './types.js'; -import { Part, Content } from '@google/genai'; +import type { Part, Content } from '@google/genai'; import { debugLogger } from '@vybestack/llxprt-code-core'; export const copyCommand: SlashCommand = { @@ -38,7 +38,7 @@ export const copyCommand: SlashCommand = { ? history.filter((item: Content) => item.role === 'model').pop() : undefined; - if (!lastAiMessage) { + if (lastAiMessage == null) { return { type: 'message', messageType: 'info', diff --git a/packages/cli/src/ui/commands/debugCommands.ts b/packages/cli/src/ui/commands/debugCommands.ts index 2291471b1e..c214651a18 100644 --- a/packages/cli/src/ui/commands/debugCommands.ts +++ b/packages/cli/src/ui/commands/debugCommands.ts @@ -10,9 +10,9 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, CommandKind, } from './types.js'; import { ConfigurationManager } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/ui/commands/diagnosticsCommand.spec.ts b/packages/cli/src/ui/commands/diagnosticsCommand.spec.ts index e74e7d4c6e..9b92cb2ff3 100644 --- a/packages/cli/src/ui/commands/diagnosticsCommand.spec.ts +++ b/packages/cli/src/ui/commands/diagnosticsCommand.spec.ts @@ -36,7 +36,7 @@ function createMockTokenStore(providers: Record): { return { listBuckets: vi.fn(async (provider: string) => { const token = providers[provider]; - return token ? ['default'] : []; + return token != null ? ['default'] : []; }), getToken: vi.fn(async (provider: string) => providers[provider] || null), saveToken: vi.fn(), @@ -58,9 +58,9 @@ function createMCPCredentials( token: { accessToken: `mcp_token_${serverName}`, refreshToken: opts?.refreshToken, - expiresAt, tokenType: opts?.tokenType || 'Bearer', scope: opts?.scope, + expiresAt, }, updatedAt: Date.now(), }; diff --git a/packages/cli/src/ui/commands/diagnosticsCommand.ts b/packages/cli/src/ui/commands/diagnosticsCommand.ts index 95e74ae828..69359f0b93 100644 --- a/packages/cli/src/ui/commands/diagnosticsCommand.ts +++ b/packages/cli/src/ui/commands/diagnosticsCommand.ts @@ -8,9 +8,9 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, CommandKind, } from './types.js'; import { getRuntimeApi } from '../contexts/RuntimeContext.js'; @@ -56,7 +56,7 @@ export const diagnosticsCommand: SlashCommand = { const settings = context.services.settings; const logger = new DebugLogger('llxprt:ui:diagnostics'); - if (!config) { + if (config == null) { return { type: 'message', messageType: 'error', @@ -79,24 +79,24 @@ export const diagnosticsCommand: SlashCommand = { // Show current OAuth bucket const oauthMgr = context.services.oauthManager; - if (oauthMgr && snapshot.providerName) { + if (oauthMgr != null && snapshot.providerName) { const sessionBucket = oauthMgr.getSessionBucket(snapshot.providerName); diagnostics.push(`- OAuth Bucket: ${sessionBucket ?? 'default'}`); } // Show bucket failover status - const failoverHandler = config.getBucketFailoverHandler?.(); - if (failoverHandler) { - const buckets = failoverHandler.getBuckets?.() ?? []; - const currentBucket = failoverHandler.getCurrentBucket?.(); - const isEnabled = failoverHandler.isEnabled?.() ?? false; + const failoverHandler = config.getBucketFailoverHandler(); + if (failoverHandler != null) { + const buckets = failoverHandler.getBuckets(); + const currentBucket = failoverHandler.getCurrentBucket(); + const isEnabled = failoverHandler.isEnabled(); diagnostics.push( `- Bucket Failover: ${isEnabled ? 'Enabled' : 'Disabled'}`, ); if (buckets.length > 0) { diagnostics.push(`- Failover Buckets: ${buckets.join(' → ')}`); diagnostics.push( - `- Current Failover Bucket: ${currentBucket ?? buckets[0] ?? 'default'}`, + `- Current Failover Bucket: ${currentBucket ?? buckets[0]}`, ); // Calculate next bucket if (currentBucket && buckets.length > 1) { @@ -108,7 +108,7 @@ export const diagnosticsCommand: SlashCommand = { diagnostics.push(`- Next Failover Bucket: ${nextBucket}`); } } - } else if (oauthMgr && snapshot.providerName) { + } else if (oauthMgr != null && snapshot.providerName) { // No handler but OAuth is configured - indicate failover not active diagnostics.push(`- Bucket Failover: Not configured`); } @@ -241,9 +241,9 @@ export const diagnosticsCommand: SlashCommand = { // Add dumpcontext status const dumpcontextMode = - ephemeralSettings['dumpcontext'] || + (ephemeralSettings['dumpcontext'] as string | undefined) ?? (ephemeralSettings['dumponerror'] === 'enabled' ? 'error' : 'off'); - if (dumpcontextMode && dumpcontextMode !== 'off') { + if (dumpcontextMode !== 'off') { diagnostics.push(`\n## Context Dumping`); diagnostics.push(`- Mode: ${dumpcontextMode}`); diagnostics.push(`- Dump Directory: ${os.homedir()}/.llxprt/dumps/`); @@ -256,7 +256,7 @@ export const diagnosticsCommand: SlashCommand = { diagnostics.push( `- Debug Mode: ${config.getDebugMode() ? 'Enabled' : 'Disabled'}`, ); - diagnostics.push(`- Approval Mode: ${config.getApprovalMode() || 'off'}`); + diagnostics.push(`- Approval Mode: ${config.getApprovalMode()}`); diagnostics.push('\n## Compression'); const compressionThreshold = @@ -268,21 +268,23 @@ export const diagnosticsCommand: SlashCommand = { ); diagnostics.push('\n## Settings'); - const merged = settings?.merged || {}; - diagnostics.push(`- Theme: ${merged.ui?.theme || 'default'}`); - diagnostics.push(`- Default Profile: ${merged.defaultProfile || 'none'}`); - diagnostics.push(`- Sandbox: ${merged.sandbox || 'disabled'}`); + const merged = settings.merged; + diagnostics.push(`- Theme: ${merged.ui?.theme ?? 'default'}`); + diagnostics.push(`- Default Profile: ${merged.defaultProfile ?? 'none'}`); + diagnostics.push(`- Sandbox: ${merged.sandbox ?? 'disabled'}`); diagnostics.push('\n## IDE Integration'); diagnostics.push( `- IDE Mode: ${config.getIdeMode() ? 'Enabled' : 'Disabled'}`, ); const ideClient = config.getIdeClient(); - diagnostics.push(`- IDE Client: ${ideClient ? 'Connected' : 'Offline'}`); + diagnostics.push( + `- IDE Client: ${ideClient != null ? 'Connected' : 'Offline'}`, + ); diagnostics.push('\n## MCP (Model Context Protocol)'); const mcpServers = config.getMcpServers(); - if (mcpServers && Object.keys(mcpServers).length > 0) { + if (mcpServers != null && Object.keys(mcpServers).length > 0) { diagnostics.push( `- MCP Servers: ${Object.keys(mcpServers).join(', ')}`, ); @@ -300,23 +302,19 @@ export const diagnosticsCommand: SlashCommand = { `- User Memory: ${userMemory ? `${userMemory.length} characters` : 'Not loaded'}`, ); diagnostics.push( - `- Context Files: ${config.getLlxprtMdFileCount() || 0} files`, + `- Context Files: ${config.getLlxprtMdFileCount()} files`, ); diagnostics.push('\n## Tools'); try { const toolRegistry = config.getToolRegistry(); - if (toolRegistry) { - const tools = toolRegistry.getAllTools(); - diagnostics.push(`- Available Tools: ${tools.length}`); - const toolNames = tools - .map((t: { name: string }) => t.name) - .slice(0, 10); - if (toolNames.length > 0) { - diagnostics.push(`- First 10 Tools: ${toolNames.join(', ')}`); - } - } else { - diagnostics.push('- Tool Registry: Not initialized'); + const tools = toolRegistry.getAllTools(); + diagnostics.push(`- Available Tools: ${tools.length}`); + const toolNames = tools + .map((t: { name: string }) => t.name) + .slice(0, 10); + if (toolNames.length > 0) { + diagnostics.push(`- First 10 Tools: ${toolNames.join(', ')}`); } } catch { diagnostics.push('- Tool Registry: Not initialized'); @@ -324,7 +322,7 @@ export const diagnosticsCommand: SlashCommand = { diagnostics.push('\n## Telemetry'); diagnostics.push( - `- Usage Statistics: ${merged.ui?.usageStatisticsEnabled ? 'Enabled' : 'Disabled'}`, + `- Usage Statistics: ${merged.ui.usageStatisticsEnabled === true ? 'Enabled' : 'Disabled'}`, ); diagnostics.push('\n## OAuth Tokens'); @@ -333,7 +331,7 @@ export const diagnosticsCommand: SlashCommand = { const runtimeApi = getRuntimeApi(); const oauthManager = runtimeApi.getCliOAuthManager(); - if (!oauthManager) { + if (oauthManager == null) { diagnostics.push('- No OAuth tokens configured'); } else { const supportedProviders = oauthManager.getSupportedProviders(); @@ -371,7 +369,7 @@ export const diagnosticsCommand: SlashCommand = { for (const bucket of buckets) { const token = await tokenStore.getToken(provider, bucket); - if (token && typeof token.expiry === 'number') { + if (token != null && typeof token.expiry === 'number') { const expiryDate = new Date(token.expiry * 1000); const timeUntilExpiry = Math.max( 0, @@ -416,7 +414,7 @@ export const diagnosticsCommand: SlashCommand = { ` - Status: ${isExpired ? 'Expired' : 'Valid'}`, ); - if (token.expiresAt) { + if (token.expiresAt !== undefined) { const expiryDate = new Date(token.expiresAt); const timeUntilExpiry = Math.max( 0, diff --git a/packages/cli/src/ui/commands/directoryCommand.test.tsx b/packages/cli/src/ui/commands/directoryCommand.test.tsx index 665eff32b3..7a95cf642d 100644 --- a/packages/cli/src/ui/commands/directoryCommand.test.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.test.tsx @@ -11,7 +11,7 @@ import { type Config, type WorkspaceContext, } from '@vybestack/llxprt-code-core'; -import { CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { MessageType } from '../types.js'; import * as os from 'os'; import * as path from 'path'; @@ -101,7 +101,7 @@ describe('directoryCommand', () => { describe('show', () => { it('should display the list of directories', () => { - if (!showCommand?.action) throw new Error('No action'); + if (showCommand?.action == null) throw new Error('No action'); // eslint-disable-next-line @typescript-eslint/no-floating-promises showCommand.action(mockContext, ''); expect(mockWorkspaceContext.getDirectories).toHaveBeenCalled(); @@ -119,7 +119,7 @@ describe('directoryCommand', () => { describe('add', () => { it('should show an error if no path is provided', () => { - if (!addCommand?.action) throw new Error('No action'); + if (addCommand?.action == null) throw new Error('No action'); // eslint-disable-next-line @typescript-eslint/no-floating-promises addCommand.action(mockContext, ''); expect(mockContext.ui.addItem).toHaveBeenCalledWith( @@ -133,7 +133,7 @@ describe('directoryCommand', () => { it('should call addDirectory and show a success message for a single path', async () => { const newPath = path.normalize('/home/user/new-project'); - if (!addCommand?.action) throw new Error('No action'); + if (addCommand?.action == null) throw new Error('No action'); await addCommand.action(mockContext, newPath); expect(mockWorkspaceContext.addDirectory).toHaveBeenCalledWith(newPath); expect(mockContext.ui.addItem).toHaveBeenCalledWith( @@ -148,7 +148,7 @@ describe('directoryCommand', () => { it('should call addDirectory for each path and show a success message for multiple paths', async () => { const newPath1 = path.normalize('/home/user/new-project1'); const newPath2 = path.normalize('/home/user/new-project2'); - if (!addCommand?.action) throw new Error('No action'); + if (addCommand?.action == null) throw new Error('No action'); await addCommand.action(mockContext, `${newPath1},${newPath2}`); expect(mockWorkspaceContext.addDirectory).toHaveBeenCalledWith(newPath1); expect(mockWorkspaceContext.addDirectory).toHaveBeenCalledWith(newPath2); @@ -167,7 +167,7 @@ describe('directoryCommand', () => { throw error; }); const newPath = path.normalize('/home/user/invalid-project'); - if (!addCommand?.action) throw new Error('No action'); + if (addCommand?.action == null) throw new Error('No action'); await addCommand.action(mockContext, newPath); expect(mockContext.ui.addItem).toHaveBeenCalledWith( expect.objectContaining({ @@ -190,7 +190,7 @@ describe('directoryCommand', () => { }, ); - if (!addCommand?.action) throw new Error('No action'); + if (addCommand?.action == null) throw new Error('No action'); await addCommand.action(mockContext, `${validPath},${invalidPath}`); expect(mockContext.ui.addItem).toHaveBeenCalledWith( @@ -241,7 +241,7 @@ describe('directoryCommand', () => { }, }; - if (!addCommand?.action) throw new Error('No action'); + if (addCommand?.action == null) throw new Error('No action'); await addCommand.action(mockContext, untrustedPath); // Should NOT have called addDirectory @@ -284,7 +284,7 @@ describe('directoryCommand', () => { }, }; - if (!addCommand?.action) throw new Error('No action'); + if (addCommand?.action == null) throw new Error('No action'); await addCommand.action(mockContext, trustedPath); // Should have called addDirectory @@ -323,7 +323,7 @@ describe('directoryCommand', () => { }, }; - if (!addCommand?.action) throw new Error('No action'); + if (addCommand?.action == null) throw new Error('No action'); await addCommand.action(mockContext, `${trustedPath},${untrustedPath}`); // Should have called addDirectory only for trusted path @@ -381,11 +381,11 @@ describe('directoryCommand', () => { }, }; - if (!addCommand?.action) throw new Error('No action'); + if (addCommand?.action == null) throw new Error('No action'); const result = await addCommand.action(mockContext, trustedPath); // Should return early with restrictive sandbox message - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -423,7 +423,7 @@ describe('directoryCommand', () => { }, }; - if (!addCommand?.action) throw new Error('No action'); + if (addCommand?.action == null) throw new Error('No action'); await addCommand.action(mockContext, `${trustedPath},${rejectedRawPath}`); expect(mockWorkspaceContext.addDirectory).toHaveBeenCalledTimes(1); diff --git a/packages/cli/src/ui/commands/directoryCommand.tsx b/packages/cli/src/ui/commands/directoryCommand.tsx index 70b39397b0..a70e3b2763 100644 --- a/packages/cli/src/ui/commands/directoryCommand.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.tsx @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SlashCommand, CommandContext, CommandKind } from './types.js'; +import { + type SlashCommand, + type CommandContext, + CommandKind, +} from './types.js'; import { MessageType } from '../types.js'; import * as os from 'os'; import * as path from 'path'; @@ -42,7 +46,7 @@ export const directoryCommand: SlashCommand = { } = context; const [...rest] = args.split(' '); - if (!config) { + if (config == null) { addItem( { type: MessageType.ERROR, @@ -90,7 +94,7 @@ export const directoryCommand: SlashCommand = { const expandedPath = expandHomeDir(pathToAdd.trim()); // Check if path is trusted (only if folder trust is enabled) - if (trustedFolders) { + if (trustedFolders != null) { const isTrusted = trustedFolders.isPathTrusted(expandedPath); if (isTrusted === false) { errors.push( @@ -162,7 +166,6 @@ export const directoryCommand: SlashCommand = { Date.now(), ); } - return; }, }, { @@ -174,7 +177,7 @@ export const directoryCommand: SlashCommand = { ui: { addItem }, services: { config }, } = context; - if (!config) { + if (config == null) { addItem( { type: MessageType.ERROR, diff --git a/packages/cli/src/ui/commands/docsCommand.test.ts b/packages/cli/src/ui/commands/docsCommand.test.ts index 9ad3ef2ea7..7f2bff0ae3 100644 --- a/packages/cli/src/ui/commands/docsCommand.test.ts +++ b/packages/cli/src/ui/commands/docsCommand.test.ts @@ -7,7 +7,7 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import open from 'open'; import { docsCommand } from './docsCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import { MessageType } from '../types.js'; @@ -31,7 +31,7 @@ describe('docsCommand', () => { }); it("should add an info message and call 'open' in a non-sandbox environment", async () => { - if (!docsCommand.action) { + if (docsCommand.action == null) { throw new Error('docsCommand must have an action.'); } @@ -52,7 +52,7 @@ describe('docsCommand', () => { }); it('should only add an info message in a sandbox environment', async () => { - if (!docsCommand.action) { + if (docsCommand.action == null) { throw new Error('docsCommand must have an action.'); } @@ -76,7 +76,7 @@ describe('docsCommand', () => { }); it("should not open browser for 'sandbox-exec'", async () => { - if (!docsCommand.action) { + if (docsCommand.action == null) { throw new Error('docsCommand must have an action.'); } diff --git a/packages/cli/src/ui/commands/dumpcontextCommand.test.ts b/packages/cli/src/ui/commands/dumpcontextCommand.test.ts index c3fe9f9bf2..9e9e878136 100644 --- a/packages/cli/src/ui/commands/dumpcontextCommand.test.ts +++ b/packages/cli/src/ui/commands/dumpcontextCommand.test.ts @@ -6,7 +6,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { dumpcontextCommand } from './dumpcontextCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; vi.mock('../contexts/RuntimeContext.js', () => ({ @@ -38,13 +38,13 @@ describe('dumpcontextCommand', () => { setEphemeralSetting: vi.fn(), } as never); - if (!dumpcontextCommand.action) { + if (dumpcontextCommand.action == null) { throw new Error('dumpcontextCommand must have an action'); } const result = await dumpcontextCommand.action(mockContext, 'status'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Context dumping: off'), @@ -57,13 +57,13 @@ describe('dumpcontextCommand', () => { setEphemeralSetting: vi.fn(), } as never); - if (!dumpcontextCommand.action) { + if (dumpcontextCommand.action == null) { throw new Error('dumpcontextCommand must have an action'); } const result = await dumpcontextCommand.action(mockContext, 'status'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Context dumping: on'), @@ -76,13 +76,13 @@ describe('dumpcontextCommand', () => { setEphemeralSetting: vi.fn(), } as never); - if (!dumpcontextCommand.action) { + if (dumpcontextCommand.action == null) { throw new Error('dumpcontextCommand must have an action'); } const result = await dumpcontextCommand.action(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Context dumping: error'), @@ -98,14 +98,14 @@ describe('dumpcontextCommand', () => { setEphemeralSetting: mockSetEphemeralSetting, } as never); - if (!dumpcontextCommand.action) { + if (dumpcontextCommand.action == null) { throw new Error('dumpcontextCommand must have an action'); } const result = await dumpcontextCommand.action(mockContext, 'on'); expect(mockSetEphemeralSetting).toHaveBeenCalledWith('dumpcontext', 'on'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Context dumping enabled'), @@ -121,7 +121,7 @@ describe('dumpcontextCommand', () => { setEphemeralSetting: mockSetEphemeralSetting, } as never); - if (!dumpcontextCommand.action) { + if (dumpcontextCommand.action == null) { throw new Error('dumpcontextCommand must have an action'); } @@ -131,7 +131,7 @@ describe('dumpcontextCommand', () => { 'dumpcontext', 'error', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Context dumping enabled for errors'), @@ -147,7 +147,7 @@ describe('dumpcontextCommand', () => { setEphemeralSetting: mockSetEphemeralSetting, } as never); - if (!dumpcontextCommand.action) { + if (dumpcontextCommand.action == null) { throw new Error('dumpcontextCommand must have an action'); } @@ -157,7 +157,7 @@ describe('dumpcontextCommand', () => { 'dumpcontext', 'off', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Context dumping disabled'), @@ -173,7 +173,7 @@ describe('dumpcontextCommand', () => { setEphemeralSetting: mockSetEphemeralSetting, } as never); - if (!dumpcontextCommand.action) { + if (dumpcontextCommand.action == null) { throw new Error('dumpcontextCommand must have an action'); } @@ -183,7 +183,7 @@ describe('dumpcontextCommand', () => { 'dumpcontext', 'now', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining( @@ -200,13 +200,13 @@ describe('dumpcontextCommand', () => { setEphemeralSetting: vi.fn(), } as never); - if (!dumpcontextCommand.action) { + if (dumpcontextCommand.action == null) { throw new Error('dumpcontextCommand must have an action'); } const result = await dumpcontextCommand.action(mockContext, 'invalid'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: expect.stringContaining('Invalid mode'), diff --git a/packages/cli/src/ui/commands/dumpcontextCommand.ts b/packages/cli/src/ui/commands/dumpcontextCommand.ts index f234d82c68..d391c61d20 100644 --- a/packages/cli/src/ui/commands/dumpcontextCommand.ts +++ b/packages/cli/src/ui/commands/dumpcontextCommand.ts @@ -5,9 +5,9 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, CommandKind, } from './types.js'; import type { CommandArgumentSchema } from './schema/types.js'; @@ -60,7 +60,7 @@ export const dumpcontextCommand: SlashCommand = { kind: CommandKind.BUILT_IN, schema: dumpcontextSchema, action: async ( - context: CommandContext, + _context: CommandContext, args: string, ): Promise => { try { diff --git a/packages/cli/src/ui/commands/editorCommand.test.ts b/packages/cli/src/ui/commands/editorCommand.test.ts index f2f53fec05..211fb83637 100644 --- a/packages/cli/src/ui/commands/editorCommand.test.ts +++ b/packages/cli/src/ui/commands/editorCommand.test.ts @@ -11,13 +11,13 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js describe('editorCommand', () => { it('should return a dialog action to open the editor dialog', () => { - if (!editorCommand.action) { + if (editorCommand.action == null) { throw new Error('The editor command must have an action.'); } const mockContext = createMockCommandContext(); const result = editorCommand.action(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'dialog', dialog: 'editor', }); diff --git a/packages/cli/src/ui/commands/extensionsCommand.test.ts b/packages/cli/src/ui/commands/extensionsCommand.test.ts index 518e109ef2..7f54b13874 100644 --- a/packages/cli/src/ui/commands/extensionsCommand.test.ts +++ b/packages/cli/src/ui/commands/extensionsCommand.test.ts @@ -8,9 +8,9 @@ import type { GeminiCLIExtension } from '@vybestack/llxprt-code-core'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import { MessageType } from '../types.js'; import { extensionsCommand } from './extensionsCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { type ExtensionUpdateAction } from '../state/extensions.js'; +import type { ExtensionUpdateAction } from '../state/extensions.js'; vi.mock('../../config/extensions/update.js', () => ({ updateExtension: vi.fn(), @@ -45,7 +45,8 @@ describe('extensionsCommand', () => { mockGetExtensions.mockReturnValue([ { name: 'test-ext', version: '1.0.0' }, ]); - if (!extensionsCommand.action) throw new Error('Action not defined'); + if (extensionsCommand.action == null) + throw new Error('Action not defined'); await extensionsCommand.action(mockContext, ''); expect(mockContext.ui.addItem).toHaveBeenCalledWith({ @@ -56,7 +57,8 @@ describe('extensionsCommand', () => { it('should show a message if no extensions are installed', async () => { mockGetExtensions.mockReturnValue([]); - if (!extensionsCommand.action) throw new Error('Action not defined'); + if (extensionsCommand.action == null) + throw new Error('Action not defined'); await extensionsCommand.action(mockContext, ''); expect(mockContext.ui.addItem).toHaveBeenCalledWith( @@ -74,7 +76,7 @@ describe('extensionsCommand', () => { (cmd) => cmd.name === 'update', )?.action; - if (!updateAction) { + if (updateAction == null) { throw new Error('Update action not found'); } @@ -266,7 +268,7 @@ describe('extensionsCommand', () => { (cmd) => cmd.name === 'update', )?.completion; - if (!updateCompletion) { + if (updateCompletion == null) { throw new Error('Update completion not found'); } @@ -333,7 +335,7 @@ describe('extensionsCommand', () => { ])('$description', async ({ extensions, partialArg, expected }) => { mockGetExtensions.mockReturnValue(extensions); const suggestions = await updateCompletion(mockContext, partialArg); - expect(suggestions).toEqual(expected); + expect(suggestions).toStrictEqual(expected); }); }); }); diff --git a/packages/cli/src/ui/commands/extensionsCommand.ts b/packages/cli/src/ui/commands/extensionsCommand.ts index bbf8e8cb12..c2153afe21 100644 --- a/packages/cli/src/ui/commands/extensionsCommand.ts +++ b/packages/cli/src/ui/commands/extensionsCommand.ts @@ -34,9 +34,10 @@ function showMessageIfNoExtensions( } async function listAction(context: CommandContext) { - const extensions = context.services.config - ? listExtensions(context.services.config) - : []; + const extensions = + context.services.config != null + ? listExtensions(context.services.config) + : []; // Check if extensions are disabled by admin if (extensions.length === 0) { @@ -88,9 +89,10 @@ function updateAction(context: CommandContext, args: string): Promise { (resolve) => (resolveUpdateComplete = resolve), ); - const extensions = context.services.config - ? listExtensions(context.services.config) - : []; + const extensions = + context.services.config != null + ? listExtensions(context.services.config) + : []; if (showMessageIfNoExtensions(context, extensions)) { return Promise.resolve(); @@ -136,7 +138,7 @@ function updateAction(context: CommandContext, args: string): Promise { const extension = extensions.find( (extension) => extension.name === name, ); - if (!extension) { + if (extension == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -144,7 +146,6 @@ function updateAction(context: CommandContext, args: string): Promise { }, Date.now(), ); - continue; } } } @@ -175,9 +176,10 @@ const updateExtensionsCommand: SlashCommand = { kind: CommandKind.BUILT_IN, action: updateAction, completion: async (context, partialArg) => { - const extensions = context.services.config - ? listExtensions(context.services.config) - : []; + const extensions = + context.services.config != null + ? listExtensions(context.services.config) + : []; const extensionNames = extensions.map((ext) => ext.name); const suggestions = extensionNames.filter((name) => name.startsWith(partialArg), @@ -196,7 +198,7 @@ async function restartAction( args: string, ): Promise { const extensionLoader = context.services.config?.getExtensionLoader(); - if (!extensionLoader) { + if (extensionLoader == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -229,7 +231,7 @@ async function restartAction( let extensionsToRestart = extensionLoader .getExtensions() .filter((extension: GeminiCLIExtension) => extension.isActive); - if (names) { + if (names != null) { extensionsToRestart = extensionsToRestart.filter( (extension: GeminiCLIExtension) => names.includes(extension.name), ); @@ -310,9 +312,10 @@ async function completeExtensions( context: CommandContext, partialArg: string, ): Promise { - let extensions = context.services.config - ? listExtensions(context.services.config) - : []; + let extensions = + context.services.config != null + ? listExtensions(context.services.config) + : []; // Filter by active state based on the command if (context.invocation?.name === 'restart') { @@ -338,7 +341,7 @@ async function installAction( const extensionLoader = context.services.config?.getExtensionLoader(); // Check if extension reloading is enabled - if (!extensionLoader) { + if (extensionLoader == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -427,7 +430,7 @@ async function uninstallAction( ): Promise { const extensionLoader = context.services.config?.getExtensionLoader(); - if (!extensionLoader) { + if (extensionLoader == null) { context.ui.addItem( { type: MessageType.ERROR, diff --git a/packages/cli/src/ui/commands/helpCommand.test.ts b/packages/cli/src/ui/commands/helpCommand.test.ts index 44dc9f17e8..96e9980705 100644 --- a/packages/cli/src/ui/commands/helpCommand.test.ts +++ b/packages/cli/src/ui/commands/helpCommand.test.ts @@ -6,7 +6,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { helpCommand } from './helpCommand'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { MessageType } from '../types.js'; describe('helpCommand', () => { diff --git a/packages/cli/src/ui/commands/helpCommand.ts b/packages/cli/src/ui/commands/helpCommand.ts index 6ffaa3f0d2..3231048f31 100644 --- a/packages/cli/src/ui/commands/helpCommand.ts +++ b/packages/cli/src/ui/commands/helpCommand.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CommandKind, SlashCommand } from './types.js'; +import { CommandKind, type SlashCommand } from './types.js'; import { MessageType } from '../types.js'; export const helpCommand: SlashCommand = { diff --git a/packages/cli/src/ui/commands/hooksCommand.test.ts b/packages/cli/src/ui/commands/hooksCommand.test.ts index c041a46396..7c3f2931d1 100644 --- a/packages/cli/src/ui/commands/hooksCommand.test.ts +++ b/packages/cli/src/ui/commands/hooksCommand.test.ts @@ -476,7 +476,7 @@ describe('hooksCommand', () => { const completions = await enableCmd.completion!(context, 'hook'); - expect(completions).toEqual(['hook1', 'hook2', 'hook3']); + expect(completions).toStrictEqual(['hook1', 'hook2', 'hook3']); }); it('should filter completions by partial arg', async () => { @@ -486,7 +486,7 @@ describe('hooksCommand', () => { const completions = await enableCmd.completion!(context, 'hook1'); - expect(completions).toEqual(['hook1']); + expect(completions).toStrictEqual(['hook1']); }); it('should return empty array if config not loaded', async () => { @@ -502,7 +502,7 @@ describe('hooksCommand', () => { const completions = await enableCmd.completion!(contextNoConfig, 'hook'); - expect(completions).toEqual([]); + expect(completions).toStrictEqual([]); }); }); diff --git a/packages/cli/src/ui/commands/hooksCommand.ts b/packages/cli/src/ui/commands/hooksCommand.ts index 3e98e3b548..412c1fd127 100644 --- a/packages/cli/src/ui/commands/hooksCommand.ts +++ b/packages/cli/src/ui/commands/hooksCommand.ts @@ -10,14 +10,14 @@ import { CommandKind, } from './types.js'; import { MessageType, type HistoryItemHooksList } from '../types.js'; -import { type HookRegistryEntry } from '@vybestack/llxprt-code-core'; +import type { HookRegistryEntry } from '@vybestack/llxprt-code-core'; /** * List all registered hooks */ async function listHooks(context: CommandContext): Promise { const { config } = context.services; - if (!config) { + if (config == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -29,7 +29,7 @@ async function listHooks(context: CommandContext): Promise { } const hookSystem = config.getHookSystem(); - if (!hookSystem) { + if (hookSystem == null) { context.ui.addItem( { type: MessageType.INFO, @@ -60,7 +60,7 @@ async function enableHook( hookName: string, ): Promise { const { config } = context.services; - if (!config) { + if (config == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -72,7 +72,7 @@ async function enableHook( } const hookSystem = config.getHookSystem(); - if (!hookSystem) { + if (hookSystem == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -92,7 +92,7 @@ async function enableHook( (entry) => hookRegistry.getHookName(entry) === hookName, ); - if (!matchingHook) { + if (matchingHook == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -130,7 +130,7 @@ async function disableHook( hookName: string, ): Promise { const { config } = context.services; - if (!config) { + if (config == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -142,7 +142,7 @@ async function disableHook( } const hookSystem = config.getHookSystem(); - if (!hookSystem) { + if (hookSystem == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -162,7 +162,7 @@ async function disableHook( (entry: HookRegistryEntry) => hookRegistry.getHookName(entry) === hookName, ); - if (!matchingHook) { + if (matchingHook == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -197,7 +197,7 @@ async function disableHook( */ async function enableAllHooks(context: CommandContext): Promise { const { config } = context.services; - if (!config) { + if (config == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -209,7 +209,7 @@ async function enableAllHooks(context: CommandContext): Promise { } const hookSystem = config.getHookSystem(); - if (!hookSystem) { + if (hookSystem == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -265,7 +265,7 @@ async function enableAllHooks(context: CommandContext): Promise { */ async function disableAllHooks(context: CommandContext): Promise { const { config } = context.services; - if (!config) { + if (config == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -277,7 +277,7 @@ async function disableAllHooks(context: CommandContext): Promise { } const hookSystem = config.getHookSystem(); - if (!hookSystem) { + if (hookSystem == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -335,12 +335,12 @@ async function completeHookNames( partialArg: string, ): Promise { const { config } = context.services; - if (!config) { + if (config == null) { return []; } const hookSystem = config.getHookSystem(); - if (!hookSystem) { + if (hookSystem == null) { return []; } @@ -455,7 +455,7 @@ export const hooksCommand: SlashCommand = { disableAllCommand, ].find((cmd) => cmd.name === subCommandName); - if (subCommand && subCommand.action) { + if (subCommand?.action != null) { await subCommand.action(context, subArgs); } else { await listHooks(context); diff --git a/packages/cli/src/ui/commands/ideCommand.test.ts b/packages/cli/src/ui/commands/ideCommand.test.ts index 8711221b66..5b7c95fe4f 100644 --- a/packages/cli/src/ui/commands/ideCommand.test.ts +++ b/packages/cli/src/ui/commands/ideCommand.test.ts @@ -5,7 +5,7 @@ */ import { - MockInstance, + type MockInstance, vi, describe, it, @@ -14,7 +14,7 @@ import { afterEach, } from 'vitest'; import { ideCommand } from './ideCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { type Config, IDE_DEFINITIONS } from '@vybestack/llxprt-code-core'; import * as core from '@vybestack/llxprt-code-core'; @@ -147,7 +147,7 @@ describe('ideCommand', () => { (c) => c.name === 'status', )!.action!(mockContext, ''); expect(mockGetConnectionStatus).toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: '[CONNECTED] Connected to VS Code', @@ -163,7 +163,7 @@ describe('ideCommand', () => { (c) => c.name === 'status', )!.action!(mockContext, ''); expect(mockGetConnectionStatus).toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: `[CONNECTING] Connecting...`, @@ -178,7 +178,7 @@ describe('ideCommand', () => { (c) => c.name === 'status', )!.action!(mockContext, ''); expect(mockGetConnectionStatus).toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: `[DISCONNECTED] Disconnected`, @@ -196,7 +196,7 @@ describe('ideCommand', () => { (c) => c.name === 'status', )!.action!(mockContext, ''); expect(mockGetConnectionStatus).toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: `[DISCONNECTED] Disconnected: ${details}`, diff --git a/packages/cli/src/ui/commands/ideCommand.ts b/packages/cli/src/ui/commands/ideCommand.ts index 437c1e6fca..824309c1ac 100644 --- a/packages/cli/src/ui/commands/ideCommand.ts +++ b/packages/cli/src/ui/commands/ideCommand.ts @@ -6,19 +6,19 @@ import * as path from 'node:path'; import { - Config, + type Config, IDEConnectionStatus, IDE_DEFINITIONS, getIdeInstaller, - IdeClient, + type IdeClient, type File, ideContext, LLXPRT_CODE_COMPANION_EXTENSION_NAME, } from '@vybestack/llxprt-code-core'; import { - CommandContext, - SlashCommand, - SlashCommandActionReturn, + type CommandContext, + type SlashCommand, + type SlashCommandActionReturn, CommandKind, } from './types.js'; import { SettingScope } from '../../config/settings.js'; @@ -61,7 +61,7 @@ async function getIdeStatusMessageWithFiles(ideClient: IdeClient): Promise<{ const context = ideContext.getIdeContext(); const openFiles = context?.workspaceState?.openFiles; - if (openFiles && openFiles.length > 0) { + if (openFiles != null && openFiles.length > 0) { content += formatFileList(openFiles); } } catch (_e) { @@ -91,15 +91,15 @@ async function getIdeStatusMessageWithFiles(ideClient: IdeClient): Promise<{ } export const ideCommand = (config: Config | null): SlashCommand | null => { - if (!config) { + if (config == null) { return null; } const ideClient = config.getIdeClient(); - if (!ideClient) { + if (ideClient == null) { return null; } const currentIDE = ideClient.getCurrentIde(); - if (!currentIDE) { + if (currentIDE == null) { return { name: 'ide', description: 'manage IDE integration', @@ -147,7 +147,7 @@ export const ideCommand = (config: Config | null): SlashCommand | null => { autoExecute: true, action: async (context) => { const installer = getIdeInstaller(currentIDE); - if (!installer) { + if (installer == null) { context.ui.addItem( { type: 'error', diff --git a/packages/cli/src/ui/commands/initCommand.test.ts b/packages/cli/src/ui/commands/initCommand.test.ts index 4578a10dd3..652f2d0cbe 100644 --- a/packages/cli/src/ui/commands/initCommand.test.ts +++ b/packages/cli/src/ui/commands/initCommand.test.ts @@ -9,7 +9,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { initCommand } from './initCommand.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; // Mock the 'fs' module vi.mock('fs', async () => { @@ -51,7 +51,7 @@ describe('initCommand', () => { const result = await initCommand.action!(mockContext, ''); // Assert: Check for the correct informational message - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -98,7 +98,7 @@ describe('initCommand', () => { const result = await initCommand.action!(noConfigContext, ''); // Assert: Check for the correct error message - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Configuration not available.', diff --git a/packages/cli/src/ui/commands/initCommand.ts b/packages/cli/src/ui/commands/initCommand.ts index 520a692060..01eefbe95c 100644 --- a/packages/cli/src/ui/commands/initCommand.ts +++ b/packages/cli/src/ui/commands/initCommand.ts @@ -8,9 +8,9 @@ import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; import { - CommandContext, - SlashCommand, - SlashCommandActionReturn, + type CommandContext, + type SlashCommand, + type SlashCommandActionReturn, CommandKind, } from './types.js'; import { PromptService, debugLogger } from '@vybestack/llxprt-code-core'; @@ -96,7 +96,7 @@ export const initCommand: SlashCommand = { context: CommandContext, _args: string, ): Promise => { - if (!context.services.config) { + if (context.services.config == null) { return { type: 'message', messageType: 'error', diff --git a/packages/cli/src/ui/commands/keyCommand.subcommands.test.ts b/packages/cli/src/ui/commands/keyCommand.subcommands.test.ts index 7269333f2e..00f237c246 100644 --- a/packages/cli/src/ui/commands/keyCommand.subcommands.test.ts +++ b/packages/cli/src/ui/commands/keyCommand.subcommands.test.ts @@ -66,8 +66,8 @@ function createTestStorage( ): ProviderKeyStorage { const secureStore = new SecureStore('llxprt-code-provider-keys', { keyringLoader: async () => mockKeyring, - fallbackDir, fallbackPolicy: 'allow', + fallbackDir, }); return new ProviderKeyStorage({ secureStore }); } @@ -644,7 +644,7 @@ describe('/key — Autocomplete (R19)', () => { mockStorage = new ProviderKeyStorage({ secureStore: failStore }); const completions = await complete('/key load '); - expect(completions).toEqual([]); + expect(completions).toStrictEqual([]); }); }); diff --git a/packages/cli/src/ui/commands/keyCommand.ts b/packages/cli/src/ui/commands/keyCommand.ts index bb12904672..46ec0fd6a1 100644 --- a/packages/cli/src/ui/commands/keyCommand.ts +++ b/packages/cli/src/ui/commands/keyCommand.ts @@ -10,10 +10,10 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, - SlashCommandActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, + type SlashCommandActionReturn, CommandKind, } from './types.js'; /** @@ -97,7 +97,7 @@ async function handleSave( if (exists) { const config = context.services.config; const isInteractive = - config && 'isInteractive' in config + config != null && 'isInteractive' in config ? (config as { isInteractive: () => boolean }).isInteractive() : true; @@ -189,7 +189,7 @@ async function handleLoad( // not the raw key value. Clear auth-key/auth-keyfile to prevent // buildRuntimeProfileSnapshot from persisting the resolved secret. const config = context.services.config; - if (config) { + if (config != null) { config.setEphemeralSetting('auth-key-name', name); config.setEphemeralSetting('auth-key', undefined); config.setEphemeralSetting('auth-keyfile', undefined); @@ -198,7 +198,7 @@ async function handleLoad( const extendedContext = context as CommandContext & { checkPaymentModeChange?: () => void; }; - if (extendedContext.checkPaymentModeChange) { + if (extendedContext.checkPaymentModeChange != null) { setTimeout(extendedContext.checkPaymentModeChange, 100); } @@ -340,7 +340,7 @@ async function handleDelete( // Non-interactive check (R17.2) const config = context.services.config; const isInteractive = - config && 'isInteractive' in config + config != null && 'isInteractive' in config ? (config as { isInteractive: () => boolean }).isInteractive() : true; @@ -542,7 +542,7 @@ async function handleLegacyKeyAction( const extendedContext = context as CommandContext & { checkPaymentModeChange?: () => void; }; - if (extendedContext.checkPaymentModeChange) { + if (extendedContext.checkPaymentModeChange != null) { setTimeout(extendedContext.checkPaymentModeChange, 100); } diff --git a/packages/cli/src/ui/commands/keyfileCommand.ts b/packages/cli/src/ui/commands/keyfileCommand.ts index 476832638f..fa48647c87 100644 --- a/packages/cli/src/ui/commands/keyfileCommand.ts +++ b/packages/cli/src/ui/commands/keyfileCommand.ts @@ -5,9 +5,9 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, CommandKind, } from './types.js'; import { promises as fs } from 'fs'; @@ -111,7 +111,7 @@ export const keyfileCommand: SlashCommand = { const extendedContext = context as CommandContext & { checkPaymentModeChange?: () => void; }; - if (extendedContext.checkPaymentModeChange) { + if (extendedContext.checkPaymentModeChange != null) { setTimeout(extendedContext.checkPaymentModeChange, 100); } diff --git a/packages/cli/src/ui/commands/loggingCommand.ts b/packages/cli/src/ui/commands/loggingCommand.ts index d812080a47..c2616a2d5e 100644 --- a/packages/cli/src/ui/commands/loggingCommand.ts +++ b/packages/cli/src/ui/commands/loggingCommand.ts @@ -4,15 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; import { CommandKind, - SlashCommand, - CommandContext, - MessageActionReturn, - OpenDialogActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, + type OpenDialogActionReturn, } from './types.js'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; +import { type LoadedSettings, SettingScope } from '../../config/settings.js'; import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; @@ -330,7 +330,7 @@ const statusCommand: SlashCommand = { description: 'show current logging status', kind: CommandKind.BUILT_IN, action: async (context: CommandContext) => { - if (!context.services.config) { + if (context.services.config == null) { context.ui.addItem( { type: 'error', @@ -361,7 +361,7 @@ const enableCommand: SlashCommand = { description: 'enable conversation logging', kind: CommandKind.BUILT_IN, action: async (context: CommandContext, args: string) => { - if (!context.services.config) { + if (context.services.config == null) { context.ui.addItem( { type: 'error', @@ -393,7 +393,7 @@ const disableCommand: SlashCommand = { description: 'disable conversation logging', kind: CommandKind.BUILT_IN, action: async (context: CommandContext) => { - if (!context.services.config) { + if (context.services.config == null) { context.ui.addItem( { type: 'error', @@ -424,7 +424,7 @@ const redactionCommand: SlashCommand = { description: 'configure data redaction settings', kind: CommandKind.BUILT_IN, action: async (context: CommandContext, args: string) => { - if (!context.services.config) { + if (context.services.config == null) { context.ui.addItem( { type: 'error', @@ -456,7 +456,7 @@ const showCommand: SlashCommand = { description: 'show last N lines from conversation log (default 50)', kind: CommandKind.BUILT_IN, action: async (context: CommandContext, args: string) => { - if (!context.services.config) { + if (context.services.config == null) { context.ui.addItem( { type: 'error', @@ -500,9 +500,9 @@ const showCommand: SlashCommand = { const typeIcon = entry.type === 'request' ? '→' : '←'; let content = ''; - if (entry.type === 'request' && entry.messages) { + if (entry.type === 'request' && entry.messages != null) { const lastMessage = entry.messages[entry.messages.length - 1]; - if (lastMessage && lastMessage.content) { + if (lastMessage?.content) { content = lastMessage.content.substring(0, 100); if (lastMessage.content.length > 100) content += '...'; } diff --git a/packages/cli/src/ui/commands/logoutCommand.ts b/packages/cli/src/ui/commands/logoutCommand.ts index 75948570b1..f32f76637f 100644 --- a/packages/cli/src/ui/commands/logoutCommand.ts +++ b/packages/cli/src/ui/commands/logoutCommand.ts @@ -5,9 +5,9 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, CommandKind, } from './types.js'; import { getRuntimeApi } from '../contexts/RuntimeContext.js'; @@ -18,7 +18,7 @@ export const logoutCommand: SlashCommand = { 'logout from OAuth authentication for a provider (gemini, qwen, anthropic)', kind: CommandKind.BUILT_IN, action: async ( - context: CommandContext, + _context: CommandContext, args: string, ): Promise => { const provider = args?.trim(); @@ -35,7 +35,7 @@ export const logoutCommand: SlashCommand = { try { const oauthManager = getRuntimeApi().getCliOAuthManager(); - if (!oauthManager) { + if (oauthManager == null) { return { type: 'message', messageType: 'error', @@ -65,14 +65,13 @@ export const logoutCommand: SlashCommand = { messageType: 'info', content: `Successfully logged out of ${provider}`, }; - } else { - // User wasn't authenticated but we cleaned up any stale tokens - return { - type: 'message', - messageType: 'info', - content: `Cleaned up authentication state for ${provider} (was not authenticated)`, - }; } + // User wasn't authenticated but we cleaned up any stale tokens + return { + type: 'message', + messageType: 'info', + content: `Cleaned up authentication state for ${provider} (was not authenticated)`, + }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); diff --git a/packages/cli/src/ui/commands/lspCommand.test.ts b/packages/cli/src/ui/commands/lspCommand.test.ts index 25f07e6410..9413f0ed51 100644 --- a/packages/cli/src/ui/commands/lspCommand.test.ts +++ b/packages/cli/src/ui/commands/lspCommand.test.ts @@ -61,7 +61,7 @@ describe('lspCommand (P34)', () => { vi.clearAllMocks(); context = createMockCommandContext(); const command = lspCommand.subCommands?.find((c) => c.name === 'status'); - if (!command) { + if (command == null) { throw new Error('status command not found'); } statusCommand = command; @@ -392,7 +392,7 @@ describe('lspCommand (P34)', () => { .slice(1); const serverIds = lines.map((line) => line.trim().split(':')[0]); const sorted = [...serverIds].sort((a, b) => a.localeCompare(b)); - expect(serverIds).toEqual(sorted); + expect(serverIds).toStrictEqual(sorted); }); it('remains usable when navigationTools is false', async () => { diff --git a/packages/cli/src/ui/commands/lspCommand.ts b/packages/cli/src/ui/commands/lspCommand.ts index ed6c359ce4..61a73782c9 100644 --- a/packages/cli/src/ui/commands/lspCommand.ts +++ b/packages/cli/src/ui/commands/lspCommand.ts @@ -10,10 +10,10 @@ */ import { - SlashCommand, - CommandContext, + type SlashCommand, + type CommandContext, CommandKind, - MessageActionReturn, + type MessageActionReturn, } from './types.js'; type LspServerConfigLike = { id: string; @@ -83,7 +83,7 @@ async function statusAction( ): Promise { const config = context.services.config as unknown as LspConfigAccessor | null; - if (!config) { + if (config == null) { return { type: 'message', messageType: 'error', @@ -92,7 +92,7 @@ async function statusAction( } const lspConfig = config.getLspConfig?.(); - if (!lspConfig) { + if (lspConfig == null || lspConfig === false) { return { type: 'message', messageType: 'info', @@ -101,7 +101,7 @@ async function statusAction( } const lspClient = config.getLspServiceClient?.(); - if (!lspClient || !lspClient.isAlive()) { + if (!lspClient?.isAlive()) { const reason = lspClient?.getUnavailableReason?.() ?? 'service startup failed'; return { diff --git a/packages/cli/src/ui/commands/mcpCommand.test.ts b/packages/cli/src/ui/commands/mcpCommand.test.ts index ce47f3fb89..9a52bfdacc 100644 --- a/packages/cli/src/ui/commands/mcpCommand.test.ts +++ b/packages/cli/src/ui/commands/mcpCommand.test.ts @@ -14,8 +14,8 @@ import { getMCPDiscoveryState, DiscoveredMCPTool, } from '@vybestack/llxprt-code-core'; -import { MessageActionReturn } from './types.js'; -import { Type, CallableTool } from '@google/genai'; +import type { MessageActionReturn } from './types.js'; +import { Type, type CallableTool } from '@google/genai'; // Mock external dependencies vi.mock('open', () => ({ @@ -140,7 +140,7 @@ describe('mcpCommand', () => { const result = await mcpCommand.action!(contextWithoutConfig, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Config not loaded.', @@ -162,7 +162,7 @@ describe('mcpCommand', () => { const result = await mcpCommand.action!(contextWithNoRegistry, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Could not retrieve tool registry.', @@ -182,7 +182,7 @@ describe('mcpCommand', () => { it('should display a message with a URL when no MCP servers are configured', async () => { const result = await mcpCommand.action!(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -257,7 +257,7 @@ describe('mcpCommand', () => { const result = await mcpCommand.action!(testContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Configured MCP servers:'), @@ -386,7 +386,7 @@ describe('mcpCommand', () => { const result = await mcpCommand.action!(testContext, 'desc'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Configured MCP servers:'), @@ -448,7 +448,7 @@ describe('mcpCommand', () => { const result = await mcpCommand.action!(testContext, 'nodesc'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Configured MCP servers:'), @@ -742,7 +742,7 @@ describe('mcpCommand', () => { const result = await mcpCommand.action!(testContext, 'schema'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Configured MCP servers:'), @@ -796,7 +796,7 @@ describe('mcpCommand', () => { const result = await mcpCommand.action!(testContext, 'schema'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Configured MCP servers:'), @@ -1031,7 +1031,7 @@ describe('mcpCommand', () => { const result = await mcpCommand.action!(testContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Configured MCP servers:'), @@ -1310,7 +1310,7 @@ describe('mcpCommand', () => { ); const result = await refreshCommand!.action!(contextWithoutConfig, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Config not loaded.', @@ -1335,7 +1335,7 @@ describe('mcpCommand', () => { ); const result = await refreshCommand!.action!(contextWithNoRegistry, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Could not retrieve tool registry.', diff --git a/packages/cli/src/ui/commands/mcpCommand.ts b/packages/cli/src/ui/commands/mcpCommand.ts index 10a6908f4b..1b29bc34d4 100644 --- a/packages/cli/src/ui/commands/mcpCommand.ts +++ b/packages/cli/src/ui/commands/mcpCommand.ts @@ -5,16 +5,15 @@ */ import { - SlashCommand, - SlashCommandActionReturn, - CommandContext, + type SlashCommand, + type SlashCommandActionReturn, + type CommandContext, CommandKind, - MessageActionReturn, + type MessageActionReturn, } from './types.js'; -import { type CommandArgumentSchema } from './schema/types.js'; +import type { CommandArgumentSchema } from './schema/types.js'; import { - Config, - DiscoveredMCPPrompt, + type DiscoveredMCPPrompt, DiscoveredMCPTool, getMCPDiscoveryState, getMCPServerStatus, @@ -22,9 +21,8 @@ import { MCPServerStatus, mcpServerRequiresOAuth, getErrorMessage, - AnyDeclarativeTool, - MCPServerConfig, - DiscoveredMCPResource, + type AnyDeclarativeTool, + type MCPServerConfig, } from '@vybestack/llxprt-code-core'; import { appEvents, AppEvent } from '../../utils/events.js'; import { withFuzzyFilter } from '../utils/fuzzyFilter.js'; @@ -50,7 +48,7 @@ const mcpAuthSchema: CommandArgumentSchema = [ */ completer: withFuzzyFilter(async (ctx) => { const { config } = ctx.services; - if (!config) { + if (config == null) { return []; } @@ -70,7 +68,7 @@ const getMcpStatus = async ( showTips: boolean = false, ): Promise => { const { config } = context.services; - if (!config) { + if (config == null) { return { type: 'message', messageType: 'error', @@ -125,16 +123,7 @@ const getMcpStatus = async ( const allTools = toolRegistry.getAllTools(); const promptRegistry = config.getPromptRegistry(); - const allResources = - ( - config as Config & { - getResourceRegistry?: () => { - getAllResources: () => DiscoveredMCPResource[]; - }; - } - ) - .getResourceRegistry?.() - ?.getAllResources?.() ?? []; + const allResources = config.getResourceRegistry().getAllResources(); for (const serverName of serverNames) { const serverTools = allTools.filter((tool: AnyDeclarativeTool) => { @@ -198,7 +187,7 @@ const getMcpStatus = async ( ); const tokenStorage = new MCPOAuthTokenStorage(); const hasToken = await tokenStorage.getCredentials(serverName); - if (hasToken) { + if (hasToken != null) { const isExpired = MCPOAuthTokenStorage.isTokenExpired(hasToken.token); if (isExpired) { message += ` ${COLOR_YELLOW}(OAuth token expired)${RESET_COLOR}`; @@ -292,7 +281,7 @@ const getMcpStatus = async ( } const parameters = tool.schema.parametersJsonSchema ?? tool.schema.parameters; - if (showSchema && parameters) { + if (showSchema && parameters != null) { // Prefix the parameters in cyan message += ` ${COLOR_CYAN}Parameters:${RESET_COLOR}\n`; @@ -432,7 +421,7 @@ const authCommand: SlashCommand = { const serverName = args.trim(); const { config } = context.services; - if (!config) { + if (config == null) { return { type: 'message', messageType: 'error', @@ -506,7 +495,7 @@ const authCommand: SlashCommand = { const { MCPOAuthProvider } = await import('@vybestack/llxprt-code-core'); let oauthConfig = server.oauth; - if (!oauthConfig) { + if (oauthConfig == null) { oauthConfig = { enabled: false }; } @@ -528,7 +517,7 @@ const authCommand: SlashCommand = { // Trigger tool re-discovery to pick up authenticated server const mcpClientManager = config.getMcpClientManager(); - if (mcpClientManager) { + if (mcpClientManager != null) { context.ui.addItem( { type: 'info', @@ -599,7 +588,7 @@ const refreshCommand: SlashCommand = { context: CommandContext, ): Promise => { const { config } = context.services; - if (!config) { + if (config == null) { return { type: 'message', messageType: 'error', diff --git a/packages/cli/src/ui/commands/memoryCommand.test.ts b/packages/cli/src/ui/commands/memoryCommand.test.ts index 8a7a1a8c08..679dce7387 100644 --- a/packages/cli/src/ui/commands/memoryCommand.test.ts +++ b/packages/cli/src/ui/commands/memoryCommand.test.ts @@ -10,7 +10,7 @@ import { memoryCommand } from './memoryCommand.js'; import type { SlashCommand, CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import { MessageType } from '../types.js'; -import { LoadedSettings } from '../../config/settings.js'; +import type { LoadedSettings } from '../../config/settings.js'; import { getErrorMessage, type FileDiscoveryService, @@ -60,7 +60,7 @@ describe('memoryCommand', () => { const subCommand = memoryCommand.subCommands?.find( (cmd) => cmd.name === name, ); - if (!subCommand) { + if (subCommand == null) { throw new Error(`/memory ${name} command not found.`); } return subCommand; @@ -88,7 +88,7 @@ describe('memoryCommand', () => { }); it('should display a message if memory is empty', async () => { - if (!showCommand.action) throw new Error('Command has no action'); + if (showCommand.action == null) throw new Error('Command has no action'); mockGetUserMemory.mockReturnValue(''); mockGetLlxprtMdFileCount.mockReturnValue(0); @@ -105,7 +105,7 @@ describe('memoryCommand', () => { }); it('should display the memory content and file count if it exists', async () => { - if (!showCommand.action) throw new Error('Command has no action'); + if (showCommand.action == null) throw new Error('Command has no action'); const memoryContent = 'This is a test memory.'; @@ -133,10 +133,10 @@ describe('memoryCommand', () => { }); it('should return an error message if no arguments are provided', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const result = addCommand.action(mockContext, ' '); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -147,10 +147,10 @@ describe('memoryCommand', () => { }); it('should return an error message if only scope is provided without text', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const result = addCommand.action(mockContext, 'global'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -161,10 +161,10 @@ describe('memoryCommand', () => { }); it('should return an error message if only "project" is provided without text', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const result = addCommand.action(mockContext, 'project'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -175,7 +175,7 @@ describe('memoryCommand', () => { }); it('should default to global scope when no scope keyword is provided', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const fact = 'remember this'; const result = addCommand.action(mockContext, ` ${fact} `); @@ -188,7 +188,7 @@ describe('memoryCommand', () => { expect.any(Number), ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'tool', toolName: 'save_memory', toolArgs: { fact }, @@ -196,7 +196,7 @@ describe('memoryCommand', () => { }); it('should return a tool action with scope "global" when "global" is specified', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const fact = 'remember this globally'; const result = addCommand.action(mockContext, `global ${fact}`); @@ -209,7 +209,7 @@ describe('memoryCommand', () => { expect.any(Number), ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'tool', toolName: 'save_memory', toolArgs: { fact, scope: 'global' }, @@ -217,7 +217,7 @@ describe('memoryCommand', () => { }); it('should return a tool action with scope "project" when "project" is specified', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const fact = 'remember this for the project'; const result = addCommand.action(mockContext, `project ${fact}`); @@ -230,7 +230,7 @@ describe('memoryCommand', () => { expect.any(Number), ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'tool', toolName: 'save_memory', toolArgs: { fact, scope: 'project' }, @@ -238,7 +238,7 @@ describe('memoryCommand', () => { }); it('should handle uppercase scope keywords', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const fact = 'test fact'; const result = addCommand.action(mockContext, `PROJECT ${fact}`); @@ -251,7 +251,7 @@ describe('memoryCommand', () => { expect.any(Number), ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'tool', toolName: 'save_memory', toolArgs: { fact, scope: 'project' }, @@ -259,7 +259,7 @@ describe('memoryCommand', () => { }); it('should handle mixed case scope keywords', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const fact = 'test fact'; const result = addCommand.action(mockContext, `Global ${fact}`); @@ -272,7 +272,7 @@ describe('memoryCommand', () => { expect.any(Number), ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'tool', toolName: 'save_memory', toolArgs: { fact, scope: 'global' }, @@ -280,7 +280,7 @@ describe('memoryCommand', () => { }); it('should treat non-scope first words as part of the fact', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const fact = 'globally important fact'; const result = addCommand.action(mockContext, fact); @@ -293,7 +293,7 @@ describe('memoryCommand', () => { expect.any(Number), ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'tool', toolName: 'save_memory', toolArgs: { fact }, @@ -301,11 +301,11 @@ describe('memoryCommand', () => { }); it('should return error when core.project is provided without content', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const result = addCommand.action(mockContext, 'core.project'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: expect.stringContaining('Usage'), @@ -313,11 +313,11 @@ describe('memoryCommand', () => { }); it('should return error when core.global is provided without content', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); const result = addCommand.action(mockContext, 'core.global'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: expect.stringContaining('Usage'), @@ -325,7 +325,7 @@ describe('memoryCommand', () => { }); it('should handle core.project scope and write directly (async)', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); mockContext = createMockCommandContext({ services: { @@ -345,7 +345,7 @@ describe('memoryCommand', () => { }); it('should handle core.global scope and write directly (async)', () => { - if (!addCommand.action) throw new Error('Command has no action'); + if (addCommand.action == null) throw new Error('Command has no action'); mockContext = createMockCommandContext({ services: { @@ -422,7 +422,8 @@ describe('memoryCommand', () => { }); it('should display success message when memory is refreshed with content', async () => { - if (!refreshCommand.action) throw new Error('Command has no action'); + if (refreshCommand.action == null) + throw new Error('Command has no action'); const refreshResult: LoadServerHierarchicalMemoryResponse = { memoryContent: 'new memory content', @@ -465,7 +466,8 @@ describe('memoryCommand', () => { }); it('should display success message when memory is refreshed with no content', async () => { - if (!refreshCommand.action) throw new Error('Command has no action'); + if (refreshCommand.action == null) + throw new Error('Command has no action'); const refreshResult = { memoryContent: '', fileCount: 0, filePaths: [] }; mockLoadHierarchicalLlxprtMemory.mockResolvedValue(refreshResult); @@ -487,7 +489,8 @@ describe('memoryCommand', () => { }); it('should display an error message if refreshing fails', async () => { - if (!refreshCommand.action) throw new Error('Command has no action'); + if (refreshCommand.action == null) + throw new Error('Command has no action'); const error = new Error('Failed to read memory files.'); mockLoadHierarchicalLlxprtMemory.mockRejectedValue(error); @@ -511,7 +514,8 @@ describe('memoryCommand', () => { }); it('should not throw if config service is unavailable', async () => { - if (!refreshCommand.action) throw new Error('Command has no action'); + if (refreshCommand.action == null) + throw new Error('Command has no action'); const nullConfigContext = createMockCommandContext({ services: { config: null }, @@ -533,7 +537,8 @@ describe('memoryCommand', () => { }); it('should use ContextManager.refresh() when JIT context is enabled', async () => { - if (!refreshCommand.action) throw new Error('Command has no action'); + if (refreshCommand.action == null) + throw new Error('Command has no action'); const mockRefresh = vi.fn().mockResolvedValue(undefined); const jitConfig = { @@ -592,7 +597,7 @@ describe('memoryCommand', () => { }); it('should display a message if no LLXPRT.md files are found', async () => { - if (!listCommand.action) throw new Error('Command has no action'); + if (listCommand.action == null) throw new Error('Command has no action'); mockGetLlxprtMdFilePaths.mockReturnValue([]); @@ -608,7 +613,7 @@ describe('memoryCommand', () => { }); it('should display the file count and paths if they exist', async () => { - if (!listCommand.action) throw new Error('Command has no action'); + if (listCommand.action == null) throw new Error('Command has no action'); const filePaths = ['/path/one/LLXPRT.md', '/path/two/LLXPRT.md']; mockGetLlxprtMdFilePaths.mockReturnValue(filePaths); diff --git a/packages/cli/src/ui/commands/memoryCommand.ts b/packages/cli/src/ui/commands/memoryCommand.ts index c254e77a4d..c9eda2cd2d 100644 --- a/packages/cli/src/ui/commands/memoryCommand.ts +++ b/packages/cli/src/ui/commands/memoryCommand.ts @@ -217,13 +217,13 @@ export const memoryCommand: SlashCommand = { try { const config = context.services.config; const settings = context.services.settings; - if (config) { + if (config != null) { let memoryContent = ''; let fileCount = 0; if (config.isJitContextEnabled()) { const contextManager = config.getContextManager(); - if (contextManager) { + if (contextManager != null) { await contextManager.refresh(); } memoryContent = config.getUserMemory(); diff --git a/packages/cli/src/ui/commands/modelCommand.ts b/packages/cli/src/ui/commands/modelCommand.ts index 75e5003d08..52c8d62879 100644 --- a/packages/cli/src/ui/commands/modelCommand.ts +++ b/packages/cli/src/ui/commands/modelCommand.ts @@ -5,12 +5,12 @@ */ import { - SlashCommand, - CommandContext, - OpenDialogActionReturn, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type OpenDialogActionReturn, + type MessageActionReturn, CommandKind, - ModelsDialogData, + type ModelsDialogData, } from './types.js'; import { getRuntimeApi } from '../contexts/RuntimeContext.js'; diff --git a/packages/cli/src/ui/commands/mouseCommand.test.ts b/packages/cli/src/ui/commands/mouseCommand.test.ts index 8e80eb0d96..fb76450a30 100644 --- a/packages/cli/src/ui/commands/mouseCommand.test.ts +++ b/packages/cli/src/ui/commands/mouseCommand.test.ts @@ -12,7 +12,7 @@ import { mouseCommand } from './mouseCommand.js'; describe('mouseCommand', () => { const runMouseCommand = async (args: string) => { const action = mouseCommand.action; - if (!action) { + if (action == null) { throw new Error('mouseCommand must have an action.'); } @@ -28,7 +28,7 @@ describe('mouseCommand', () => { const result = await runMouseCommand('on'); expect(isMouseEventsActive()).toBe(true); - expect(result).toEqual( + expect(result).toStrictEqual( expect.objectContaining({ type: 'message', messageType: 'info', @@ -42,7 +42,7 @@ describe('mouseCommand', () => { const result = await runMouseCommand('off'); expect(isMouseEventsActive()).toBe(false); - expect(result).toEqual( + expect(result).toStrictEqual( expect.objectContaining({ type: 'message', messageType: 'info', @@ -54,7 +54,7 @@ describe('mouseCommand', () => { const result = await runMouseCommand(''); expect(isMouseEventsActive()).toBe(true); - expect(result).toEqual( + expect(result).toStrictEqual( expect.objectContaining({ type: 'message', messageType: 'info', @@ -66,7 +66,7 @@ describe('mouseCommand', () => { const result = await runMouseCommand('maybe'); expect(isMouseEventsActive()).toBe(false); - expect(result).toEqual( + expect(result).toStrictEqual( expect.objectContaining({ type: 'message', messageType: 'error', diff --git a/packages/cli/src/ui/commands/mouseCommand.ts b/packages/cli/src/ui/commands/mouseCommand.ts index 6f790d66a9..8b1ca64678 100644 --- a/packages/cli/src/ui/commands/mouseCommand.ts +++ b/packages/cli/src/ui/commands/mouseCommand.ts @@ -38,8 +38,7 @@ export const mouseCommand: SlashCommand = { } const currentlyActive = isMouseEventsActive(); - const nextActive = - mode === 'toggle' ? !currentlyActive : mode === 'on' ? true : false; + const nextActive = mode === 'toggle' ? !currentlyActive : mode === 'on'; setMouseEventsActive(nextActive); diff --git a/packages/cli/src/ui/commands/permissionsCommand.test.ts b/packages/cli/src/ui/commands/permissionsCommand.test.ts index 75a356f51d..d018bdef79 100644 --- a/packages/cli/src/ui/commands/permissionsCommand.test.ts +++ b/packages/cli/src/ui/commands/permissionsCommand.test.ts @@ -79,7 +79,7 @@ describe('permissionsCommand', () => { const mockContext = createMockContext(); const result = permissionsCommand.action?.(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'dialog', dialog: 'permissions', }); @@ -89,7 +89,7 @@ describe('permissionsCommand', () => { const mockContext = createMockContext(); const result = permissionsCommand.action?.(mockContext, ' '); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'dialog', dialog: 'permissions', }); @@ -108,7 +108,7 @@ describe('permissionsCommand', () => { path.normalize(targetPath), 'TRUST_FOLDER', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Trust level set to TRUST_FOLDER'), @@ -126,7 +126,7 @@ describe('permissionsCommand', () => { path.normalize(targetPath), 'TRUST_PARENT', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Trust level set to TRUST_PARENT'), @@ -144,7 +144,7 @@ describe('permissionsCommand', () => { path.normalize(targetPath), 'DO_NOT_TRUST', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Trust level set to DO_NOT_TRUST'), @@ -158,7 +158,7 @@ describe('permissionsCommand', () => { const result = permissionsCommand.action?.(mockContext, args); expect(mockSetValue).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: expect.stringContaining('Invalid trust level'), @@ -172,7 +172,7 @@ describe('permissionsCommand', () => { const result = permissionsCommand.action?.(mockContext, args); expect(mockSetValue).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: expect.stringContaining('path is required'), @@ -190,7 +190,7 @@ describe('permissionsCommand', () => { path.normalize(targetPath), 'TRUST_FOLDER', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Trust level set to TRUST_FOLDER'), @@ -207,7 +207,7 @@ describe('permissionsCommand', () => { expect.any(String), 'TRUST_FOLDER', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('Trust level set to TRUST_FOLDER'), @@ -224,7 +224,7 @@ describe('permissionsCommand', () => { const result = permissionsCommand.action?.(mockContext, args); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: expect.stringContaining('Failed to save trust settings'), diff --git a/packages/cli/src/ui/commands/permissionsCommand.ts b/packages/cli/src/ui/commands/permissionsCommand.ts index 8767fa9675..8a37418d5e 100644 --- a/packages/cli/src/ui/commands/permissionsCommand.ts +++ b/packages/cli/src/ui/commands/permissionsCommand.ts @@ -7,9 +7,9 @@ import * as path from 'node:path'; import { CommandKind, - OpenDialogActionReturn, - SlashCommand, - MessageActionReturn, + type OpenDialogActionReturn, + type SlashCommand, + type MessageActionReturn, } from './types.js'; import { loadTrustedFolders, TrustLevel } from '../../config/trustedFolders.js'; diff --git a/packages/cli/src/ui/commands/policiesCommand.test.ts b/packages/cli/src/ui/commands/policiesCommand.test.ts index 7d0434aa3f..61d5c8e310 100644 --- a/packages/cli/src/ui/commands/policiesCommand.test.ts +++ b/packages/cli/src/ui/commands/policiesCommand.test.ts @@ -6,7 +6,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { policiesCommand } from './policiesCommand.js'; -import { type CommandContext, type MessageActionReturn } from './types.js'; +import type { CommandContext, MessageActionReturn } from './types.js'; import { PolicyEngine, PolicyDecision, @@ -32,7 +32,7 @@ describe('policiesCommand', () => { }); it('should return an error when config is not available', () => { - if (!policiesCommand.action) { + if (policiesCommand.action == null) { throw new Error('Policies command has no action'); } @@ -41,7 +41,7 @@ describe('policiesCommand', () => { '', ) as MessageActionReturn; - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Configuration not available', @@ -59,7 +59,7 @@ describe('policiesCommand', () => { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), } as unknown as CommandContext['services']['config']; - if (!policiesCommand.action) { + if (policiesCommand.action == null) { throw new Error('Policies command has no action'); } @@ -68,7 +68,7 @@ describe('policiesCommand', () => { '', ) as MessageActionReturn; - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'No policy rules configured.', @@ -104,7 +104,7 @@ describe('policiesCommand', () => { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), } as unknown as CommandContext['services']['config']; - if (!policiesCommand.action) { + if (policiesCommand.action == null) { throw new Error('Policies command has no action'); } @@ -152,7 +152,7 @@ describe('policiesCommand', () => { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), } as unknown as CommandContext['services']['config']; - if (!policiesCommand.action) { + if (policiesCommand.action == null) { throw new Error('Policies command has no action'); } @@ -186,7 +186,7 @@ describe('policiesCommand', () => { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), } as unknown as CommandContext['services']['config']; - if (!policiesCommand.action) { + if (policiesCommand.action == null) { throw new Error('Policies command has no action'); } @@ -220,7 +220,7 @@ describe('policiesCommand', () => { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), } as unknown as CommandContext['services']['config']; - if (!policiesCommand.action) { + if (policiesCommand.action == null) { throw new Error('Policies command has no action'); } @@ -253,7 +253,7 @@ describe('policiesCommand', () => { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), } as unknown as CommandContext['services']['config']; - if (!policiesCommand.action) { + if (policiesCommand.action == null) { throw new Error('Policies command has no action'); } @@ -289,7 +289,7 @@ describe('policiesCommand', () => { getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine), } as unknown as CommandContext['services']['config']; - if (!policiesCommand.action) { + if (policiesCommand.action == null) { throw new Error('Policies command has no action'); } diff --git a/packages/cli/src/ui/commands/policiesCommand.ts b/packages/cli/src/ui/commands/policiesCommand.ts index fbc02d8155..46e20b1043 100644 --- a/packages/cli/src/ui/commands/policiesCommand.ts +++ b/packages/cli/src/ui/commands/policiesCommand.ts @@ -36,9 +36,8 @@ function getTierBand(priority: number): string { return 'Tier 2 (User-defined)'; } else if (priority >= 1.0) { return 'Tier 1 (Defaults)'; - } else { - return 'Tier 0 (System)'; } + return 'Tier 0 (System)'; } /** diff --git a/packages/cli/src/ui/commands/privacyCommand.ts b/packages/cli/src/ui/commands/privacyCommand.ts index bfeaf39cd2..d7724c37bb 100644 --- a/packages/cli/src/ui/commands/privacyCommand.ts +++ b/packages/cli/src/ui/commands/privacyCommand.ts @@ -6,9 +6,9 @@ import { CommandKind, - SlashCommand, - CommandContext, - OpenDialogActionReturn, + type SlashCommand, + type CommandContext, + type OpenDialogActionReturn, } from './types.js'; /** diff --git a/packages/cli/src/ui/commands/profileCommand.ts b/packages/cli/src/ui/commands/profileCommand.ts index 263a44c8ec..e1b003110a 100644 --- a/packages/cli/src/ui/commands/profileCommand.ts +++ b/packages/cli/src/ui/commands/profileCommand.ts @@ -5,17 +5,14 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, - OpenDialogActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, + type OpenDialogActionReturn, CommandKind, } from './types.js'; import { SettingScope } from '../../config/settings.js'; -import { - type CommandArgumentSchema, - type CompleterFn, -} from './schema/types.js'; +import type { CommandArgumentSchema, CompleterFn } from './schema/types.js'; import { getRuntimeApi } from '../contexts/RuntimeContext.js'; import { DebugLogger, @@ -253,7 +250,7 @@ const saveCommand: SlashCommand = { _context: CommandContext, args: string, ): Promise => { - const trimmedArgs = args?.trim(); + const trimmedArgs = args.trim(); if (!trimmedArgs) { return { @@ -283,11 +280,10 @@ const saveCommand: SlashCommand = { let bucketArgs: string[] = []; // Check if profile name is quoted - const profileNameMatch = parts - .slice(1) - .join(' ') - .match(/^"([^"]+)"(?:\s+(.+))?$/); - if (profileNameMatch) { + const profileNameMatch = RegExp(/^"([^"]+)"(?:\s+(.+))?$/).exec( + parts.slice(1).join(' '), + ); + if (profileNameMatch != null) { profileName = profileNameMatch[1]; if (profileNameMatch[2]) { bucketArgs = profileNameMatch[2] @@ -462,12 +458,12 @@ const saveCommand: SlashCommand = { const lbProfile = { version: 1 as const, type: 'loadbalancer' as const, - policy, profiles: selectedProfiles, provider: '', model: '', modelParams: {}, ephemeralSettings: filteredEphemerals, + policy, }; await runtime.saveLoadBalancerProfile(lbProfileName, lbProfile); @@ -510,7 +506,7 @@ const loadCommand: SlashCommand = { args: string, ): Promise => { // Parse profile name from args - const trimmedArgs = args?.trim(); + const trimmedArgs = args.trim(); if (!trimmedArgs) { // Open interactive profile selection dialog @@ -521,8 +517,9 @@ const loadCommand: SlashCommand = { } // Extract profile name - handle quoted names - const profileNameMatch = trimmedArgs.match(/^"([^"]+)"$/); - const profileName = profileNameMatch ? profileNameMatch[1] : trimmedArgs; + const profileNameMatch = RegExp(/^"([^"]+)"$/).exec(trimmedArgs); + const profileName = + profileNameMatch != null ? profileNameMatch[1] : trimmedArgs; if (!profileName) { return { @@ -559,17 +556,17 @@ const loadCommand: SlashCommand = { ); } } - const infoMessages = (result.infoMessages ?? []) + const infoMessages = result.infoMessages .map((message) => `\n- ${message}`) .join(''); - const warningMessages = (result.warnings ?? []) - .map((warning) => `\n⚠ ${warning}`) + const warningMessages = result.warnings + .map((warning) => `\n ${warning}`) .join(''); const configService = context.services.config; - if (configService) { - const providerManager = configService.getProviderManager?.(); - if (providerManager && result.providerName) { + if (configService != null) { + const providerManager = configService.getProviderManager(); + if (providerManager != null && result.providerName) { logger.debug( () => `[profile] forcing config provider manager switch to '${result.providerName}'`, @@ -594,11 +591,11 @@ const loadCommand: SlashCommand = { `[profile] failed to set provider on config manager: ${error instanceof Error ? error.message : String(error)}`, ); } - configService.setProvider?.(result.providerName); + configService.setProvider(result.providerName); } - const geminiClient = configService.getGeminiClient?.(); - if (geminiClient && typeof geminiClient.setTools === 'function') { + const geminiClient = configService.getGeminiClient(); + if (typeof geminiClient.setTools === 'function') { try { await geminiClient.setTools(); } catch (error) { @@ -627,7 +624,7 @@ const loadCommand: SlashCommand = { try { const statusAfter = runtime.getActiveProviderStatus(); context.recordingIntegration?.recordProviderSwitch( - statusAfter.providerName ?? result.providerName ?? 'unknown', + statusAfter.providerName ?? result.providerName, statusAfter.modelName ?? 'unknown', ); } catch { @@ -637,7 +634,7 @@ const loadCommand: SlashCommand = { const extendedContext = context as CommandContext & { checkPaymentModeChange?: (forcePreviousProvider?: string) => void; }; - if (extendedContext.checkPaymentModeChange) { + if (extendedContext.checkPaymentModeChange != null) { setTimeout( () => extendedContext.checkPaymentModeChange?.( @@ -712,11 +709,11 @@ const deleteCommand: SlashCommand = { kind: CommandKind.BUILT_IN, schema: profileDeleteSchema, action: async ( - context: CommandContext, + _context: CommandContext, args: string, ): Promise => { // Parse profile name from args - const trimmedArgs = args?.trim(); + const trimmedArgs = args.trim(); if (!trimmedArgs) { // For now, show usage until dialog system is implemented @@ -728,8 +725,9 @@ const deleteCommand: SlashCommand = { } // Extract profile name - handle quoted names - const profileNameMatch = trimmedArgs.match(/^"([^"]+)"$/); - const profileName = profileNameMatch ? profileNameMatch[1] : trimmedArgs; + const profileNameMatch = RegExp(/^"([^"]+)"$/).exec(trimmedArgs); + const profileName = + profileNameMatch != null ? profileNameMatch[1] : trimmedArgs; if (!profileName) { return { @@ -795,7 +793,7 @@ const setDefaultCommand: SlashCommand = { args: string, ): Promise => { // Parse profile name from args - const trimmedArgs = args?.trim(); + const trimmedArgs = args.trim(); if (!trimmedArgs) { return { @@ -807,8 +805,9 @@ const setDefaultCommand: SlashCommand = { } // Extract profile name - handle quoted names - const profileNameMatch = trimmedArgs.match(/^"([^"]+)"$/); - const profileName = profileNameMatch ? profileNameMatch[1] : trimmedArgs; + const profileNameMatch = RegExp(/^"([^"]+)"$/).exec(trimmedArgs); + const profileName = + profileNameMatch != null ? profileNameMatch[1] : trimmedArgs; if (!profileName) { return { @@ -820,7 +819,7 @@ const setDefaultCommand: SlashCommand = { } try { - // Check if settings service is available + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions -- runtime guard if (!context.services.settings) { return { type: 'message', @@ -937,7 +936,7 @@ const showCommand: SlashCommand = { _context: CommandContext, args: string, ): Promise => { - const trimmedArgs = args?.trim(); + const trimmedArgs = args.trim(); if (!trimmedArgs) { return { @@ -948,8 +947,9 @@ const showCommand: SlashCommand = { } // Extract profile name - handle quoted names - const profileNameMatch = trimmedArgs.match(/^"([^"]+)"$/); - const profileName = profileNameMatch ? profileNameMatch[1] : trimmedArgs; + const profileNameMatch = RegExp(/^"([^"]+)"$/).exec(trimmedArgs); + const profileName = + profileNameMatch != null ? profileNameMatch[1] : trimmedArgs; if (!profileName) { return { @@ -1003,7 +1003,7 @@ const editCommand: SlashCommand = { _context: CommandContext, args: string, ): Promise => { - const trimmedArgs = args?.trim(); + const trimmedArgs = args.trim(); if (!trimmedArgs) { return { @@ -1014,8 +1014,9 @@ const editCommand: SlashCommand = { } // Extract profile name - handle quoted names - const profileNameMatch = trimmedArgs.match(/^"([^"]+)"$/); - const profileName = profileNameMatch ? profileNameMatch[1] : trimmedArgs; + const profileNameMatch = RegExp(/^"([^"]+)"$/).exec(trimmedArgs); + const profileName = + profileNameMatch != null ? profileNameMatch[1] : trimmedArgs; if (!profileName) { return { diff --git a/packages/cli/src/ui/commands/providerCommand.test.ts b/packages/cli/src/ui/commands/providerCommand.test.ts index c39a53f726..e09bf15d58 100644 --- a/packages/cli/src/ui/commands/providerCommand.test.ts +++ b/packages/cli/src/ui/commands/providerCommand.test.ts @@ -29,8 +29,8 @@ const mocks = vi.hoisted(() => { return { getProviderManagerMock: vi.fn(), refreshAliasProvidersMock: vi.fn(), - runtimeApi, getRuntimeApiMock: vi.fn(() => runtimeApi), + runtimeApi, }; }); @@ -114,13 +114,13 @@ describe('providerCommand /provider save', () => { }, }); - if (!providerCommand.action) { + if (providerCommand.action == null) { throw new Error('providerCommand must have an action'); } const result = await providerCommand.action(context, 'save myalias'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining('myalias'), @@ -167,7 +167,7 @@ describe('providerCommand /provider switch', () => { const context = createMockCommandContext(); - if (!providerCommand.action) { + if (providerCommand.action == null) { throw new Error('providerCommand must have an action'); } @@ -184,7 +184,7 @@ describe('providerCommand /provider switch', () => { }, expect.any(Number), ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: 'Switched from openai to qwen', @@ -202,13 +202,13 @@ describe('providerCommand /provider switch', () => { const context = createMockCommandContext(); - if (!providerCommand.action) { + if (providerCommand.action == null) { throw new Error('providerCommand must have an action'); } const result = await providerCommand.action(context, 'unknown'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Failed to switch provider: provider not found', diff --git a/packages/cli/src/ui/commands/providerCommand.ts b/packages/cli/src/ui/commands/providerCommand.ts index 3c87cb407a..8766b32636 100644 --- a/packages/cli/src/ui/commands/providerCommand.ts +++ b/packages/cli/src/ui/commands/providerCommand.ts @@ -5,10 +5,10 @@ */ import { - SlashCommand, - CommandContext, - OpenDialogActionReturn, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type OpenDialogActionReturn, + type MessageActionReturn, CommandKind, } from './types.js'; import { @@ -97,7 +97,7 @@ async function handleSaveAlias( } const config = context.services.config; - if (!config) { + if (config == null) { return { type: 'message', messageType: 'error', @@ -259,7 +259,7 @@ export const providerCommand: SlashCommand = { const extendedContext = context as CommandContext & { checkPaymentModeChange?: (forcePreviousProvider?: string) => void; }; - if (extendedContext.checkPaymentModeChange) { + if (extendedContext.checkPaymentModeChange != null) { setTimeout( () => extendedContext.checkPaymentModeChange!(fromProvider), 100, diff --git a/packages/cli/src/ui/commands/restoreCommand.test.ts b/packages/cli/src/ui/commands/restoreCommand.test.ts index 0bd72c004c..8a05c0775c 100644 --- a/packages/cli/src/ui/commands/restoreCommand.test.ts +++ b/packages/cli/src/ui/commands/restoreCommand.test.ts @@ -9,9 +9,9 @@ import * as fs from 'fs/promises'; import * as os from 'os'; import * as path from 'path'; import { restoreCommand } from './restoreCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; -import { Config, GitService } from '@vybestack/llxprt-code-core'; +import type { Config, GitService } from '@vybestack/llxprt-code-core'; import { createCompletionHandler } from './schema/index.js'; describe('restoreCommand', () => { @@ -69,7 +69,7 @@ describe('restoreCommand', () => { }); it('should return the command if checkpointing is enabled', () => { - expect(restoreCommand(mockConfig)).toEqual( + expect(restoreCommand(mockConfig)).toStrictEqual( expect.objectContaining({ name: 'restore', description: expect.any(String), @@ -87,7 +87,7 @@ describe('restoreCommand', () => { expect( await restoreCommand(mockConfig)?.action?.(mockContext, ''), - ).toEqual({ + ).toStrictEqual({ type: 'message', messageType: 'error', content: 'Could not determine the configuration directory path.', @@ -99,7 +99,7 @@ describe('restoreCommand', () => { await fs.rm(checkpointsDir, { recursive: true, force: true }); const command = restoreCommand(mockConfig); - expect(await command?.action?.(mockContext, '')).toEqual({ + expect(await command?.action?.(mockContext, '')).toStrictEqual({ type: 'message', messageType: 'info', content: 'No restorable tool calls found.', @@ -113,7 +113,7 @@ describe('restoreCommand', () => { await fs.writeFile(path.join(checkpointsDir, 'test2.json'), '{}'); const command = restoreCommand(mockConfig); - expect(await command?.action?.(mockContext, '')).toEqual({ + expect(await command?.action?.(mockContext, '')).toStrictEqual({ type: 'message', messageType: 'info', content: 'Available tool calls to restore:\n\ntest1\ntest2', @@ -124,7 +124,7 @@ describe('restoreCommand', () => { await fs.writeFile(path.join(checkpointsDir, 'test1.json'), '{}'); const command = restoreCommand(mockConfig); - expect(await command?.action?.(mockContext, 'test2')).toEqual({ + expect(await command?.action?.(mockContext, 'test2')).toStrictEqual({ type: 'message', messageType: 'error', content: 'File not found: test2.json', @@ -141,7 +141,9 @@ describe('restoreCommand', () => { await fs.mkdir(checkpointPath); const command = restoreCommand(mockConfig); - expect(await command?.action?.(mockContext, checkpointName)).toEqual({ + expect( + await command?.action?.(mockContext, checkpointName), + ).toStrictEqual({ type: 'message', messageType: 'error', content: expect.stringContaining( @@ -163,7 +165,9 @@ describe('restoreCommand', () => { ); const command = restoreCommand(mockConfig); - expect(await command?.action?.(mockContext, 'my-checkpoint')).toEqual({ + expect( + await command?.action?.(mockContext, 'my-checkpoint'), + ).toStrictEqual({ type: 'tool', toolName: 'run_shell_command', toolArgs: 'ls', @@ -195,7 +199,9 @@ describe('restoreCommand', () => { const command = restoreCommand(mockConfig); - expect(await command?.action?.(mockContext, 'my-checkpoint')).toEqual({ + expect( + await command?.action?.(mockContext, 'my-checkpoint'), + ).toStrictEqual({ type: 'tool', toolName: 'run_shell_command', toolArgs: 'ls', @@ -215,7 +221,7 @@ describe('restoreCommand', () => { ); const command = restoreCommand(mockConfig); - expect(await command?.action?.(mockContext, checkpointName)).toEqual({ + expect(await command?.action?.(mockContext, checkpointName)).toStrictEqual({ type: 'message', messageType: 'error', // A more specific error message would be ideal, but for now, we can assert the current behavior. @@ -229,7 +235,7 @@ describe('restoreCommand', () => { fullLine: string = `/restore ${partialArg}`, ): Promise => { const command = restoreCommand(mockConfig); - if (!command?.schema) { + if (command?.schema == null) { throw new Error('restore command schema missing'); } @@ -239,8 +245,8 @@ describe('restoreCommand', () => { { args: partialArg, completedArgs: [], - partialArg, commandPathLength: 1, + partialArg, }, fullLine, ); @@ -249,12 +255,12 @@ describe('restoreCommand', () => { it('returns an empty array if temp dir is not found', async () => { vi.mocked(mockConfig.storage.getProjectTempDir).mockReturnValueOnce(''); - expect(await runCompletion('')).toEqual([]); + expect(await runCompletion('')).toStrictEqual([]); }); it('returns an empty array on readdir error', async () => { await fs.rm(checkpointsDir, { recursive: true, force: true }); - expect(await runCompletion('')).toEqual([]); + expect(await runCompletion('')).toStrictEqual([]); }); it('returns a filtered list of checkpoint names', async () => { @@ -266,9 +272,9 @@ describe('restoreCommand', () => { '{}', ); - expect(await runCompletion('')).toEqual(['test1', 'test2']); - expect(await runCompletion('test')).toEqual(['test1', 'test2']); - expect(await runCompletion('test2')).toEqual(['test2']); + expect(await runCompletion('')).toStrictEqual(['test1', 'test2']); + expect(await runCompletion('test')).toStrictEqual(['test1', 'test2']); + expect(await runCompletion('test2')).toStrictEqual(['test2']); }); }); }); diff --git a/packages/cli/src/ui/commands/restoreCommand.ts b/packages/cli/src/ui/commands/restoreCommand.ts index 40f3bf7a4a..b33ee43b8b 100644 --- a/packages/cli/src/ui/commands/restoreCommand.ts +++ b/packages/cli/src/ui/commands/restoreCommand.ts @@ -12,8 +12,8 @@ import { type SlashCommandActionReturn, CommandKind, } from './types.js'; -import { Config } from '@vybestack/llxprt-code-core'; -import { type CommandArgumentSchema } from './schema/types.js'; +import type { Config } from '@vybestack/llxprt-code-core'; +import type { CommandArgumentSchema } from './schema/types.js'; import { withFuzzyFilter } from '../utils/fuzzyFilter.js'; const checkpointSuggestionDescription = 'Restorable tool call checkpoint'; diff --git a/packages/cli/src/ui/commands/schema/argumentResolver.test.ts b/packages/cli/src/ui/commands/schema/argumentResolver.test.ts index 7da111ed91..b648c43ed9 100644 --- a/packages/cli/src/ui/commands/schema/argumentResolver.test.ts +++ b/packages/cli/src/ui/commands/schema/argumentResolver.test.ts @@ -27,8 +27,8 @@ const literal = ( next?: CommandArgumentSchema, ): LiteralArgument => ({ kind: 'literal', - value, description: description || `Literal ${value}`, + value, next, }); @@ -41,11 +41,11 @@ const value = ( next?: CommandArgumentSchema, ): ValueArgument => ({ kind: 'value', - name, - description, options: (options || []).map((opt) => typeof opt === 'string' ? { value: opt } : opt, ), + name, + description, completer, hint, next, @@ -187,7 +187,7 @@ describe('argumentResolver @plan:PLAN-20251013-AUTOCOMPLETE.P04', () => { it('returns empty suggestions when value has no options or completer @plan:PLAN-20251013-AUTOCOMPLETE.P04 @requirement:REQ-002', async () => { await fc.assert( fc.asyncProperty(fc.string(), async (input) => { - const schema: CommandArgumentSchema = [value('mode', '', undefined)]; + const schema: CommandArgumentSchema = [value('mode', '')]; const handler = createCompletionHandler(schema); const result = await handler(mockContext, '', `/command ${input}`); expect(result.suggestions).toEqual([]); diff --git a/packages/cli/src/ui/commands/schema/deepPathCompletion.test.ts b/packages/cli/src/ui/commands/schema/deepPathCompletion.test.ts index 15fe5edc78..f32a517ade 100644 --- a/packages/cli/src/ui/commands/schema/deepPathCompletion.test.ts +++ b/packages/cli/src/ui/commands/schema/deepPathCompletion.test.ts @@ -23,8 +23,8 @@ const literal = ( next?: CommandArgumentSchema, ): LiteralArgument => ({ kind: 'literal', - value, description: description ?? `Literal ${value}`, + value, next, }); @@ -57,7 +57,7 @@ describe('Deep Path Completion @plan:PLAN-411-DEEPCOMPLETION', () => { const result = await handler(mockContext, '', '/set tea'); // Should find 'teatime' as a direct match - expect(result.suggestions).toEqual( + expect(result.suggestions).toStrictEqual( expect.arrayContaining([expect.objectContaining({ value: 'teatime' })]), ); @@ -239,7 +239,7 @@ describe('Deep Path Completion @plan:PLAN-411-DEEPCOMPLETION', () => { const result = await handler(mockContext, '', '/set '); // Empty query should return at least the single-level options - expect(result.suggestions).toEqual( + expect(result.suggestions).toStrictEqual( expect.arrayContaining([ expect.objectContaining({ value: 'unset' }), expect.objectContaining({ value: 'modelparam' }), @@ -312,13 +312,13 @@ describe('Deep Path Completion @plan:PLAN-411-DEEPCOMPLETION', () => { // Full match const result1 = await handler(mockContext, '', '/cmd create'); - expect(result1.suggestions).toEqual( + expect(result1.suggestions).toStrictEqual( expect.arrayContaining([expect.objectContaining({ value: 'create' })]), ); // Partial match const result2 = await handler(mockContext, '', '/cmd cr'); - expect(result2.suggestions).toEqual( + expect(result2.suggestions).toStrictEqual( expect.arrayContaining([expect.objectContaining({ value: 'create' })]), ); diff --git a/packages/cli/src/ui/commands/schema/index.ts b/packages/cli/src/ui/commands/schema/index.ts index 6270aac6c8..e10477277d 100644 --- a/packages/cli/src/ui/commands/schema/index.ts +++ b/packages/cli/src/ui/commands/schema/index.ts @@ -68,7 +68,7 @@ function mergeSchemas( primary: CommandArgumentSchema | undefined, secondary: CommandArgumentSchema, ): CommandArgumentSchema { - if (!primary || primary.length === 0) { + if (primary == null || primary.length === 0) { return secondary; } if (secondary.length === 0) { @@ -135,7 +135,11 @@ function normalizeCompletionContext( if (!explicitCompleted) { completedArgs = [...argsFromTokens]; - if (!hasTrailingSpace && tokenInfo.partialToken && argsFromTokens.length) { + if ( + !hasTrailingSpace && + tokenInfo.partialToken.length > 0 && + argsFromTokens.length > 0 + ) { completedArgs = argsFromTokens.slice(0, -1); } } @@ -204,7 +208,7 @@ function resolveActiveStep( const candidate = remainingArgs[0]; const matched = literals.find((literal) => literal.value === candidate); - if (matched) { + if (matched != null) { remainingArgs.shift(); consumedCount += 1; consumedLiterals += 1; @@ -255,7 +259,7 @@ async function suggestForValue( tokenInfo: TokenInfo, ): Promise { try { - if (node.completer) { + if (node.completer != null) { const results = await node.completer(ctx, partialArg, tokenInfo); if (!Array.isArray(results)) { return []; @@ -266,10 +270,10 @@ async function suggestForValue( })); } - if (node.options?.length) { + if (node.options != null && node.options.length > 0) { // Get the fuzzy filtering setting from context // Default to true if setting is not defined - const settingValue = ctx.services.settings?.merged?.enableFuzzyFiltering; + const settingValue = ctx.services.settings.merged.enableFuzzyFiltering; const enableFuzzy = settingValue ?? true; // Use filterCompletions for both fuzzy and exact prefix matching @@ -301,9 +305,9 @@ function flattenSchemaPaths( // If this node has a 'next' that contains value arguments with options, // we can create deep paths - if (node.next && node.next.length > 0) { + if (node.next != null && node.next.length > 0) { for (const nextNode of node.next) { - if (nextNode.kind === 'value' && nextNode.options) { + if (nextNode.kind === 'value' && nextNode.options != null) { // For each option in the value node, create a flattened path for (const option of nextNode.options) { flattened.push({ @@ -341,7 +345,7 @@ function suggestForLiterals( // Get the fuzzy filtering setting from context // Default to true if setting is not defined - const settingValue = ctx.services.settings?.merged?.enableFuzzyFiltering; + const settingValue = ctx.services.settings.merged.enableFuzzyFiltering; const enableFuzzy = settingValue ?? true; // Filter single-level options @@ -398,7 +402,7 @@ async function computeHintForValue( tokenInfo: TokenInfo, ): Promise { try { - if (node.hint) { + if (node.hint != null) { if (typeof node.hint === 'function') { return await node.hint(ctx, tokenInfo); } @@ -498,7 +502,7 @@ export function tokenize(fullLine: string): TokenInfo { const firstToken = tokens[0]; const prefixChars = new Set(['/', '@']); - const prefixChar = firstToken?.[0]; + const prefixChar = firstToken[0]; // Stryker disable next-line BooleanLiteral const hasPrefixChar = typeof prefixChar === 'string' && prefixChars.has(prefixChar); @@ -524,10 +528,10 @@ export function tokenize(fullLine: string): TokenInfo { } return { - tokens, partialToken: partialTokenValue, - hasTrailingSpace, position: tokens.length, + tokens, + hasTrailingSpace, }; } diff --git a/packages/cli/src/ui/commands/schema/types.ts b/packages/cli/src/ui/commands/schema/types.ts index 834ba33e22..730f162861 100644 --- a/packages/cli/src/ui/commands/schema/types.ts +++ b/packages/cli/src/ui/commands/schema/types.ts @@ -16,7 +16,7 @@ * - Line 6: Union CommandArgumentSchema type */ -import { CommandContext } from '../types.js'; +import type { CommandContext } from '../types.js'; // Line 1: LiteralArgument structure export interface LiteralArgument { diff --git a/packages/cli/src/ui/commands/setCommand.test.ts b/packages/cli/src/ui/commands/setCommand.test.ts index a4df90af65..3a6d344a71 100644 --- a/packages/cli/src/ui/commands/setCommand.test.ts +++ b/packages/cli/src/ui/commands/setCommand.test.ts @@ -32,7 +32,7 @@ describe('setCommand runtime integration', () => { it('requires arguments and shows usage when missing', async () => { const result = await setCommand.action!(context, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -47,7 +47,7 @@ describe('setCommand runtime integration', () => { 'context-limit', 32000, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -67,7 +67,7 @@ describe('setCommand runtime integration', () => { 'X-Test': 'value', }), ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -79,7 +79,7 @@ describe('setCommand runtime integration', () => { // Issue #884: /set streaming true should work without error const result = await setCommand.action!(context, 'streaming true'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: `Ephemeral setting 'streaming' set to "enabled" (session only, use /profile save to persist)`, @@ -95,7 +95,7 @@ describe('setCommand runtime integration', () => { // Issue #884: /set streaming false should work without error const result = await setCommand.action!(context, 'streaming false'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: `Ephemeral setting 'streaming' set to "disabled" (session only, use /profile save to persist)`, @@ -110,7 +110,7 @@ describe('setCommand runtime integration', () => { it('rejects invalid ephemeral keys', async () => { const result = await setCommand.action!(context, 'invalid-key value'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: expect.stringContaining( @@ -126,7 +126,7 @@ describe('setCommand runtime integration', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -147,7 +147,7 @@ describe('setCommand runtime integration', () => { key, Number(value), ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining(`'${key}'`), @@ -166,7 +166,7 @@ describe('setCommand runtime integration', () => { for (const { key, value } of testCases) { const result = await setCommand.action!(context, `${key} ${value}`); expect(mockRuntime.setEphemeralSetting).toHaveBeenCalledWith(key, -1); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: expect.stringContaining(`'${key}'`), @@ -205,7 +205,7 @@ describe('setCommand runtime integration', () => { for (const { key, value, expectedError } of invalidCases) { const result = await setCommand.action!(context, `${key} ${value}`); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: expect.stringContaining(expectedError), @@ -223,7 +223,7 @@ describe('setCommand runtime integration', () => { 'temperature', 0.7, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: "Model parameter 'temperature' set to 0.7", @@ -234,7 +234,7 @@ describe('setCommand runtime integration', () => { const result = await setCommand.action!(context, 'modelparam temperature'); expect(mockRuntime.setActiveModelParam).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -249,7 +249,7 @@ describe('setCommand runtime integration', () => { const result = await setCommand.action!(context, 'modelparam foo 1'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'Failed to set model parameter: boom', @@ -263,7 +263,7 @@ describe('setCommand runtime integration', () => { 'base-url', undefined, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: "Ephemeral setting 'base-url' cleared", @@ -274,7 +274,7 @@ describe('setCommand runtime integration', () => { const result = await setCommand.action!(context, 'unset modelparam foo'); expect(mockRuntime.clearActiveModelParam).toHaveBeenCalledWith('foo'); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: "Model parameter 'foo' cleared", @@ -285,7 +285,7 @@ describe('setCommand runtime integration', () => { const result = await setCommand.action!(context, 'unset modelparam'); expect(mockRuntime.clearActiveModelParam).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -307,7 +307,7 @@ describe('setCommand runtime integration', () => { 'custom-headers', undefined, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: "Custom header 'X-Test' cleared", @@ -325,7 +325,7 @@ describe('setCommand runtime integration', () => { ); expect(mockRuntime.setEphemeralSetting).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: "No custom header named 'X-Test' found", diff --git a/packages/cli/src/ui/commands/setCommand.ts b/packages/cli/src/ui/commands/setCommand.ts index 10ed458136..ce43026fee 100644 --- a/packages/cli/src/ui/commands/setCommand.ts +++ b/packages/cli/src/ui/commands/setCommand.ts @@ -5,9 +5,9 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, CommandKind, } from './types.js'; import type { @@ -231,7 +231,7 @@ const setSchema: CommandArgumentSchema = [ const headers = getRuntimeApi().getEphemeralSettings()[ 'custom-headers' ] as Record | undefined; - if (headers) { + if (headers != null) { const headerNames = Object.keys(headers); const filtered = filterStrings(headerNames, partial, { enableFuzzy, @@ -325,7 +325,7 @@ const setSchema: CommandArgumentSchema = [ const registryOptions = directSettingSpecByValue.get(resolvedSetting)?.options; - if (registryOptions && registryOptions.length > 0) { + if (registryOptions != null && registryOptions.length > 0) { return filterCompletions(registryOptions, partial, { enableFuzzy }); } @@ -334,7 +334,7 @@ const setSchema: CommandArgumentSchema = [ const headers = getRuntimeApi().getEphemeralSettings()[ 'custom-headers' ] as Record | undefined; - if (headers) { + if (headers != null) { const headerNames = Object.keys(headers); const filtered = filterStrings(headerNames, partial, { enableFuzzy, @@ -473,7 +473,7 @@ export const setCommand: SlashCommand = { const currentHeaders = runtime.getEphemeralSettings()[ 'custom-headers' ] as Record | undefined; - if (currentHeaders && subKey in currentHeaders) { + if (currentHeaders != null && subKey in currentHeaders) { const nextHeaders = { ...currentHeaders }; delete nextHeaders[subKey]; runtime.setEphemeralSetting( @@ -537,7 +537,7 @@ export const setCommand: SlashCommand = { // Get the config to apply settings const config = context.services.config; - if (!config) { + if (config == null) { return { type: 'message', messageType: 'error', diff --git a/packages/cli/src/ui/commands/setCommand.userAgent.test.ts b/packages/cli/src/ui/commands/setCommand.userAgent.test.ts index b613ee87ce..3663777e51 100644 --- a/packages/cli/src/ui/commands/setCommand.userAgent.test.ts +++ b/packages/cli/src/ui/commands/setCommand.userAgent.test.ts @@ -36,7 +36,7 @@ describe('setCommand user-agent ephemeral', () => { 'user-agent', 'RooCode/1.0', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: diff --git a/packages/cli/src/ui/commands/settingsCommand.test.ts b/packages/cli/src/ui/commands/settingsCommand.test.ts index 36a739b6da..d5d33307bf 100644 --- a/packages/cli/src/ui/commands/settingsCommand.test.ts +++ b/packages/cli/src/ui/commands/settingsCommand.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { settingsCommand } from './settingsCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; describe('settingsCommand', () => { @@ -17,11 +17,11 @@ describe('settingsCommand', () => { }); it('should return a dialog action to open the settings dialog', () => { - if (!settingsCommand.action) { + if (settingsCommand.action == null) { throw new Error('The settings command must have an action.'); } const result = settingsCommand.action(mockContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'dialog', dialog: 'settings', }); diff --git a/packages/cli/src/ui/commands/settingsCommand.ts b/packages/cli/src/ui/commands/settingsCommand.ts index ce8ee0495d..1762ded2c3 100644 --- a/packages/cli/src/ui/commands/settingsCommand.ts +++ b/packages/cli/src/ui/commands/settingsCommand.ts @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CommandKind, OpenDialogActionReturn, SlashCommand } from './types.js'; +import { + CommandKind, + type OpenDialogActionReturn, + type SlashCommand, +} from './types.js'; export const settingsCommand: SlashCommand = { name: 'settings', diff --git a/packages/cli/src/ui/commands/setupCommand.test.ts b/packages/cli/src/ui/commands/setupCommand.test.ts index 80840b7e3e..40cfb9a7ca 100644 --- a/packages/cli/src/ui/commands/setupCommand.test.ts +++ b/packages/cli/src/ui/commands/setupCommand.test.ts @@ -6,7 +6,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { setupCommand } from './setupCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; // Mock the welcome config module @@ -33,7 +33,7 @@ describe('setupCommand', () => { }); it('should reset welcome config and return dialog action', async () => { - if (!setupCommand.action) { + if (setupCommand.action == null) { throw new Error('setupCommand must have an action.'); } @@ -45,7 +45,7 @@ describe('setupCommand', () => { }); // Verify it returns a dialog action for 'welcome' - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'dialog', dialog: 'welcome', }); diff --git a/packages/cli/src/ui/commands/setupCommand.ts b/packages/cli/src/ui/commands/setupCommand.ts index eeefb66212..9a22a0dd78 100644 --- a/packages/cli/src/ui/commands/setupCommand.ts +++ b/packages/cli/src/ui/commands/setupCommand.ts @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CommandKind, OpenDialogActionReturn, SlashCommand } from './types.js'; +import { + CommandKind, + type OpenDialogActionReturn, + type SlashCommand, +} from './types.js'; import { saveWelcomeConfig } from '../../config/welcomeConfig.js'; /** diff --git a/packages/cli/src/ui/commands/setupGithubCommand.ts b/packages/cli/src/ui/commands/setupGithubCommand.ts index 678a9956ef..2675efe634 100644 --- a/packages/cli/src/ui/commands/setupGithubCommand.ts +++ b/packages/cli/src/ui/commands/setupGithubCommand.ts @@ -58,8 +58,7 @@ function getOpenUrlsCommands(readmeUrl: string): string[] { } // Create and join the individual commands - const commands = urlsToOpen.map((url) => `${openCmd} "${url}"`); - return commands; + return urlsToOpen.map((url) => `${openCmd} "${url}"`); } // Add LLxprt Code specific entries to .gitignore file @@ -135,7 +134,7 @@ async function downloadFiles({ ); } const body = response.body; - if (!body) { + if (body == null) { throw new Error( `Empty body while downloading ${endpoint}: ${response.status} - ${response.statusText}`, ); diff --git a/packages/cli/src/ui/commands/skillsCommand.test.ts b/packages/cli/src/ui/commands/skillsCommand.test.ts index 953ffbe208..eb2e813297 100644 --- a/packages/cli/src/ui/commands/skillsCommand.test.ts +++ b/packages/cli/src/ui/commands/skillsCommand.test.ts @@ -479,7 +479,7 @@ describe('skillsCommand', () => { ); const completions = await disableCmd.completion!(context, 'sk'); - expect(completions).toEqual(['skill1']); + expect(completions).toStrictEqual(['skill1']); }); it('should provide completions for enable (only disabled skills)', async () => { @@ -509,7 +509,7 @@ describe('skillsCommand', () => { ); const completions = await enableCmd.completion!(context, 'sk'); - expect(completions).toEqual(['skill2']); + expect(completions).toStrictEqual(['skill2']); }); }); }); diff --git a/packages/cli/src/ui/commands/skillsCommand.ts b/packages/cli/src/ui/commands/skillsCommand.ts index 78dab18366..020b66c9a1 100644 --- a/packages/cli/src/ui/commands/skillsCommand.ts +++ b/packages/cli/src/ui/commands/skillsCommand.ts @@ -39,7 +39,7 @@ async function listAction( } const skillManager = context.services.config?.getSkillManager(); - if (!skillManager) { + if (skillManager == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -101,7 +101,7 @@ async function disableAction( } const skill = skillManager?.getSkill(skillName); - if (!skill) { + if (skill == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -186,7 +186,7 @@ async function reloadAction( context: CommandContext, ): Promise { const config = context.services.config; - if (!config) { + if (config == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -282,7 +282,7 @@ async function disableCompletion( partialArg: string, ): Promise { const skillManager = context.services.config?.getSkillManager(); - if (!skillManager) { + if (skillManager == null) { return []; } return skillManager @@ -296,7 +296,7 @@ async function enableCompletion( partialArg: string, ): Promise { const skillManager = context.services.config?.getSkillManager(); - if (!skillManager) { + if (skillManager == null) { return []; } return skillManager diff --git a/packages/cli/src/ui/commands/statsCommand.test.ts b/packages/cli/src/ui/commands/statsCommand.test.ts index 81c23fe0a3..957a109890 100644 --- a/packages/cli/src/ui/commands/statsCommand.test.ts +++ b/packages/cli/src/ui/commands/statsCommand.test.ts @@ -6,7 +6,7 @@ import { vi, describe, it, expect, beforeEach } from 'vitest'; import { statsCommand } from './statsCommand.js'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; import { MessageType } from '../types.js'; import { formatDuration } from '../utils/formatters.js'; @@ -49,7 +49,7 @@ describe('statsCommand', () => { }); it('should display general session stats when run with no subcommand', async () => { - if (!statsCommand.action) throw new Error('Command has no action'); + if (statsCommand.action == null) throw new Error('Command has no action'); await statsCommand.action(mockContext, ''); @@ -67,7 +67,8 @@ describe('statsCommand', () => { const modelSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'model', ); - if (!modelSubCommand?.action) throw new Error('Subcommand has no action'); + if (modelSubCommand?.action == null) + throw new Error('Subcommand has no action'); // eslint-disable-next-line @typescript-eslint/no-floating-promises modelSubCommand.action(mockContext, ''); @@ -84,7 +85,8 @@ describe('statsCommand', () => { const toolsSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'tools', ); - if (!toolsSubCommand?.action) throw new Error('Subcommand has no action'); + if (toolsSubCommand?.action == null) + throw new Error('Subcommand has no action'); // eslint-disable-next-line @typescript-eslint/no-floating-promises toolsSubCommand.action(mockContext, ''); @@ -101,7 +103,8 @@ describe('statsCommand', () => { const cacheSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'cache', ); - if (!cacheSubCommand?.action) throw new Error('Subcommand has no action'); + if (cacheSubCommand?.action == null) + throw new Error('Subcommand has no action'); // eslint-disable-next-line @typescript-eslint/no-floating-promises cacheSubCommand.action(mockContext, ''); @@ -142,7 +145,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('Subcommand has no action'); + if (quotaSubCommand?.action == null) + throw new Error('Subcommand has no action'); await quotaSubCommand.action(mockContext, ''); @@ -206,7 +210,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('Subcommand has no action'); + if (quotaSubCommand?.action == null) + throw new Error('Subcommand has no action'); await quotaSubCommand.action(mockContext, ''); @@ -264,7 +269,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('Subcommand has no action'); + if (quotaSubCommand?.action == null) + throw new Error('Subcommand has no action'); await quotaSubCommand.action(mockContext, ''); @@ -289,7 +295,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('Subcommand has no action'); + if (quotaSubCommand?.action == null) + throw new Error('Subcommand has no action'); await quotaSubCommand.action(mockContext, ''); @@ -361,7 +368,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('Subcommand has no action'); + if (quotaSubCommand?.action == null) + throw new Error('Subcommand has no action'); await quotaSubCommand.action(mockContext, ''); @@ -400,7 +408,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('Subcommand has no action'); + if (quotaSubCommand?.action == null) + throw new Error('Subcommand has no action'); await quotaSubCommand.action(mockContext, ''); @@ -453,7 +462,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('Subcommand has no action'); + if (quotaSubCommand?.action == null) + throw new Error('Subcommand has no action'); await quotaSubCommand.action(mockContext, ''); @@ -489,7 +499,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (sc) => sc.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('Subcommand has no action'); + if (quotaSubCommand?.action == null) + throw new Error('Subcommand has no action'); await quotaSubCommand.action(mockContext, ''); @@ -545,7 +556,7 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (cmd) => cmd.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('No quota subcommand'); + if (quotaSubCommand?.action == null) throw new Error('No quota subcommand'); await quotaSubCommand.action(mockContext, ''); @@ -583,7 +594,7 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (cmd) => cmd.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('No quota subcommand'); + if (quotaSubCommand?.action == null) throw new Error('No quota subcommand'); await quotaSubCommand.action(mockContext, ''); @@ -645,7 +656,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (cmd) => cmd.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('No quota subcommand'); + if (quotaSubCommand?.action == null) + throw new Error('No quota subcommand'); await quotaSubCommand.action(mockContext, ''); @@ -691,7 +703,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (cmd) => cmd.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('No quota subcommand'); + if (quotaSubCommand?.action == null) + throw new Error('No quota subcommand'); await quotaSubCommand.action(mockContext, ''); @@ -737,7 +750,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (cmd) => cmd.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('No quota subcommand'); + if (quotaSubCommand?.action == null) + throw new Error('No quota subcommand'); await quotaSubCommand.action(mockContext, ''); @@ -783,7 +797,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (cmd) => cmd.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('No quota subcommand'); + if (quotaSubCommand?.action == null) + throw new Error('No quota subcommand'); await quotaSubCommand.action(mockContext, ''); @@ -828,7 +843,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (cmd) => cmd.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('No quota subcommand'); + if (quotaSubCommand?.action == null) + throw new Error('No quota subcommand'); await quotaSubCommand.action(mockContext, ''); @@ -874,7 +890,8 @@ describe('statsCommand', () => { const quotaSubCommand = statsCommand.subCommands?.find( (cmd) => cmd.name === 'quota', ); - if (!quotaSubCommand?.action) throw new Error('No quota subcommand'); + if (quotaSubCommand?.action == null) + throw new Error('No quota subcommand'); await quotaSubCommand.action(mockContext, ''); diff --git a/packages/cli/src/ui/commands/statsCommand.ts b/packages/cli/src/ui/commands/statsCommand.ts index 454d74c542..bcce82cfb6 100644 --- a/packages/cli/src/ui/commands/statsCommand.ts +++ b/packages/cli/src/ui/commands/statsCommand.ts @@ -6,7 +6,8 @@ * @plan PLAN-20260214-SESSIONBROWSER.P24 */ -import { MessageType, HistoryItemStats } from '../types.js'; +import type { HistoryItemStats } from '../types.js'; +import { MessageType } from '../types.js'; import { formatDuration } from '../utils/formatters.js'; import { type CommandContext, @@ -97,7 +98,7 @@ async function fetchApiKeyProviderQuota( ): Promise<{ provider: string; lines: string[] } | null> { let provider: 'zai' | 'synthetic' | 'chutes' | 'kimi' | null = null; let baseUrlForFetch: string | undefined; - const activeProviderName = runtimeApi.getActiveProviderName?.(); + const activeProviderName = runtimeApi.getActiveProviderName(); // Strategy 1: Check ephemeral base-url setting (highest priority) const ephemeralBaseUrl = runtimeApi.getEphemeralSetting('base-url'); @@ -110,53 +111,50 @@ async function fetchApiKeyProviderQuota( } // Strategy 2 & 3: If not found, try provider config base URLs - if (!provider && activeProviderName) { - const providerManager = runtimeApi.getCliProviderManager?.(); - if (providerManager) { - const providerInstance = - providerManager.getProviderByName?.(activeProviderName); - if (providerInstance) { - // Try providerConfig['base-url'] first (kebab-case per PR #1491) - const providerConfig = ( + if (!provider && activeProviderName !== '') { + const providerManager = runtimeApi.getCliProviderManager(); + const providerInstance = + providerManager.getProviderByName(activeProviderName); + if (providerInstance) { + // Try providerConfig['base-url'] first (kebab-case per PR #1491) + const providerConfig = ( + providerInstance as { + providerConfig?: { 'base-url'?: string }; + } + ).providerConfig; + if (providerConfig) { + const configBaseUrl = providerConfig['base-url']?.trim() ?? undefined; + if (configBaseUrl) { + provider = detectApiKeyProvider(configBaseUrl); + baseUrlForFetch = configBaseUrl; + if (provider) { + logger.debug( + () => `Detected ${provider} from provider config base-url`, + ); + } + } + } + + // Try baseProviderConfig['base-url'] if still not found (kebab-case per PR #1491) + if (!provider) { + const baseProviderConfig = ( providerInstance as { - providerConfig?: { 'base-url'?: string }; + baseProviderConfig?: { 'base-url'?: string }; } - ).providerConfig; - if (providerConfig) { - const configBaseUrl = providerConfig['base-url']?.trim() || undefined; - if (configBaseUrl) { - provider = detectApiKeyProvider(configBaseUrl); - baseUrlForFetch = configBaseUrl; + ).baseProviderConfig; + if (baseProviderConfig) { + const baseConfigUrl = + baseProviderConfig['base-url']?.trim() ?? undefined; + if (baseConfigUrl) { + provider = detectApiKeyProvider(baseConfigUrl); + baseUrlForFetch = baseConfigUrl; if (provider) { logger.debug( - () => `Detected ${provider} from provider config base-url`, + () => `Detected ${provider} from base provider config base-url`, ); } } } - - // Try baseProviderConfig['base-url'] if still not found (kebab-case per PR #1491) - if (!provider) { - const baseProviderConfig = ( - providerInstance as { - baseProviderConfig?: { 'base-url'?: string }; - } - ).baseProviderConfig; - if (baseProviderConfig) { - const baseConfigUrl = - baseProviderConfig['base-url']?.trim() || undefined; - if (baseConfigUrl) { - provider = detectApiKeyProvider(baseConfigUrl); - baseUrlForFetch = baseConfigUrl; - if (provider) { - logger.debug( - () => - `Detected ${provider} from base provider config base-url`, - ); - } - } - } - } } } } @@ -217,9 +215,9 @@ function formatGeminiQuotaLines(quotaData: Record): string[] { const lines: string[] = []; for (const bucket of buckets) { const b = bucket as Record; - const model = (b.modelId as string) ?? 'unknown'; - const tokenType = (b.tokenType as string) ?? 'tokens'; - const remaining = (b.remainingAmount as string) ?? '?'; + const model = (b.modelId as string | undefined) ?? 'unknown'; + const tokenType = (b.tokenType as string | undefined) ?? 'tokens'; + const remaining = (b.remainingAmount as string | undefined) ?? '?'; const fraction = typeof b.remainingFraction === 'number' ? ` (${Math.round(b.remainingFraction * 100)}%)` @@ -408,7 +406,8 @@ async function fetchAllQuotaInfo( async function defaultSessionView(context: CommandContext): Promise { const now = new Date(); const { sessionStartTime } = context.session.stats; - if (!sessionStartTime) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime guard + if (sessionStartTime == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -578,9 +577,10 @@ export const statsCommand: SlashCommand = { const stats = await tokenStore.getBucketStats(provider, bucket); if (stats) { - const lastUsedStr = stats.lastUsed - ? new Date(stats.lastUsed).toISOString().split('T')[0] - : 'Never'; + const lastUsedStr = + stats.lastUsed != null + ? new Date(stats.lastUsed).toISOString().split('T')[0] + : 'Never'; output.push(`- ${bucket}:`); output.push( diff --git a/packages/cli/src/ui/commands/subagentCommand.ts b/packages/cli/src/ui/commands/subagentCommand.ts index f800163c03..fc6d135540 100644 --- a/packages/cli/src/ui/commands/subagentCommand.ts +++ b/packages/cli/src/ui/commands/subagentCommand.ts @@ -9,9 +9,9 @@ import React from 'react'; import { Text } from 'ink'; import { - SlashCommand, - CommandContext, - SlashCommandActionReturn, + type SlashCommand, + type CommandContext, + type SlashCommandActionReturn, CommandKind, } from './types.js'; import { MessageType } from '../types.js'; @@ -36,11 +36,11 @@ function parseSaveArgs(args: string): { mode: 'auto' | 'manual'; input: string; } | null { - const match = args.match( + const match = RegExp( /^(\S+)\s+(\S+)\s+(auto|manual)\s+"((?:[^"\\]|\\.)*)("|"?)/, - ); + ).exec(args); - if (!match) { + if (match == null) { return null; } @@ -81,7 +81,7 @@ async function saveSubagent( existed: boolean, ): Promise { const manager = context.services.subagentManager; - if (!manager) { + if (manager == null) { return { type: 'message', messageType: 'error', @@ -125,13 +125,13 @@ const subagentSchema = [ description: 'Enter subagent name', completer: withFuzzyFilter(async (ctx: CommandContext) => { const manager = ctx.services.subagentManager; - if (!manager) { + if (manager == null) { return []; } const names = await manager.listSubagents(); - const suggestions = await Promise.all( + return Promise.all( names.map(async (name) => { try { const details = await manager.loadSubagent(name); @@ -147,8 +147,6 @@ const subagentSchema = [ } }), ); - - return suggestions; }), }, { @@ -158,7 +156,7 @@ const subagentSchema = [ completer: withFuzzyFilter(async (ctx: CommandContext) => { const profileManager = ctx.services.profileManager; if ( - !profileManager || + profileManager == null || typeof profileManager.listProfiles !== 'function' ) { return []; @@ -229,7 +227,7 @@ const saveCommand: SlashCommand = { // Parse arguments: "" const parsedArgs = parseSaveArgs(args); - if (!parsedArgs) { + if (parsedArgs == null) { return { type: 'message', messageType: 'error', @@ -240,7 +238,7 @@ const saveCommand: SlashCommand = { const { name, profile, mode, input } = parsedArgs; const { services, overwriteConfirmed, invocation } = context; const subagentManager = services.subagentManager; // @plan:PLAN-20250117-SUBAGENTCONFIG.P14 @requirement:REQ-003 - if (!subagentManager) { + if (subagentManager == null) { return { type: 'message', messageType: 'error', @@ -287,31 +285,30 @@ const saveCommand: SlashCommand = { return handleManualMode(context, name, profile, finalSystemPrompt, { existed: exists, }); - } else { - // Auto mode: generate using LLM - const configService = services.config; // @plan:PLAN-20250117-SUBAGENTCONFIG.P14 @requirement:REQ-003 - if (!configService) { - return { - type: 'message', - messageType: 'error', - content: - 'Configuration service unavailable. Set up the CLI before using auto mode.', - }; - } + } + // Auto mode: generate using LLM + const configService = services.config; // @plan:PLAN-20250117-SUBAGENTCONFIG.P14 @requirement:REQ-003 + if (configService == null) { + return { + type: 'message', + messageType: 'error', + content: + 'Configuration service unavailable. Set up the CLI before using auto mode.', + }; + } - try { - finalSystemPrompt = await generateAutoPrompt(configService, input); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - return { - type: 'message', - messageType: 'error', - content: `Error: Failed to generate system prompt (${errorMessage}). Try manual mode or check your connection.`, - }; - } - return saveSubagent(context, name, profile, finalSystemPrompt, exists); + try { + finalSystemPrompt = await generateAutoPrompt(configService, input); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + return { + type: 'message', + messageType: 'error', + content: `Error: Failed to generate system prompt (${errorMessage}). Try manual mode or check your connection.`, + }; } + return saveSubagent(context, name, profile, finalSystemPrompt, exists); }, }; @@ -327,7 +324,7 @@ const listCommand: SlashCommand = { kind: CommandKind.BUILT_IN, action: async (context: CommandContext, _args: string): Promise => { const manager = context.services.subagentManager; - if (!manager) { + if (manager == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -410,7 +407,7 @@ const showCommand: SlashCommand = { // Validate subagent exists const { services } = context; const subagentManager = services.subagentManager; - if (!subagentManager) { + if (subagentManager == null) { return { type: 'message', messageType: 'error', @@ -461,7 +458,7 @@ const deleteCommand: SlashCommand = { const { services } = context; const subagentManager = services.subagentManager; - if (!subagentManager) { + if (subagentManager == null) { return { type: 'message', messageType: 'error', @@ -544,7 +541,7 @@ const editCommand: SlashCommand = { const { services } = context; const subagentManager = services.subagentManager; - if (!subagentManager) { + if (subagentManager == null) { return { type: 'message', messageType: 'error', diff --git a/packages/cli/src/ui/commands/tasksCommand.ts b/packages/cli/src/ui/commands/tasksCommand.ts index 756a18bc6d..46ad72f999 100644 --- a/packages/cli/src/ui/commands/tasksCommand.ts +++ b/packages/cli/src/ui/commands/tasksCommand.ts @@ -70,7 +70,7 @@ function formatTask(task: AsyncTaskInfo): string { */ function getRunningTaskIds(context: CommandContext): string[] { const asyncTaskManager = context.services.config?.getAsyncTaskManager?.(); - if (!asyncTaskManager) { + if (asyncTaskManager == null) { return []; } @@ -102,7 +102,7 @@ export const taskCommand: SlashCommand = { action: (context: CommandContext) => { const asyncTaskManager = context.services.config?.getAsyncTaskManager?.(); - if (!asyncTaskManager) { + if (asyncTaskManager == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -174,7 +174,7 @@ export const taskCommand: SlashCommand = { const asyncTaskManager = context.services.config?.getAsyncTaskManager?.(); - if (!asyncTaskManager) { + if (asyncTaskManager == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -188,13 +188,16 @@ export const taskCommand: SlashCommand = { // Try exact match first let task = asyncTaskManager.getTask(taskId); - if (!task) { + if (task == null) { // Try prefix match const result = asyncTaskManager.getTaskByPrefix(taskId); - if (result.task) { + if (result.task != null) { task = result.task; - } else if (result.candidates && result.candidates.length > 0) { + } else if ( + result.candidates != null && + result.candidates.length > 0 + ) { // Ambiguous - show full task IDs const candidateList = result.candidates .map((c) => ` ${c.id}`) diff --git a/packages/cli/src/ui/commands/terminalSetupCommand.test.ts b/packages/cli/src/ui/commands/terminalSetupCommand.test.ts index c56ddb69d6..1c5fe137ef 100644 --- a/packages/cli/src/ui/commands/terminalSetupCommand.test.ts +++ b/packages/cli/src/ui/commands/terminalSetupCommand.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { terminalSetupCommand } from './terminalSetupCommand.js'; import * as terminalSetupModule from '../utils/terminalSetup.js'; -import { CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; vi.mock('../utils/terminalSetup.js'); @@ -30,7 +30,7 @@ describe('terminalSetupCommand', () => { const result = await terminalSetupCommand.action({} as CommandContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', content: 'Terminal configured successfully', messageType: 'info', @@ -46,7 +46,7 @@ describe('terminalSetupCommand', () => { const result = await terminalSetupCommand.action({} as CommandContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', content: 'Terminal configured successfully\n\nPlease restart your terminal for the changes to take effect.', @@ -62,7 +62,7 @@ describe('terminalSetupCommand', () => { const result = await terminalSetupCommand.action({} as CommandContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', content: 'Failed to detect terminal', messageType: 'error', @@ -76,7 +76,7 @@ describe('terminalSetupCommand', () => { const result = await terminalSetupCommand.action({} as CommandContext, ''); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', content: 'Failed to configure terminal: Error: Unexpected error', messageType: 'error', diff --git a/packages/cli/src/ui/commands/terminalSetupCommand.ts b/packages/cli/src/ui/commands/terminalSetupCommand.ts index 5055f9f518..274c0ed3ca 100644 --- a/packages/cli/src/ui/commands/terminalSetupCommand.ts +++ b/packages/cli/src/ui/commands/terminalSetupCommand.ts @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { MessageActionReturn, SlashCommand, CommandKind } from './types.js'; +import { + type MessageActionReturn, + type SlashCommand, + CommandKind, +} from './types.js'; import { terminalSetup } from '../utils/terminalSetup.js'; /** @@ -31,8 +35,8 @@ export const terminalSetupCommand: SlashCommand = { return { type: 'message', - content, messageType: result.success ? 'info' : 'error', + content, }; } catch (error) { return { diff --git a/packages/cli/src/ui/commands/test/setCommand.mutation.test.ts b/packages/cli/src/ui/commands/test/setCommand.mutation.test.ts index 3c5a487e98..2ba57eed0f 100644 --- a/packages/cli/src/ui/commands/test/setCommand.mutation.test.ts +++ b/packages/cli/src/ui/commands/test/setCommand.mutation.test.ts @@ -36,7 +36,7 @@ describe('setCommand action mutation coverage', () => { 'context-limit', 32000, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -50,7 +50,7 @@ describe('setCommand action mutation coverage', () => { 'context-limit not-a-number', ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'error', content: 'context-limit must be a positive integer (e.g., 100000)', @@ -64,7 +64,7 @@ describe('setCommand action mutation coverage', () => { 'socket-keepalive', true, ); - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'message', messageType: 'info', content: @@ -84,7 +84,7 @@ describe('setCommand action mutation coverage', () => { ); const failure = await setCommand.action!(context, 'streaming maybe'); - expect(failure).toEqual({ + expect(failure).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -110,7 +110,7 @@ describe('setCommand action mutation coverage', () => { context, 'compression-threshold 1.4', ); - expect(failure).toEqual({ + expect(failure).toStrictEqual({ type: 'message', messageType: 'error', content: @@ -148,7 +148,7 @@ describe('setCommand action mutation coverage', () => { context, 'tool-output-max-items 0', ); - expect(failure).toEqual({ + expect(failure).toStrictEqual({ type: 'message', messageType: 'error', content: 'tool-output-max-items must be a positive integer', diff --git a/packages/cli/src/ui/commands/test/setCommand.phase09.test.ts b/packages/cli/src/ui/commands/test/setCommand.phase09.test.ts index e28cbfa5d1..35d6355ac3 100644 --- a/packages/cli/src/ui/commands/test/setCommand.phase09.test.ts +++ b/packages/cli/src/ui/commands/test/setCommand.phase09.test.ts @@ -50,7 +50,7 @@ import { setCommand } from '../setCommand.js'; const commandSchema = setCommand.schema; -if (!commandSchema) { +if (commandSchema == null) { throw new Error('setCommand schema is not configured'); } @@ -109,7 +109,7 @@ describe('setCommand schema integration', () => { it('should suggest available subcommands when no input provided @plan:PLAN-20251013-AUTOCOMPLETE.P09 @requirement:REQ-006', async () => { const result = await handler(mockContext, '', '/set '); - expect(result.suggestions).toEqual( + expect(result.suggestions).toStrictEqual( expect.arrayContaining([ expect.objectContaining({ value: 'unset' }), expect.objectContaining({ value: 'modelparam' }), @@ -127,7 +127,7 @@ describe('setCommand schema integration', () => { // With deep path completion, we now also get nested paths like 'modelparam temperature' // The single-level 'modelparam' should still be included and appear first - expect(result.suggestions).toEqual( + expect(result.suggestions).toStrictEqual( expect.arrayContaining([ expect.objectContaining({ value: 'modelparam' }), ]), @@ -155,7 +155,7 @@ describe('setCommand schema integration', () => { it('should accept exact literal match and advance to next arguments @plan:PLAN-20251013-AUTOCOMPLETE.P09 @requirement:REQ-006', async () => { const result = await handler(mockContext, '', '/set unset '); - expect(result.suggestions).toEqual( + expect(result.suggestions).toStrictEqual( expect.arrayContaining([ expect.objectContaining({ value: 'context-limit' }), expect.objectContaining({ value: 'emojifilter' }), @@ -171,7 +171,7 @@ describe('setCommand schema integration', () => { it('should provide emojifilter mode options after selecting emojifilter @plan:PLAN-20251013-AUTOCOMPLETE.P09 @requirement:REQ-006', async () => { const result = await handler(mockContext, '', '/set emojifilter '); - expect(result.suggestions).toEqual([ + expect(result.suggestions).toStrictEqual([ expect.objectContaining({ value: 'allowed' }), expect.objectContaining({ value: 'auto' }), expect.objectContaining({ value: 'warn' }), @@ -188,7 +188,7 @@ describe('setCommand schema integration', () => { }); const result = await handler(mockContext, '', '/set modelparam '); - expect(result.suggestions).toEqual( + expect(result.suggestions).toStrictEqual( expect.arrayContaining([ expect.objectContaining({ value: 'temperature', @@ -211,7 +211,7 @@ describe('setCommand schema integration', () => { '/set modelparam temperature ', ); - expect(result.suggestions).toEqual([]); // No predefined options for values + expect(result.suggestions).toStrictEqual([]); // No predefined options for values expect(result.hint).toBe( 'value to set for the parameter (number, string, boolean, or JSON)', ); @@ -227,7 +227,7 @@ describe('setCommand schema integration', () => { '/set context-limit 50000 ', ); - expect(result.suggestions).toEqual([]); + expect(result.suggestions).toStrictEqual([]); expect(result.hint).toBe(''); }); @@ -238,7 +238,7 @@ describe('setCommand schema integration', () => { '/set socket-keepalive true ', ); - expect(result.suggestions).toEqual([]); + expect(result.suggestions).toStrictEqual([]); expect(result.hint).toBe(''); }); }); @@ -288,7 +288,7 @@ describe('setCommand schema integration', () => { const result = await handler(mockContext, '', '/set modelparam '); - expect(result.suggestions.map((s) => s.value)).toEqual( + expect(result.suggestions.map((s) => s.value)).toStrictEqual( expect.arrayContaining(paramNames), ); expect(result.hint).toBe('parameter name'); diff --git a/packages/cli/src/ui/commands/test/subagentCommand.schema.test.ts b/packages/cli/src/ui/commands/test/subagentCommand.schema.test.ts index 0e18268f66..fd909dc231 100644 --- a/packages/cli/src/ui/commands/test/subagentCommand.schema.test.ts +++ b/packages/cli/src/ui/commands/test/subagentCommand.schema.test.ts @@ -71,7 +71,7 @@ const invoke = async ( (cmd) => cmd.name === 'save', ); - if (!saveCommand?.schema) { + if (saveCommand?.schema == null) { throw new Error('saveCommand schema is not configured'); } @@ -97,7 +97,7 @@ describe('subagent schema resolver integration @plan:PLAN-20250214-AUTOCOMPLETE. commandPathLength: 2, }); - expect(result.suggestions).toEqual([ + expect(result.suggestions).toStrictEqual([ { value: 'agent1', description: 'Profile: default' }, { value: 'agent2', description: 'Profile: custom' }, { value: 'code-helper', description: 'Profile: coding' }, @@ -114,7 +114,7 @@ describe('subagent schema resolver integration @plan:PLAN-20250214-AUTOCOMPLETE. commandPathLength: 2, }); - expect(result.suggestions.map((s) => s.value)).toEqual([ + expect(result.suggestions.map((s) => s.value)).toStrictEqual([ 'agent1', 'agent2', ]); @@ -129,7 +129,7 @@ describe('subagent schema resolver integration @plan:PLAN-20250214-AUTOCOMPLETE. commandPathLength: 2, }); - expect(result.suggestions.map((s) => s.value)).toEqual(mockProfiles); + expect(result.suggestions.map((s) => s.value)).toStrictEqual(mockProfiles); expect(result.hint).toBe('Select profile configuration'); expect(result.position).toBe(2); }); @@ -142,7 +142,10 @@ describe('subagent schema resolver integration @plan:PLAN-20250214-AUTOCOMPLETE. commandPathLength: 2, }); - expect(result.suggestions.map((s) => s.value)).toEqual(['auto', 'manual']); + expect(result.suggestions.map((s) => s.value)).toStrictEqual([ + 'auto', + 'manual', + ]); expect(result.hint).toBe('Select mode'); }); @@ -154,7 +157,7 @@ describe('subagent schema resolver integration @plan:PLAN-20250214-AUTOCOMPLETE. commandPathLength: 2, }); - expect(result.suggestions).toEqual([]); + expect(result.suggestions).toStrictEqual([]); expect(result.hint).toBe('Enter system prompt for automatic mode'); }); }); diff --git a/packages/cli/src/ui/commands/test/subagentCommand.test.ts b/packages/cli/src/ui/commands/test/subagentCommand.test.ts index 1224fa1ad1..bef993dca8 100644 --- a/packages/cli/src/ui/commands/test/subagentCommand.test.ts +++ b/packages/cli/src/ui/commands/test/subagentCommand.test.ts @@ -25,7 +25,7 @@ vi.mock('../../utils/autoPromptGenerator.js', async (importOriginal) => { return { ...actual, generateAutoPrompt: (...args: unknown[]) => { - if (generateAutoPromptOverride) { + if (generateAutoPromptOverride != null) { return generateAutoPromptOverride(...args); } return actual.generateAutoPrompt( @@ -57,20 +57,20 @@ import * as os from 'os'; import { SubagentManager, ProfileManager, - Logger, - SessionMetrics, + type Logger, + type SessionMetrics, } from '@vybestack/llxprt-code-core'; import { SubagentView } from '../../components/SubagentManagement/types.js'; import { MessageType } from '../../types.js'; import { FunctionCallingConfigMode } from '@google/genai'; -import { +import type { CommandContext, MessageActionReturn, ConfirmActionReturn, SlashCommandActionReturn, } from '../types.js'; -import { LoadedSettings } from '../../../config/settings.js'; -import { SessionStatsState } from '../../contexts/SessionContext.js'; +import type { LoadedSettings } from '../../../config/settings.js'; +import type { SessionStatsState } from '../../contexts/SessionContext.js'; let subagentCommand: typeof import('../subagentCommand.js').subagentCommand; @@ -143,10 +143,10 @@ const createTestContext = ({ const stats: SessionStatsState = { sessionId: 'test-session', sessionStartTime: new Date(), - metrics, lastPromptTokenCount: 0, historyTokenCount: 0, promptCount: 0, + metrics, }; return { @@ -157,8 +157,8 @@ const createTestContext = ({ }, services: { config: null, - settings, git: undefined, + settings, logger, profileManager, subagentManager, @@ -690,7 +690,7 @@ describe('saveCommand - auto mode @requirement:REQ-003', () => { const callArgs = mockGeminiClient.generateDirectMessage.mock.calls[0][0]; expect(callArgs.message).toMatch(/expert Python debugger/); expect(callArgs.message).toMatch(/system prompt/i); - expect(callArgs.config).toEqual({ + expect(callArgs.config).toStrictEqual({ toolConfig: { functionCallingConfig: { mode: FunctionCallingConfigMode.NONE, diff --git a/packages/cli/src/ui/commands/test/useSlashCompletion.schema.test.ts b/packages/cli/src/ui/commands/test/useSlashCompletion.schema.test.ts index 29eac39a00..0cfb649219 100644 --- a/packages/cli/src/ui/commands/test/useSlashCompletion.schema.test.ts +++ b/packages/cli/src/ui/commands/test/useSlashCompletion.schema.test.ts @@ -28,8 +28,12 @@ vi.mock('../schema/index.js', () => ({ import { renderHook, waitFor } from '../../../test-utils/render.js'; import { useSlashCompletion } from '../../hooks/useSlashCompletion.js'; import { useTextBuffer } from '../../components/shared/text-buffer.js'; -import { CommandContext, CommandKind, SlashCommand } from '../types.js'; -import { Config, FileDiscoveryService } from '@vybestack/llxprt-code-core'; +import { + type CommandContext, + CommandKind, + type SlashCommand, +} from '../types.js'; +import { type Config, FileDiscoveryService } from '@vybestack/llxprt-code-core'; const mockCommandContext = {} as CommandContext; diff --git a/packages/cli/src/ui/commands/themeCommand.test.ts b/packages/cli/src/ui/commands/themeCommand.test.ts index feceeee5f5..0027a75878 100644 --- a/packages/cli/src/ui/commands/themeCommand.test.ts +++ b/packages/cli/src/ui/commands/themeCommand.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach } from 'vitest'; import { themeCommand } from './themeCommand'; -import { type CommandContext } from './types.js'; +import type { CommandContext } from './types.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; describe('themeCommand', () => { @@ -25,7 +25,7 @@ describe('themeCommand', () => { const result = themeCommand.action(mockContext, ''); // Assert that the action returns the correct object to trigger the theme dialog. - expect(result).toEqual({ + expect(result).toStrictEqual({ type: 'dialog', dialog: 'theme', }); diff --git a/packages/cli/src/ui/commands/themeCommand.ts b/packages/cli/src/ui/commands/themeCommand.ts index cf8737c9f9..4278ac6ec5 100644 --- a/packages/cli/src/ui/commands/themeCommand.ts +++ b/packages/cli/src/ui/commands/themeCommand.ts @@ -4,7 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CommandKind, OpenDialogActionReturn, SlashCommand } from './types.js'; +import { + CommandKind, + type OpenDialogActionReturn, + type SlashCommand, +} from './types.js'; export const themeCommand: SlashCommand = { name: 'theme', diff --git a/packages/cli/src/ui/commands/todoCommand.test.ts b/packages/cli/src/ui/commands/todoCommand.test.ts index 0352faf453..5fb4918c53 100644 --- a/packages/cli/src/ui/commands/todoCommand.test.ts +++ b/packages/cli/src/ui/commands/todoCommand.test.ts @@ -93,7 +93,7 @@ describe('todoCommand', () => { // Verify updateTodos([]) was called expect(ctx.todoContext?.updateTodos).toHaveBeenCalledWith([]); - expect(ctx.todoContext?.todos).toEqual([]); + expect(ctx.todoContext?.todos).toStrictEqual([]); }); /** @@ -629,7 +629,7 @@ describe('todoCommand', () => { // eslint-disable-next-line @typescript-eslint/no-floating-promises removeSubcommand!.action!(ctx, `${start}-${end}`); - if (ctx.todoContext?.updateTodos) { + if (ctx.todoContext?.updateTodos != null) { const calls = ( ctx.todoContext.updateTodos as ReturnType ).mock.calls; diff --git a/packages/cli/src/ui/commands/todoCommand.ts b/packages/cli/src/ui/commands/todoCommand.ts index fac71273ab..c4da17f4ea 100644 --- a/packages/cli/src/ui/commands/todoCommand.ts +++ b/packages/cli/src/ui/commands/todoCommand.ts @@ -56,8 +56,8 @@ export function parsePosition(pos: string, todos: Todo[]): ParsedPosition { } // Line 53: ELSE IF position matches /^(\d+)\.(\d+|last)$/ - const subtaskMatch = pos.match(/^(\d+)\.(\d+|last)$/); - if (subtaskMatch) { + const subtaskMatch = RegExp(/^(\d+)\.(\d+|last)$/).exec(pos); + if (subtaskMatch != null) { // Line 54: PARSE parent_pos, subtask_pos const parentIndex = parseInt(subtaskMatch[1], 10) - 1; @@ -155,7 +155,7 @@ export const todoCommand: SlashCommand = { * @requirement REQ-003 */ action: (context) => { - if (!context.todoContext) { + if (context.todoContext == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -186,7 +186,7 @@ export const todoCommand: SlashCommand = { * @requirement REQ-004 */ action: (context) => { - if (!context.todoContext) { + if (context.todoContext == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -225,7 +225,7 @@ export const todoCommand: SlashCommand = { lines.push(`${pos}. ${statusIcon} ${todo.content}`.trim()); // Display subtasks if present - if (todo.subtasks && todo.subtasks.length > 0) { + if (todo.subtasks != null && todo.subtasks.length > 0) { todo.subtasks.forEach((subtask, subIdx) => { const subPos = `${pos}.${subIdx + 1}`; lines.push(` ${subPos}. ${subtask.content}`); @@ -254,7 +254,7 @@ export const todoCommand: SlashCommand = { * @requirement REQ-008 */ action: (context, args) => { - if (!context.todoContext) { + if (context.todoContext == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -350,7 +350,7 @@ Examples: * @requirement REQ-008 */ action: (context, args) => { - if (!context.todoContext) { + if (context.todoContext == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -449,7 +449,7 @@ Examples: * @pseudocode lines 42-74 */ action: (context, args) => { - if (!context.todoContext) { + if (context.todoContext == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -509,7 +509,8 @@ Examples: const newTodos = [...todos]; // Clone the parent object and its subtasks to avoid mutation const parent = { ...newTodos[parsed.parentIndex] }; - parent.subtasks = parent.subtasks ? [...parent.subtasks] : []; + parent.subtasks = + parent.subtasks != null ? [...parent.subtasks] : []; const newSubtask = { id: `${newId}-${parsed.subtaskIndex}`, @@ -575,7 +576,7 @@ Examples: * @pseudocode Extended with range and "all" support */ action: (context, args) => { - if (!context.todoContext) { + if (context.todoContext == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -626,8 +627,8 @@ Examples: } // Check for range pattern (e.g., "2-4") - const rangeMatch = posStr.match(/^(\d+)-(\d+)$/); - if (rangeMatch) { + const rangeMatch = RegExp(/^(\d+)-(\d+)$/).exec(posStr); + if (rangeMatch != null) { const start = parseInt(rangeMatch[1], 10); const end = parseInt(rangeMatch[2], 10); @@ -682,7 +683,7 @@ Examples: const parent = newTodos[parsed.parentIndex]; if ( - !parent.subtasks || + parent.subtasks == null || parsed.subtaskIndex >= parent.subtasks.length ) { context.ui.addItem( @@ -816,8 +817,8 @@ Examples: } // Check for range pattern (e.g., "1-3") - const rangeMatch = posStr.match(/^(\d+)-(\d+)$/); - if (rangeMatch) { + const rangeMatch = RegExp(/^(\d+)-(\d+)$/).exec(posStr); + if (rangeMatch != null) { const start = parseInt(rangeMatch[1], 10); const end = parseInt(rangeMatch[2], 10); @@ -918,7 +919,7 @@ Examples: * @requirement REQ-011 */ action: (context, args) => { - if (!context.todoContext) { + if (context.todoContext == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -975,8 +976,8 @@ Examples: } // Check for range pattern (e.g., "1-3") - const rangeMatch = posStr.match(/^(\d+)-(\d+)$/); - if (rangeMatch) { + const rangeMatch = RegExp(/^(\d+)-(\d+)$/).exec(posStr); + if (rangeMatch != null) { const start = parseInt(rangeMatch[1], 10); const end = parseInt(rangeMatch[2], 10); @@ -1180,7 +1181,7 @@ Examples: * @requirement REQ-009 */ action: async (context, args) => { - if (!context.todoContext) { + if (context.todoContext == null) { context.ui.addItem( { type: MessageType.ERROR, diff --git a/packages/cli/src/ui/commands/toolkeyCommand.test.ts b/packages/cli/src/ui/commands/toolkeyCommand.test.ts index 6085ec9d86..d14956f867 100644 --- a/packages/cli/src/ui/commands/toolkeyCommand.test.ts +++ b/packages/cli/src/ui/commands/toolkeyCommand.test.ts @@ -71,7 +71,7 @@ describe('toolkeyCommand', () => { describe('schema completion', () => { it('suggests tool names for first argument with descriptions', async () => { const schema = toolkeyCommand.schema; - if (!schema) { + if (schema == null) { throw new Error('toolkey schema missing'); } @@ -87,7 +87,7 @@ describe('toolkeyCommand', () => { '/toolkey ex', ); - expect(result.suggestions).toEqual( + expect(result.suggestions).toStrictEqual( expect.arrayContaining([expect.objectContaining({ value: 'exa' })]), ); expect(result.hint).toBe('Select built-in tool'); @@ -95,7 +95,7 @@ describe('toolkeyCommand', () => { it('suggests none and shows key hint after selecting tool', async () => { const schema = toolkeyCommand.schema; - if (!schema) { + if (schema == null) { throw new Error('toolkey schema missing'); } @@ -111,7 +111,7 @@ describe('toolkeyCommand', () => { '/toolkey exa ', ); - expect(result.suggestions).toEqual( + expect(result.suggestions).toStrictEqual( expect.arrayContaining([expect.objectContaining({ value: 'none' })]), ); expect(result.hint).toBe( diff --git a/packages/cli/src/ui/commands/toolkeyCommand.ts b/packages/cli/src/ui/commands/toolkeyCommand.ts index bc095f4864..0feff32dab 100644 --- a/packages/cli/src/ui/commands/toolkeyCommand.ts +++ b/packages/cli/src/ui/commands/toolkeyCommand.ts @@ -11,9 +11,9 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, CommandKind, } from './types.js'; import type { CommandArgumentSchema } from './schema/types.js'; @@ -29,9 +29,10 @@ const toolNameOptions = getSupportedToolNames().map((toolName) => { const entry = getToolKeyEntry(toolName); return { value: toolName, - description: entry - ? `${entry.displayName}: ${entry.description}` - : `Tool ${toolName}`, + description: + entry != null + ? `${entry.displayName}: ${entry.description}` + : `Tool ${toolName}`, }; }); @@ -59,7 +60,7 @@ export const toolkeyCommand: SlashCommand = { kind: CommandKind.BUILT_IN, schema: toolkeySchema, action: async ( - context: CommandContext, + _context: CommandContext, args: string, ): Promise => { // @pseudocode lines 276-278: Parse arguments @@ -99,13 +100,12 @@ export const toolkeyCommand: SlashCommand = { messageType: 'info', content: `${entry.displayName} API key: ${masked}`, }; - } else { - return { - type: 'message', - messageType: 'info', - content: `No API key configured for '${entry.displayName}'`, - }; } + return { + type: 'message', + messageType: 'info', + content: `No API key configured for '${entry.displayName}'`, + }; } // @pseudocode lines 305-308: Clear key (case-insensitive "none") diff --git a/packages/cli/src/ui/commands/toolkeyfileCommand.test.ts b/packages/cli/src/ui/commands/toolkeyfileCommand.test.ts index 159e49c053..b284b96aaf 100644 --- a/packages/cli/src/ui/commands/toolkeyfileCommand.test.ts +++ b/packages/cli/src/ui/commands/toolkeyfileCommand.test.ts @@ -77,7 +77,7 @@ describe('toolkeyfileCommand', () => { describe('schema completion', () => { it('suggests tool names for first argument with descriptions', async () => { const schema = toolkeyfileCommand.schema; - if (!schema) { + if (schema == null) { throw new Error('toolkeyfile schema missing'); } @@ -93,7 +93,7 @@ describe('toolkeyfileCommand', () => { '/toolkeyfile ex', ); - expect(result.suggestions).toEqual( + expect(result.suggestions).toStrictEqual( expect.arrayContaining([expect.objectContaining({ value: 'exa' })]), ); expect(result.hint).toBe('Select built-in tool'); @@ -101,7 +101,7 @@ describe('toolkeyfileCommand', () => { it('suggests none and shows filepath hint after selecting tool', async () => { const schema = toolkeyfileCommand.schema; - if (!schema) { + if (schema == null) { throw new Error('toolkeyfile schema missing'); } @@ -117,7 +117,7 @@ describe('toolkeyfileCommand', () => { '/toolkeyfile exa ', ); - expect(result.suggestions).toEqual( + expect(result.suggestions).toStrictEqual( expect.arrayContaining([expect.objectContaining({ value: 'none' })]), ); expect(result.hint).toBe( diff --git a/packages/cli/src/ui/commands/toolkeyfileCommand.ts b/packages/cli/src/ui/commands/toolkeyfileCommand.ts index 9df8d1a634..b2b6635194 100644 --- a/packages/cli/src/ui/commands/toolkeyfileCommand.ts +++ b/packages/cli/src/ui/commands/toolkeyfileCommand.ts @@ -11,9 +11,9 @@ */ import { - SlashCommand, - CommandContext, - MessageActionReturn, + type SlashCommand, + type CommandContext, + type MessageActionReturn, CommandKind, } from './types.js'; import type { CommandArgumentSchema } from './schema/types.js'; @@ -31,9 +31,10 @@ const toolNameOptions = getSupportedToolNames().map((toolName) => { const entry = getToolKeyEntry(toolName); return { value: toolName, - description: entry - ? `${entry.displayName}: ${entry.description}` - : `Tool ${toolName}`, + description: + entry != null + ? `${entry.displayName}: ${entry.description}` + : `Tool ${toolName}`, }; }); @@ -63,7 +64,7 @@ export const toolkeyfileCommand: SlashCommand = { kind: CommandKind.BUILT_IN, schema: toolkeyfileSchema, action: async ( - context: CommandContext, + _context: CommandContext, args: string, ): Promise => { // @pseudocode lines 327-328: Parse arguments @@ -102,13 +103,12 @@ export const toolkeyfileCommand: SlashCommand = { messageType: 'info', content: `${entry.displayName} keyfile: ${currentPath}`, }; - } else { - return { - type: 'message', - messageType: 'info', - content: `No keyfile configured for '${entry.displayName}'`, - }; } + return { + type: 'message', + messageType: 'info', + content: `No keyfile configured for '${entry.displayName}'`, + }; } // @pseudocode lines 355-358: Clear keyfile (case-insensitive "none") diff --git a/packages/cli/src/ui/commands/toolsCommand.test.ts b/packages/cli/src/ui/commands/toolsCommand.test.ts index b989dd5f23..57eacdfb43 100644 --- a/packages/cli/src/ui/commands/toolsCommand.test.ts +++ b/packages/cli/src/ui/commands/toolsCommand.test.ts @@ -38,7 +38,7 @@ describe('toolsCommand', () => { ui: { addItem: vi.fn() }, }); - if (!toolsCommand.action) throw new Error('Action not defined'); + if (toolsCommand.action == null) throw new Error('Action not defined'); await toolsCommand.action(mockContext, 'list'); expect(mockContext.ui.addItem).toHaveBeenCalledWith( @@ -65,7 +65,7 @@ describe('toolsCommand', () => { ui: { addItem: vi.fn() }, }); - if (!toolsCommand.action) throw new Error('Action not defined'); + if (toolsCommand.action == null) throw new Error('Action not defined'); await toolsCommand.action(mockContext, 'list'); const output = (mockContext.ui.addItem as vi.Mock).mock.calls[0][0].text; @@ -87,10 +87,10 @@ describe('toolsCommand', () => { ui: { addItem: vi.fn() }, }); - if (!toolsCommand.action) throw new Error('Action not defined'); + if (toolsCommand.action == null) throw new Error('Action not defined'); await toolsCommand.action(mockContext, 'disable "File Reader"'); - expect(settings.get('tools.disabled')).toEqual(['file-reader']); + expect(settings.get('tools.disabled')).toStrictEqual(['file-reader']); const output = (mockContext.ui.addItem as vi.Mock).mock.calls[0][0].text; expect(output).toContain("Disabled tool 'File Reader'"); }); @@ -111,7 +111,7 @@ describe('toolsCommand', () => { ui: { addItem: vi.fn() }, }); - if (!toolsCommand.action) throw new Error('Action not defined'); + if (toolsCommand.action == null) throw new Error('Action not defined'); await toolsCommand.action(mockContext, 'disable code-editor'); expect(setToolsSpy).toHaveBeenCalled(); @@ -132,10 +132,10 @@ describe('toolsCommand', () => { ui: { addItem: vi.fn() }, }); - if (!toolsCommand.action) throw new Error('Action not defined'); + if (toolsCommand.action == null) throw new Error('Action not defined'); await toolsCommand.action(mockContext, 'enable code-editor'); - expect(settings.get('tools.disabled')).toEqual([]); + expect(settings.get('tools.disabled')).toStrictEqual([]); const output = (mockContext.ui.addItem as vi.Mock).mock.calls[0][0].text; expect(output).toContain("Enabled tool 'Code Editor'"); }); @@ -153,7 +153,7 @@ describe('toolsCommand', () => { ui: { addItem: vi.fn() }, }); - if (!toolsCommand.action) throw new Error('Action not defined'); + if (toolsCommand.action == null) throw new Error('Action not defined'); await toolsCommand.action(mockContext, 'disable missing'); const output = (mockContext.ui.addItem as vi.Mock).mock.calls[0][0]; @@ -176,11 +176,11 @@ describe('toolsCommand', () => { ui: { addItem: vi.fn() }, }); - if (!toolsCommand.action) throw new Error('Action not defined'); + if (toolsCommand.action == null) throw new Error('Action not defined'); await toolsCommand.action(mockContext, 'enable code-editor'); - expect(settings.get('tools.disabled')).toEqual([]); - expect(settings.get('tools.allowed')).toEqual([]); + expect(settings.get('tools.disabled')).toStrictEqual([]); + expect(settings.get('tools.allowed')).toStrictEqual([]); }); it('enabling a tool preserves existing allowed whitelist', async () => { @@ -202,11 +202,11 @@ describe('toolsCommand', () => { ui: { addItem: vi.fn() }, }); - if (!toolsCommand.action) throw new Error('Action not defined'); + if (toolsCommand.action == null) throw new Error('Action not defined'); await toolsCommand.action(mockContext, 'enable code-editor'); - expect(settings.get('tools.disabled')).toEqual([]); - expect(settings.get('tools.allowed')).toEqual( + expect(settings.get('tools.disabled')).toStrictEqual([]); + expect(settings.get('tools.allowed')).toStrictEqual( expect.arrayContaining(['file-reader', 'code-editor']), ); }); @@ -226,7 +226,7 @@ describe('toolsCommand', () => { ui: { addItem: vi.fn() }, }); - if (!toolsCommand.action) throw new Error('Action not defined'); + if (toolsCommand.action == null) throw new Error('Action not defined'); await toolsCommand.action(mockContext, 'enable file-reader'); (mockContext.ui.addItem as vi.Mock).mockClear(); diff --git a/packages/cli/src/ui/commands/toolsCommand.ts b/packages/cli/src/ui/commands/toolsCommand.ts index 991c16b0ee..ab785bb52a 100644 --- a/packages/cli/src/ui/commands/toolsCommand.ts +++ b/packages/cli/src/ui/commands/toolsCommand.ts @@ -14,7 +14,7 @@ import type { AnyDeclarativeTool, SettingsService, } from '@vybestack/llxprt-code-core'; -import { type CommandArgumentSchema } from './schema/types.js'; +import type { CommandArgumentSchema } from './schema/types.js'; const toolsSchema: CommandArgumentSchema = [ { @@ -46,7 +46,7 @@ function stripQuotes(value: string): string { function getSettingsService(context: CommandContext): SettingsService | null { const config = context.services.config; - if (config && typeof config.getSettingsService === 'function') { + if (config != null && typeof config.getSettingsService === 'function') { return config.getSettingsService(); } return null; @@ -60,10 +60,10 @@ function readToolLists(context: CommandContext): { const config = context.services.config; const read = (key: string): unknown => { - if (settings) { + if (settings != null) { return settings.get(key); } - if (config && typeof config.getEphemeralSetting === 'function') { + if (config != null && typeof config.getEphemeralSetting === 'function') { return config.getEphemeralSetting(key); } return undefined; @@ -97,13 +97,13 @@ function persistToolLists( const config = context.services.config; const settings = getSettingsService(context); - if (settings) { + if (settings != null) { settings.set('tools.disabled', disabledList); settings.set('disabled-tools', disabledList); settings.set('tools.allowed', allowedList); } - if (config) { + if (config != null) { if (typeof config.setEphemeralSetting === 'function') { config.setEphemeralSetting('tools.disabled', disabledList); config.setEphemeralSetting('disabled-tools', disabledList); @@ -196,7 +196,7 @@ export const toolsCommand: SlashCommand = { const config = context.services.config; const toolRegistry = config?.getToolRegistry(); - if (!toolRegistry) { + if (toolRegistry == null) { context.ui.addItem( { type: MessageType.ERROR, @@ -247,7 +247,7 @@ export const toolsCommand: SlashCommand = { } const target = resolveToolByName(identifier, tools); - if (!target || 'serverName' in target) { + if (target == null || 'serverName' in target) { context.ui.addItem( { type: MessageType.ERROR, @@ -280,7 +280,7 @@ export const toolsCommand: SlashCommand = { ? config.getGeminiClient() : undefined; - if (geminiClient && typeof geminiClient.setTools === 'function') { + if (geminiClient != null && typeof geminiClient.setTools === 'function') { try { await geminiClient.setTools(); } catch (error) { diff --git a/packages/cli/src/ui/commands/vimCommand.ts b/packages/cli/src/ui/commands/vimCommand.ts index 43847c90a5..73cd81014f 100644 --- a/packages/cli/src/ui/commands/vimCommand.ts +++ b/packages/cli/src/ui/commands/vimCommand.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { CommandKind, SlashCommand } from './types.js'; +import { CommandKind, type SlashCommand } from './types.js'; export const vimCommand: SlashCommand = { name: 'vim', diff --git a/packages/cli/src/ui/components/AboutBox.tsx b/packages/cli/src/ui/components/AboutBox.tsx index fc43b7c8da..d794f3c70f 100644 --- a/packages/cli/src/ui/components/AboutBox.tsx +++ b/packages/cli/src/ui/components/AboutBox.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { GIT_COMMIT_INFO } from '../../generated/git-commit.js'; diff --git a/packages/cli/src/ui/components/AnsiOutput.tsx b/packages/cli/src/ui/components/AnsiOutput.tsx index 57b90dea15..7a7e865cc0 100644 --- a/packages/cli/src/ui/components/AnsiOutput.tsx +++ b/packages/cli/src/ui/components/AnsiOutput.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import type { AnsiLine, diff --git a/packages/cli/src/ui/components/AppHeader.tsx b/packages/cli/src/ui/components/AppHeader.tsx index ccad4ed857..1d5312c1a1 100644 --- a/packages/cli/src/ui/components/AppHeader.tsx +++ b/packages/cli/src/ui/components/AppHeader.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import type { Config } from '@vybestack/llxprt-code-core'; import { SemanticColors } from '../colors.js'; diff --git a/packages/cli/src/ui/components/AuthDialog.tsx b/packages/cli/src/ui/components/AuthDialog.tsx index 9dcd9d9775..353bdf8d51 100644 --- a/packages/cli/src/ui/components/AuthDialog.tsx +++ b/packages/cli/src/ui/components/AuthDialog.tsx @@ -8,7 +8,7 @@ import React, { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; +import { type LoadedSettings, SettingScope } from '../../config/settings.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { useRuntimeApi } from '../contexts/RuntimeContext.js'; @@ -102,7 +102,7 @@ export function AuthDialog({ // This will call the same code as /auth gemini enable/disable const oauthManager = runtime.getCliOAuthManager(); - if (oauthManager) { + if (oauthManager != null) { try { const newState = await oauthManager.toggleOAuthEnabled(providerName); diff --git a/packages/cli/src/ui/components/AuthInProgress.tsx b/packages/cli/src/ui/components/AuthInProgress.tsx index 85da5d9501..7ed27657f5 100644 --- a/packages/cli/src/ui/components/AuthInProgress.tsx +++ b/packages/cli/src/ui/components/AuthInProgress.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect } from 'react'; +import type React from 'react'; +import { useState, useEffect } from 'react'; import { Box, Text } from 'ink'; import Spinner from 'ink-spinner'; import { Colors } from '../colors.js'; diff --git a/packages/cli/src/ui/components/AutoAcceptIndicator.tsx b/packages/cli/src/ui/components/AutoAcceptIndicator.tsx index c711b29f18..14b112f78d 100644 --- a/packages/cli/src/ui/components/AutoAcceptIndicator.tsx +++ b/packages/cli/src/ui/components/AutoAcceptIndicator.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { ApprovalMode } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/ui/components/BucketAuthConfirmation.tsx b/packages/cli/src/ui/components/BucketAuthConfirmation.tsx index 79d15a2b20..3109e73ff4 100644 --- a/packages/cli/src/ui/components/BucketAuthConfirmation.tsx +++ b/packages/cli/src/ui/components/BucketAuthConfirmation.tsx @@ -4,17 +4,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState } from 'react'; +import type React from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { Box, Text } from 'ink'; import { MessageBusType, - BucketAuthConfirmationRequest, + type BucketAuthConfirmationRequest, DebugLogger, + type MessageBus, } from '@vybestack/llxprt-code-core'; -import type { MessageBus } from '@vybestack/llxprt-code-core'; import { RadioButtonSelect, - RadioSelectItem, + type RadioSelectItem, } from './shared/RadioButtonSelect.js'; import { Colors } from '../colors.js'; @@ -59,7 +60,7 @@ export const BucketAuthConfirmation: React.FC = ({ logger.debug('BucketAuthConfirmation useEffect running', { hasMessageBus: !!messageBus, }); - if (!messageBus) { + if (messageBus == null) { logger.debug('No message bus available, skipping subscription'); return; } @@ -92,7 +93,7 @@ export const BucketAuthConfirmation: React.FC = ({ const handleConfirm = useCallback( (confirmed: boolean) => { - if (!pendingRequest || !messageBus) { + if (pendingRequest == null || messageBus == null) { return; } @@ -108,7 +109,7 @@ export const BucketAuthConfirmation: React.FC = ({ useKeypress( (key) => { - if (!pendingRequest || !isFocused) return; + if (pendingRequest == null || !isFocused) return; if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { handleConfirm(false); } @@ -123,7 +124,7 @@ export const BucketAuthConfirmation: React.FC = ({ [handleConfirm], ); - if (!pendingRequest) { + if (pendingRequest == null) { return null; } diff --git a/packages/cli/src/ui/components/CacheStatsDisplay.test.tsx b/packages/cli/src/ui/components/CacheStatsDisplay.test.tsx index 0a2c42c953..b98299fbad 100644 --- a/packages/cli/src/ui/components/CacheStatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/CacheStatsDisplay.test.tsx @@ -22,11 +22,12 @@ vi.mock('../contexts/RuntimeContext.js', async (importOriginal) => { const useRuntimeApiMock = vi.mocked(RuntimeContext.useRuntimeApi); const renderWithMockedCacheStats = (cacheStats: CacheStatistics | null) => { - const mockProviderManager = cacheStats - ? { - getCacheStatistics: vi.fn().mockReturnValue(cacheStats), - } - : null; + const mockProviderManager = + cacheStats != null + ? { + getCacheStatistics: vi.fn().mockReturnValue(cacheStats), + } + : null; useRuntimeApiMock.mockReturnValue({ getCliProviderManager: vi.fn().mockReturnValue(mockProviderManager), diff --git a/packages/cli/src/ui/components/CacheStatsDisplay.tsx b/packages/cli/src/ui/components/CacheStatsDisplay.tsx index aed6309c3a..df89c86796 100644 --- a/packages/cli/src/ui/components/CacheStatsDisplay.tsx +++ b/packages/cli/src/ui/components/CacheStatsDisplay.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import type { CacheStatistics } from '@vybestack/llxprt-code-core'; import { Colors } from '../colors.js'; @@ -59,7 +59,7 @@ export const CacheStatsDisplay: React.FC = () => { } ).getCacheStatistics?.() ?? null; - if (!cacheStats) { + if (cacheStats == null) { return ( ({ diff --git a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx index c2dcacb931..367529949a 100644 --- a/packages/cli/src/ui/components/ContextSummaryDisplay.tsx +++ b/packages/cli/src/ui/components/ContextSummaryDisplay.tsx @@ -7,10 +7,7 @@ import type React from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; -import { - type IdeContext, - type MCPServerConfig, -} from '@vybestack/llxprt-code-core'; +import type { IdeContext, MCPServerConfig } from '@vybestack/llxprt-code-core'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { isNarrowWidth } from '../utils/isNarrowWidth.js'; diff --git a/packages/cli/src/ui/components/ContextUsageDisplay.semantic.test.tsx b/packages/cli/src/ui/components/ContextUsageDisplay.semantic.test.tsx index 951283d77b..56942f338f 100644 --- a/packages/cli/src/ui/components/ContextUsageDisplay.semantic.test.tsx +++ b/packages/cli/src/ui/components/ContextUsageDisplay.semantic.test.tsx @@ -13,7 +13,7 @@ import { DefaultDark } from '../themes/default.js'; // Mock the tokenLimit function vi.mock('@vybestack/llxprt-code-core', () => ({ tokenLimit: vi.fn( - (model: string, contextLimit?: number) => contextLimit || 100000, // Default 100k tokens + (_model: string, contextLimit?: number) => contextLimit || 100000, // Default 100k tokens ), })); diff --git a/packages/cli/src/ui/components/DebugProfiler.tsx b/packages/cli/src/ui/components/DebugProfiler.tsx index 808ffe0c9e..63cdf3c10d 100644 --- a/packages/cli/src/ui/components/DebugProfiler.tsx +++ b/packages/cli/src/ui/components/DebugProfiler.tsx @@ -12,7 +12,7 @@ import { useUIState } from '../contexts/UIStateContext.js'; import { debugState } from '../debug.js'; import { appEvents, AppEvent } from '../../utils/events.js'; import { coreEvents, CoreEvent } from '@vybestack/llxprt-code-core'; -import { EventEmitter } from 'node:events'; +import type { EventEmitter } from 'node:events'; // Frames that render at least this far before or after an action are considered // idle frames. @@ -139,7 +139,7 @@ export const DebugProfiler = () => { // These events are expected to trigger UI renders. // Cast to base EventEmitter to allow generic event name iteration. const coreEventsBase = coreEvents as unknown as EventEmitter; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const appEventsBase = appEvents as unknown as EventEmitter; for (const eventName of Object.values(CoreEvent)) { coreEventsBase.on(eventName, handler); diff --git a/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx b/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx index 49cf70b653..c292d108e0 100644 --- a/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx +++ b/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx @@ -4,10 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; -import { ConsoleMessageItem } from '../types.js'; +import type { ConsoleMessageItem } from '../types.js'; import { MaxSizedBox } from './shared/MaxSizedBox.js'; interface DetailedMessagesDisplayProps { diff --git a/packages/cli/src/ui/components/EditorSettingsDialog.tsx b/packages/cli/src/ui/components/EditorSettingsDialog.tsx index 1ccc8ed8cf..43794ce45c 100644 --- a/packages/cli/src/ui/components/EditorSettingsDialog.tsx +++ b/packages/cli/src/ui/components/EditorSettingsDialog.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback } from 'react'; +import type React from 'react'; +import { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { @@ -13,9 +14,9 @@ import { type EditorDisplay, } from '../editors/editorSettingsManager.js'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; +import { type LoadedSettings, SettingScope } from '../../config/settings.js'; import { - EditorType, + type EditorType, isEditorAvailable, debugLogger, } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/ui/components/ErrorBoundary.tsx b/packages/cli/src/ui/components/ErrorBoundary.tsx index 764c79b7d5..6819cf9ae2 100644 --- a/packages/cli/src/ui/components/ErrorBoundary.tsx +++ b/packages/cli/src/ui/components/ErrorBoundary.tsx @@ -5,7 +5,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { Component, ErrorInfo, ReactNode } from 'react'; +import type React from 'react'; +import { Component, type ErrorInfo, type ReactNode } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; @@ -87,7 +88,7 @@ export class ErrorBoundary extends Component { console.error('Error count in window:', this.errorTimestamps.length); // Call custom error handler if provided - if (this.props.onError) { + if (this.props.onError != null) { this.props.onError(error, errorInfo); } @@ -114,9 +115,9 @@ export class ErrorBoundary extends Component { } override render() { - if (this.state.hasError && this.state.error) { + if (this.state.hasError && this.state.error != null) { // Use custom fallback if provided - if (this.props.fallback && this.state.errorInfo) { + if (this.props.fallback != null && this.state.errorInfo != null) { return this.props.fallback(this.state.error, this.state.errorInfo); } diff --git a/packages/cli/src/ui/components/FolderTrustDialog.tsx b/packages/cli/src/ui/components/FolderTrustDialog.tsx index 2edd3bd454..67de74752a 100644 --- a/packages/cli/src/ui/components/FolderTrustDialog.tsx +++ b/packages/cli/src/ui/components/FolderTrustDialog.tsx @@ -5,12 +5,13 @@ */ import { Box, Text } from 'ink'; -import React, { useState } from 'react'; +import type React from 'react'; +import { useState } from 'react'; import { Colors } from '../colors.js'; import { theme } from '../semantic-colors.js'; import { RadioButtonSelect, - RadioSelectItem, + type RadioSelectItem, } from './shared/RadioButtonSelect.js'; import { useKeypress } from '../hooks/useKeypress.js'; import * as process from 'node:process'; diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index 7651a51c91..8735db2208 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -431,7 +431,7 @@ export const Footer = React.memo( const lbProvider = providerManager.getProviderByName('load-balancer'); if ( - lbProvider && + lbProvider != null && 'getStats' in lbProvider && typeof (lbProvider as { getStats?: () => unknown }) .getStats === 'function' diff --git a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx index 867799e723..079ecdd8a0 100644 --- a/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx +++ b/packages/cli/src/ui/components/GeminiRespondingSpinner.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Text, useIsScreenReaderEnabled } from 'ink'; import Spinner from 'ink-spinner'; import type { SpinnerName } from 'cli-spinners'; diff --git a/packages/cli/src/ui/components/Header.tsx b/packages/cli/src/ui/components/Header.tsx index 85445de119..8de38c3dfc 100644 --- a/packages/cli/src/ui/components/Header.tsx +++ b/packages/cli/src/ui/components/Header.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors, SemanticColors } from '../colors.js'; import { shortAsciiLogo, longAsciiLogo } from './AsciiArt.js'; @@ -43,7 +43,7 @@ export const Header: React.FC = ({ flexShrink={0} flexDirection="column" > - {Colors.GradientColors ? ( + {Colors.GradientColors != null ? ( {displayTitle} diff --git a/packages/cli/src/ui/components/Help.tsx b/packages/cli/src/ui/components/Help.tsx index 43086da8ff..6ec64aeb7b 100644 --- a/packages/cli/src/ui/components/Help.tsx +++ b/packages/cli/src/ui/components/Help.tsx @@ -4,10 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; -import { SlashCommand } from '../commands/types.js'; +import type { SlashCommand } from '../commands/types.js'; import { KEYBOARD_SHORTCUTS_URL } from '../constants.js'; interface Help { @@ -76,16 +76,15 @@ export const Help: React.FC = ({ commands }) => ( {command.description && ' - ' + command.description} - {command.subCommands && - command.subCommands.map((subCommand) => ( - - - {' '} - {subCommand.name} - - {subCommand.description && ' - ' + subCommand.description} + {command.subCommands?.map((subCommand) => ( + + + {' '} + {subCommand.name} - ))} + {subCommand.description && ' - ' + subCommand.description} + + ))} ))} diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index 88d97db627..6b03f2ede6 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -28,7 +28,7 @@ import { CacheStatsDisplay } from './CacheStatsDisplay.js'; import { LBStatsDisplay } from './LBStatsDisplay.js'; import { SessionSummaryDisplay } from './SessionSummaryDisplay.js'; import { Help } from './Help.js'; -import { Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; import type { SlashCommand } from '../commands/types.js'; import { ChatList } from './views/ChatList.js'; import { ExtensionsList } from './views/ExtensionsList.js'; diff --git a/packages/cli/src/ui/components/HookStatusDisplay.tsx b/packages/cli/src/ui/components/HookStatusDisplay.tsx index 8bffe8b83b..687c99f73c 100644 --- a/packages/cli/src/ui/components/HookStatusDisplay.tsx +++ b/packages/cli/src/ui/components/HookStatusDisplay.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import type { ActiveHook } from '../types.js'; diff --git a/packages/cli/src/ui/components/InputPrompt.paste.spec.tsx b/packages/cli/src/ui/components/InputPrompt.paste.spec.tsx index 0fcdbacdeb..c4e383df5e 100644 --- a/packages/cli/src/ui/components/InputPrompt.paste.spec.tsx +++ b/packages/cli/src/ui/components/InputPrompt.paste.spec.tsx @@ -122,9 +122,9 @@ import { render } from 'ink-testing-library'; import { act } from 'react-dom/test-utils'; import { InputPrompt } from './InputPrompt.js'; import { AppDispatchProvider } from '../contexts/AppDispatchContext.js'; -import { TextBuffer } from './shared/text-buffer.js'; -import { CommandContext } from '../commands/types.js'; -import { Config } from '@vybestack/llxprt-code-core'; +import type { TextBuffer } from './shared/text-buffer.js'; +import type { CommandContext } from '../commands/types.js'; +import type { Config } from '@vybestack/llxprt-code-core'; import clipboardy from 'clipboardy'; import * as clipboardUtils from '../utils/clipboardUtils.js'; import { useMouse, type MouseEvent } from '../hooks/useMouse.js'; @@ -207,7 +207,7 @@ describe('InputPrompt paste functionality', () => { sendKey = async (key: Record) => { const handler = keypressHandler; - if (!handler) { + if (handler == null) { throw new Error('keypressHandler not initialized'); } await act(async () => { diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 28a83573af..3edbac53a3 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -601,8 +601,8 @@ describe('InputPrompt', () => { mockedUseCommandCompletion.mockReturnValue({ ...mockCommandCompletion, showSuggestions: true, - suggestions, activeSuggestionIndex: activeIndex, + suggestions, }); props.buffer.setText(bufferText); const { stdin, unmount } = renderWithProviders(); @@ -1213,10 +1213,10 @@ describe('InputPrompt', () => { mockedUseCommandCompletion.mockReturnValue({ ...mockCommandCompletion, - showSuggestions, suggestions: showSuggestions ? [{ label: 'suggestion', value: 'suggestion' }] : [], + showSuggestions, }); const { unmount } = renderWithProviders(); @@ -1941,7 +1941,7 @@ describe('InputPrompt', () => { }); mockedUseReverseSearchCompletion.mockImplementation( - (buffer, shellHistory, reverseSearchActive) => ({ + (_buffer, _shellHistory, reverseSearchActive) => ({ ...mockReverseSearchCompletion, suggestions: reverseSearchActive ? [ @@ -2027,7 +2027,7 @@ describe('InputPrompt', () => { // Mock the reverse search completion to be active and then reset mockedUseReverseSearchCompletion.mockImplementation( - (buffer, shellHistory, reverseSearchActiveFromInputPrompt) => ({ + (_buffer, _shellHistory, reverseSearchActiveFromInputPrompt) => ({ ...mockReverseSearchCompletion, suggestions: reverseSearchActiveFromInputPrompt ? [{ label: 'history item', value: 'history item' }] @@ -2057,7 +2057,7 @@ describe('InputPrompt', () => { await waitFor(() => { expect(stdout.lastFrame()).not.toContain('(r:)'); expect(props.buffer.text).toBe(initialText); - expect(props.buffer.cursor).toEqual(initialCursor); + expect(props.buffer.cursor).toStrictEqual(initialCursor); }); unmount(); @@ -2109,7 +2109,7 @@ describe('InputPrompt', () => { props.shellModeActive = false; vi.mocked(useReverseSearchCompletion).mockImplementation( - (buffer, data, isActive) => ({ + (_buffer, _data, isActive) => ({ ...mockReverseSearchCompletion, suggestions: isActive ? [ @@ -2293,8 +2293,6 @@ describe('InputPrompt', () => { const mockAccept = vi.fn(); mockedUseCommandCompletion.mockReturnValue({ ...mockCommandCompletion, - showSuggestions, - suggestions, promptCompletion: { text: ghostText, accept: mockAccept, @@ -2303,6 +2301,8 @@ describe('InputPrompt', () => { isActive: ghostText !== '', markSelected: vi.fn(), }, + showSuggestions, + suggestions, }); const { stdin, unmount } = renderWithProviders( diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 01933b41fd..b0b7108cd3 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -4,13 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useState, useRef } from 'react'; +import type React from 'react'; +import { useCallback, useEffect, useState, useRef } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { Colors } from '../colors.js'; import { SuggestionsDisplay } from './SuggestionsDisplay.js'; import { useInputHistory } from '../hooks/useInputHistory.js'; -import { TextBuffer, logicalPosToOffset } from './shared/text-buffer.js'; +import { type TextBuffer, logicalPosToOffset } from './shared/text-buffer.js'; import { cpSlice, cpLen, @@ -22,7 +23,7 @@ import { useShellHistory } from '../hooks/useShellHistory.js'; import { useReverseSearchCompletion } from '../hooks/useReverseSearchCompletion.js'; import { useCommandCompletion } from '../hooks/useCommandCompletion.js'; import { useShellPathCompletion } from '../hooks/useShellPathCompletion.js'; -import { useKeypress, Key } from '../hooks/useKeypress.js'; +import { useKeypress, type Key } from '../hooks/useKeypress.js'; import { keyMatchers, Command } from '../keyMatchers.js'; import type { CommandContext, SlashCommand } from '../commands/types.js'; import type { Config, ApprovalMode } from '@vybestack/llxprt-code-core'; @@ -194,7 +195,7 @@ export const InputPrompt: React.FC = ({ ]); const resetEscapeState = useCallback(() => { - if (escapeTimerRef.current) { + if (escapeTimerRef.current != null) { clearTimeout(escapeTimerRef.current); escapeTimerRef.current = null; } @@ -204,7 +205,7 @@ export const InputPrompt: React.FC = ({ // Notify parent component about escape prompt state changes useEffect(() => { - if (onEscapePromptChange) { + if (onEscapePromptChange != null) { onEscapePromptChange(showEscapePrompt); } }, [showEscapePrompt, onEscapePromptChange]); @@ -212,7 +213,7 @@ export const InputPrompt: React.FC = ({ // Clear escape prompt timer on unmount useEffect( () => () => { - if (escapeTimerRef.current) { + if (escapeTimerRef.current != null) { clearTimeout(escapeTimerRef.current); } }, @@ -433,7 +434,7 @@ export const InputPrompt: React.FC = ({ return; } - if (vimHandleInput && vimHandleInput(key)) { + if (vimHandleInput?.(key)) { return; } @@ -491,7 +492,7 @@ export const InputPrompt: React.FC = ({ } escPressCount.current = 1; setShowEscapePrompt(true); - if (escapeTimerRef.current) { + if (escapeTimerRef.current != null) { clearTimeout(escapeTimerRef.current); } escapeTimerRef.current = setTimeout(() => { @@ -617,7 +618,7 @@ export const InputPrompt: React.FC = ({ completion.getCommandFromSuggestion(targetIndex); if ( isAutoExecutableCommand(command) && - !command?.completion + command?.completion == null ) { const completedText = completion.handleAutocomplete(targetIndex); diff --git a/packages/cli/src/ui/components/LBStatsDisplay.tsx b/packages/cli/src/ui/components/LBStatsDisplay.tsx index a5e3eba0cb..7ac3415964 100644 --- a/packages/cli/src/ui/components/LBStatsDisplay.tsx +++ b/packages/cli/src/ui/components/LBStatsDisplay.tsx @@ -7,7 +7,7 @@ * Issue #489 Phase 8 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import type { ExtendedLoadBalancerStats } from '@vybestack/llxprt-code-core'; import { Colors } from '../colors.js'; @@ -80,7 +80,7 @@ export const LBStatsDisplay: React.FC = () => { // Check if provider exists and has getStats method if ( - !lbProvider || + lbProvider == null || !('getStats' in lbProvider) || typeof lbProvider.getStats !== 'function' ) { diff --git a/packages/cli/src/ui/components/LayoutManager.tsx b/packages/cli/src/ui/components/LayoutManager.tsx index 06b025090b..dbc320a5ba 100644 --- a/packages/cli/src/ui/components/LayoutManager.tsx +++ b/packages/cli/src/ui/components/LayoutManager.tsx @@ -4,16 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { +import type React from 'react'; +import { createContext, useContext, useState, useMemo, useEffect, useRef, - RefObject, + type RefObject, } from 'react'; -import { DOMElement, measureElement } from 'ink'; +import { type DOMElement, measureElement } from 'ink'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; interface LayoutContextValue { @@ -32,7 +33,7 @@ const LayoutContext = createContext(undefined); export const useLayout = () => { const context = useContext(LayoutContext); - if (!context) { + if (context == null) { throw new Error('useLayout must be used within LayoutManager'); } return context; @@ -56,7 +57,7 @@ export const LayoutManager: React.FC = ({ children }) => { // Measure footer element when it changes useEffect(() => { - if (footerRef.current) { + if (footerRef.current != null) { const measurement = measureElement(footerRef.current); setFooterHeight(measurement.height); } diff --git a/packages/cli/src/ui/components/LoadProfileDialog.tsx b/packages/cli/src/ui/components/LoadProfileDialog.tsx index dfbe0e9b0e..9133a89a95 100644 --- a/packages/cli/src/ui/components/LoadProfileDialog.tsx +++ b/packages/cli/src/ui/components/LoadProfileDialog.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; +import type React from 'react'; +import { useState } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { useKeypress } from '../hooks/useKeypress.js'; diff --git a/packages/cli/src/ui/components/LoadingIndicator.tsx b/packages/cli/src/ui/components/LoadingIndicator.tsx index e9541ba643..63904fe85b 100644 --- a/packages/cli/src/ui/components/LoadingIndicator.tsx +++ b/packages/cli/src/ui/components/LoadingIndicator.tsx @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ThoughtSummary } from '@vybestack/llxprt-code-core'; -import React from 'react'; +import type { ThoughtSummary } from '@vybestack/llxprt-code-core'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { useStreamingContext } from '../contexts/StreamingContext.js'; diff --git a/packages/cli/src/ui/components/LoggingDialog.tsx b/packages/cli/src/ui/components/LoggingDialog.tsx index 3b0f736a3c..8fe75b983b 100644 --- a/packages/cli/src/ui/components/LoggingDialog.tsx +++ b/packages/cli/src/ui/components/LoggingDialog.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useMemo, useEffect } from 'react'; +import type React from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { Box, Text } from 'ink'; import { SemanticColors } from '../colors.js'; import { useResponsive } from '../hooks/useResponsive.js'; @@ -54,17 +55,16 @@ const formatTimestamp = (timestamp: string, isNarrow: boolean): string => { minute: '2-digit', hour12: false, }); - } else { - // Full format for wider screens - return date.toLocaleString('en-US', { - month: 'short', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false, - }); } + // Full format for wider screens + return date.toLocaleString('en-US', { + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }); }; const formatContent = (content: string, maxLength: number): string => { @@ -82,7 +82,7 @@ const formatTokenCount = (tokens?: { input?: number; output?: number; }): string => { - if (!tokens) return ''; + if (tokens == null) return ''; const parts: string[] = []; if (tokens.input) parts.push(`in:${tokens.input}`); if (tokens.output) parts.push(`out:${tokens.output}`); @@ -191,7 +191,7 @@ export const LoggingDialog: React.FC = ({ // Format the main content let mainContent = ''; - if (entry.type === 'request' && entry.messages) { + if (entry.type === 'request' && entry.messages != null) { const lastMessage = entry.messages[entry.messages.length - 1]; if (lastMessage) { mainContent = formatContent(lastMessage.content, contentWidth); @@ -208,7 +208,7 @@ export const LoggingDialog: React.FC = ({ toolContent += ' FAILED'; } // Add git stats if present - if (entry.gitStats) { + if (entry.gitStats != null) { const { linesAdded, linesRemoved, filesChanged } = entry.gitStats; toolContent += ` [+${linesAdded} -${linesRemoved} in ${filesChanged} files]`; } diff --git a/packages/cli/src/ui/components/MemoryUsageDisplay.tsx b/packages/cli/src/ui/components/MemoryUsageDisplay.tsx index 283dfee059..1e3a10c944 100644 --- a/packages/cli/src/ui/components/MemoryUsageDisplay.tsx +++ b/packages/cli/src/ui/components/MemoryUsageDisplay.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useState } from 'react'; +import type React from 'react'; +import { useEffect, useState } from 'react'; import { Box, Text } from 'ink'; import { SemanticColors } from '../colors.js'; import process from 'node:process'; diff --git a/packages/cli/src/ui/components/ModelDialog.tsx b/packages/cli/src/ui/components/ModelDialog.tsx index 34760256ed..5d0e221a4e 100644 --- a/packages/cli/src/ui/components/ModelDialog.tsx +++ b/packages/cli/src/ui/components/ModelDialog.tsx @@ -4,19 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { - useState, - useMemo, - useEffect, - useCallback, - useRef, -} from 'react'; +import type React from 'react'; +import { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { Box, Text } from 'ink'; import { SemanticColors } from '../colors.js'; import { useResponsive } from '../hooks/useResponsive.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { useRuntimeApi } from '../contexts/RuntimeContext.js'; -import { type HydratedModel } from '@vybestack/llxprt-code-core'; +import type { HydratedModel } from '@vybestack/llxprt-code-core'; export interface CapabilityFilters { vision: boolean; @@ -401,7 +396,7 @@ export const ModelsDialog: React.FC = ({ key.sequence.length === 1 && !key.ctrl && !key.meta && - key.sequence.match(/[\x20-\x7E]/) + RegExp(/[\x20-\x7E]/).exec(key.sequence) != null ) { setState((prev) => ({ ...prev, diff --git a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx index 93cbe9e7f1..b11596e3f2 100644 --- a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx @@ -27,9 +27,9 @@ const renderWithMockedStats = (metrics: SessionMetrics) => { stats: { sessionId: 'test-session', sessionStartTime: new Date(), - metrics, lastPromptTokenCount: 0, promptCount: 5, + metrics, }, getPromptCount: () => 5, diff --git a/packages/cli/src/ui/components/Notifications.tsx b/packages/cli/src/ui/components/Notifications.tsx index eb92b710f2..eb8e17cdf9 100644 --- a/packages/cli/src/ui/components/Notifications.tsx +++ b/packages/cli/src/ui/components/Notifications.tsx @@ -83,7 +83,7 @@ export const Notifications = ({ if ( !showStartupWarnings && !showInitError && - !updateInfo && + updateInfo == null && !showScreenReaderNudge ) { return null; diff --git a/packages/cli/src/ui/components/OAuthCodeDialog.tsx b/packages/cli/src/ui/components/OAuthCodeDialog.tsx index bf3f9eb1d8..72675b0c36 100644 --- a/packages/cli/src/ui/components/OAuthCodeDialog.tsx +++ b/packages/cli/src/ui/components/OAuthCodeDialog.tsx @@ -10,10 +10,11 @@ * Allows users to paste authorization code from browser */ -import React, { useState, useCallback } from 'react'; +import type React from 'react'; +import { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; -import { useKeypress, Key } from '../hooks/useKeypress.js'; +import { useKeypress, type Key } from '../hooks/useKeypress.js'; interface OAuthCodeDialogProps { provider: string; @@ -45,12 +46,11 @@ export const OAuthCodeDialog: React.FC = ({ 'Please paste it into your browser to authenticate with Google.', 'After authenticating, paste the verification code you receive below:', ]; - } else { - return [ - 'Please check your browser and authorize the application.', - 'After authorizing, paste the authorization code below:', - ]; } + return [ + 'Please check your browser and authorize the application.', + 'After authorizing, paste the authorization code below:', + ]; }, [provider]); /** @@ -91,7 +91,6 @@ export const OAuthCodeDialog: React.FC = ({ // Replace the entire code with the pasted content (don't append) setCode(cleanInput); } - return; } // Explicitly ignore ALL other input including: diff --git a/packages/cli/src/ui/components/PermissionsModifyTrustDialog.test.tsx b/packages/cli/src/ui/components/PermissionsModifyTrustDialog.test.tsx index ba447c8d43..f66790ecbf 100644 --- a/packages/cli/src/ui/components/PermissionsModifyTrustDialog.test.tsx +++ b/packages/cli/src/ui/components/PermissionsModifyTrustDialog.test.tsx @@ -7,7 +7,7 @@ import { renderWithProviders, waitFor } from '../../test-utils/render.js'; import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js'; -import React from 'react'; +import type React from 'react'; import { SettingsContext } from '../contexts/SettingsContext.js'; const mockedExit = vi.hoisted(() => vi.fn()); diff --git a/packages/cli/src/ui/components/PermissionsModifyTrustDialog.tsx b/packages/cli/src/ui/components/PermissionsModifyTrustDialog.tsx index e90a46a424..2fa06f5353 100644 --- a/packages/cli/src/ui/components/PermissionsModifyTrustDialog.tsx +++ b/packages/cli/src/ui/components/PermissionsModifyTrustDialog.tsx @@ -4,19 +4,20 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useState, useMemo } from 'react'; +import type React from 'react'; +import { useCallback, useState, useMemo } from 'react'; import { Box, Text } from 'ink'; import * as path from 'node:path'; import { Colors } from '../colors.js'; import { RadioButtonSelect, - RadioSelectItem, + type RadioSelectItem, } from './shared/RadioButtonSelect.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { usePermissionsModifyTrust } from '../hooks/usePermissionsModifyTrust.js'; import { TrustLevel } from '../../config/trustedFolders.js'; -import { HistoryItemWithoutId, MessageType } from '../types.js'; -import { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js'; +import { type HistoryItemWithoutId, MessageType } from '../types.js'; +import type { UseHistoryManagerReturn } from '../hooks/useHistoryManager.js'; interface PermissionsModifyTrustDialogProps { onExit: () => void; diff --git a/packages/cli/src/ui/components/PrepareLabel.tsx b/packages/cli/src/ui/components/PrepareLabel.tsx index 8fdf101d8a..f4b0224f3a 100644 --- a/packages/cli/src/ui/components/PrepareLabel.tsx +++ b/packages/cli/src/ui/components/PrepareLabel.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Text } from 'ink'; import { Colors } from '../colors.js'; diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/AdvancedParamsStep.tsx b/packages/cli/src/ui/components/ProfileCreateWizard/AdvancedParamsStep.tsx index 298f5b737f..f731660f9d 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/AdvancedParamsStep.tsx +++ b/packages/cli/src/ui/components/ProfileCreateWizard/AdvancedParamsStep.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback } from 'react'; +import type React from 'react'; +import { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/AuthenticationStep.tsx b/packages/cli/src/ui/components/ProfileCreateWizard/AuthenticationStep.tsx index 3de06dede4..c828fc5a8f 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/AuthenticationStep.tsx +++ b/packages/cli/src/ui/components/ProfileCreateWizard/AuthenticationStep.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback } from 'react'; +import type React from 'react'; +import { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/BaseUrlConfigStep.tsx b/packages/cli/src/ui/components/ProfileCreateWizard/BaseUrlConfigStep.tsx index 0b526220cf..65668242c2 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/BaseUrlConfigStep.tsx +++ b/packages/cli/src/ui/components/ProfileCreateWizard/BaseUrlConfigStep.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback } from 'react'; +import type React from 'react'; +import { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { TextInput } from './TextInput.js'; diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/ModelSelectStep.tsx b/packages/cli/src/ui/components/ProfileCreateWizard/ModelSelectStep.tsx index 435e5a40a4..ef31bb70cf 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/ModelSelectStep.tsx +++ b/packages/cli/src/ui/components/ProfileCreateWizard/ModelSelectStep.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback, useEffect } from 'react'; +import type React from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/NavigationMenu.tsx b/packages/cli/src/ui/components/ProfileCreateWizard/NavigationMenu.tsx index 8a7d823d84..e89579651a 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/NavigationMenu.tsx +++ b/packages/cli/src/ui/components/ProfileCreateWizard/NavigationMenu.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback } from 'react'; +import type React from 'react'; +import { useState, useCallback } from 'react'; import { Box } from 'ink'; import { RadioButtonSelect, @@ -34,9 +35,9 @@ export const NavigationMenu: React.FC = ({ const handleSelect = useCallback( (value: string) => { - if (value === 'continue' && onContinue && !continueDisabled) { + if (value === 'continue' && onContinue != null && !continueDisabled) { onContinue(); - } else if (value === 'back' && onBack) { + } else if (value === 'back' && onBack != null) { onBack(); } else if (value === 'cancel') { setShowCancelConfirm(true); @@ -75,7 +76,7 @@ export const NavigationMenu: React.FC = ({ const items: Array> = []; - if (onContinue) { + if (onContinue != null) { items.push({ label: continueDisabled ? `${continueLabel} (disabled)` : continueLabel, value: 'continue', @@ -83,7 +84,7 @@ export const NavigationMenu: React.FC = ({ }); } - if (onBack && showBack) { + if (onBack != null && showBack) { items.push({ label: 'Back', value: 'back', key: 'back' }); } diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/ProfileSaveStep.tsx b/packages/cli/src/ui/components/ProfileCreateWizard/ProfileSaveStep.tsx index d2afcf4ee6..aa3ec70d9d 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/ProfileSaveStep.tsx +++ b/packages/cli/src/ui/components/ProfileCreateWizard/ProfileSaveStep.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback, useEffect } from 'react'; +import type React from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { promises as fs } from 'node:fs'; import path from 'node:path'; import os from 'node:os'; diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/ProfileSuccessSummary.tsx b/packages/cli/src/ui/components/ProfileCreateWizard/ProfileSuccessSummary.tsx index bb5fafd689..4f2f9ff7c0 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/ProfileSuccessSummary.tsx +++ b/packages/cli/src/ui/components/ProfileCreateWizard/ProfileSuccessSummary.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; @@ -37,7 +38,7 @@ export const ProfileSuccessSummary: React.FC = ({ (value: string) => { if (value === 'load') { // Load the profile using the provided handler - if (onLoadProfile && state.profileName) { + if (onLoadProfile != null && state.profileName) { onLoadProfile(state.profileName); } onClose(); diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/ProviderSelectStep.tsx b/packages/cli/src/ui/components/ProfileCreateWizard/ProviderSelectStep.tsx index 315afce7c8..7e1d8f371f 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/ProviderSelectStep.tsx +++ b/packages/cli/src/ui/components/ProfileCreateWizard/ProviderSelectStep.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import { Box, Text } from 'ink'; import { DebugLogger } from '@vybestack/llxprt-code-core'; import { Colors } from '../../colors.js'; diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/TextInput.tsx b/packages/cli/src/ui/components/ProfileCreateWizard/TextInput.tsx index e77779879b..f8562e09b1 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/TextInput.tsx +++ b/packages/cli/src/ui/components/ProfileCreateWizard/TextInput.tsx @@ -4,9 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback, useEffect } from 'react'; +import type React from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { Box, Text } from 'ink'; -import { useKeypress, Key } from '../../hooks/useKeypress.js'; +import { useKeypress, type Key } from '../../hooks/useKeypress.js'; import { Colors } from '../../colors.js'; export interface TextInputProps { @@ -46,7 +47,7 @@ export const TextInput: React.FC = ({ if (!isFocused) return false; // Enter - submit - if (key.name === 'return' && onSubmit) { + if (key.name === 'return' && onSubmit != null) { onSubmit(); return true; } diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/index.tsx b/packages/cli/src/ui/components/ProfileCreateWizard/index.tsx index 5ffd16ec32..4abc0b19ff 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/index.tsx +++ b/packages/cli/src/ui/components/ProfileCreateWizard/index.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback } from 'react'; +import type React from 'react'; +import { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; diff --git a/packages/cli/src/ui/components/ProfileCreateWizard/utils.ts b/packages/cli/src/ui/components/ProfileCreateWizard/utils.ts index 07a761de8a..ba212547c9 100644 --- a/packages/cli/src/ui/components/ProfileCreateWizard/utils.ts +++ b/packages/cli/src/ui/components/ProfileCreateWizard/utils.ts @@ -35,8 +35,7 @@ function expandTilde(filePath: string): string { export function needsBaseUrlConfig(provider: string | null): boolean { if (!provider) return false; const providerOption = PROVIDER_OPTIONS.find((p) => p.value === provider); - const result = providerOption?.needsBaseUrl ?? false; - return result; + return providerOption?.needsBaseUrl ?? false; } export function generateProfileNameSuggestions( @@ -96,7 +95,7 @@ export function buildProfileJSON(state: WizardState): Record { } // Add parameters if configured - if (state.config.params) { + if (state.config.params != null) { if (state.config.params.temperature !== undefined) { (profile.modelParams as Record).temperature = state.config.params.temperature; @@ -190,7 +189,7 @@ export function formatConfigSummary(state: WizardState): string { lines.push(`Auth: ${authDisplay}`); // Parameters (if configured) - if (state.config.params) { + if (state.config.params != null) { if (state.config.params.temperature !== undefined) { lines.push(`Temperature: ${state.config.params.temperature}`); } diff --git a/packages/cli/src/ui/components/ProfileDetailDialog.tsx b/packages/cli/src/ui/components/ProfileDetailDialog.tsx index a40019ad6b..4a3f83eb0f 100644 --- a/packages/cli/src/ui/components/ProfileDetailDialog.tsx +++ b/packages/cli/src/ui/components/ProfileDetailDialog.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState } from 'react'; +import type React from 'react'; +import { useState } from 'react'; import { Box, Text } from 'ink'; import { SemanticColors } from '../colors.js'; import { useResponsive } from '../hooks/useResponsive.js'; @@ -94,7 +95,7 @@ export const ProfileDetailDialog: React.FC = ({ } // On error / missing profile screens, only Esc should do anything. - if (error || !profile) { + if (error || profile == null) { return; } @@ -158,7 +159,7 @@ export const ProfileDetailDialog: React.FC = ({ ); } - if (!profile) { + if (profile == null) { return ( = ({ } if (key.sequence === 'G') { setCursorLine(lines.length - 1); - return; } }, { isActive: true }, diff --git a/packages/cli/src/ui/components/SecureKeyInput.tsx b/packages/cli/src/ui/components/SecureKeyInput.tsx index b1ceb6a4bb..7df6ee5f4c 100644 --- a/packages/cli/src/ui/components/SecureKeyInput.tsx +++ b/packages/cli/src/ui/components/SecureKeyInput.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback, useEffect } from 'react'; +import type React from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { Box, Text } from 'ink'; import { DebugLogger } from '@vybestack/llxprt-code-core'; import { Colors } from '../colors.js'; diff --git a/packages/cli/src/ui/components/SessionBrowserDialog.tsx b/packages/cli/src/ui/components/SessionBrowserDialog.tsx index 3d81da91f6..ef89573a94 100644 --- a/packages/cli/src/ui/components/SessionBrowserDialog.tsx +++ b/packages/cli/src/ui/components/SessionBrowserDialog.tsx @@ -8,7 +8,7 @@ */ import { Box, Text } from 'ink'; -import React from 'react'; +import type React from 'react'; import type { SessionSummary } from '@vybestack/llxprt-code-core'; @@ -389,7 +389,7 @@ export function SessionBrowserDialog( // Render selection detail (wide mode only) const renderSelectionDetail = (): React.ReactNode => { - if (isNarrow || !state.selectedSession) return null; + if (isNarrow || state.selectedSession == null) return null; const session = state.selectedSession; const relTime = formatRelativeTime(session.lastModified, { mode: 'long' }); diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx index 3dac482d67..90f9b99dbd 100644 --- a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx +++ b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx @@ -8,7 +8,7 @@ import { render } from 'ink-testing-library'; import { describe, it, expect, vi } from 'vitest'; import { SessionSummaryDisplay } from './SessionSummaryDisplay.js'; import * as SessionContext from '../contexts/SessionContext.js'; -import { SessionMetrics } from '../contexts/SessionContext.js'; +import type { SessionMetrics } from '../contexts/SessionContext.js'; vi.mock('../contexts/SessionContext.js', async (importOriginal) => { const actual = await importOriginal(); @@ -24,9 +24,9 @@ const renderWithMockedStats = (metrics: SessionMetrics) => { useSessionStatsMock.mockReturnValue({ stats: { sessionStartTime: new Date(), - metrics, lastPromptTokenCount: 0, promptCount: 5, + metrics, }, getPromptCount: () => 5, diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.tsx index 604c8d715e..35f9e5abf1 100644 --- a/packages/cli/src/ui/components/SessionSummaryDisplay.tsx +++ b/packages/cli/src/ui/components/SessionSummaryDisplay.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { StatsDisplay } from './StatsDisplay.js'; interface SessionSummaryDisplayProps { diff --git a/packages/cli/src/ui/components/SettingsDialog.test.tsx b/packages/cli/src/ui/components/SettingsDialog.test.tsx index c1f57a7d0f..0a2dbecbd7 100644 --- a/packages/cli/src/ui/components/SettingsDialog.test.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.test.tsx @@ -1387,7 +1387,7 @@ describe('SettingsDialog', () => { const { lastFrame, stdin } = renderDialog(settings, onSelect); - if (stdinActions) { + if (stdinActions != null) { stdinActions(stdin); } diff --git a/packages/cli/src/ui/components/SettingsDialog.tsx b/packages/cli/src/ui/components/SettingsDialog.tsx index 9764dcfa64..defe11730a 100644 --- a/packages/cli/src/ui/components/SettingsDialog.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.tsx @@ -45,7 +45,6 @@ import { getCachedStringWidth, } from '../utils/textUtils.js'; import type { Config } from '@vybestack/llxprt-code-core'; -import { SettingDefinition as _SettingDefinition } from '../../config/settingsSchema.js'; import { generateDynamicToolSettings } from '../../utils/dynamicSettings.js'; import { keyMatchers, Command } from '../keyMatchers.js'; import { debugLogger } from '@vybestack/llxprt-code-core'; @@ -76,7 +75,7 @@ interface TextInputProps { * Simple text input component for search. */ function TextInput({ focus, value, placeholder }: TextInputProps) { - const showPlaceholder = !value && placeholder; + const showPlaceholder = value.length === 0 && placeholder !== undefined; if (showPlaceholder) { return {placeholder}; @@ -116,7 +115,7 @@ export function getToolCurrentState( ): ToolEnabledState { try { const excludeTools = - pendingExcludeTools ?? ((settings.merged.excludeTools as string[]) || []); + pendingExcludeTools ?? (settings.merged.excludeTools as string[]); // Tool is enabled if not in excludeTools return excludeTools.includes(toolName) ? 'disabled' : 'enabled'; } catch (error) { @@ -135,8 +134,7 @@ export function updateToolExclusion( scope: SettingScope, ): void { try { - const currentExcludeTools = - (settings.merged.excludeTools as string[]) || []; + const currentExcludeTools = settings.merged.excludeTools as string[]; let newExcludeTools = [...currentExcludeTools]; if (state === 'enabled') { @@ -213,7 +211,7 @@ export function SettingsDialog({ // Perform search useEffect(() => { let active = true; - if (!searchQuery.trim() || !fzfInstance) { + if (searchQuery.trim().length === 0) { setFilteredKeys(getDialogSettingKeys()); return; } @@ -330,7 +328,7 @@ export function SettingsDialog({ if ( subSettingsMode.isActive && subSettingsMode.parentKey === 'coreToolSettings' && - config + config != null ) { return generateDynamicToolSettings(config); } @@ -340,14 +338,13 @@ export function SettingsDialog({ const generateSettingsItems = () => { if (subSettingsMode.isActive) { return generateSubSettingsItems(subSettingsMode.parentKey); - } else { - return generateNormalSettingsItems(); } + return generateNormalSettingsItems(); }; const generateSubSettingsItems = (parentKey: string) => { const parentDefinition = getSettingDefinition(parentKey); - let subSettings = parentDefinition?.subSettings || {}; + let subSettings = parentDefinition?.subSettings ?? {}; // If this is the coreToolSettings, use the memoized dynamic settings if (parentKey === 'coreToolSettings') { @@ -368,8 +365,8 @@ export function SettingsDialog({ // For core tools, use excludeTools logic if (parentKey === 'coreToolSettings') { // Calculate new excludeTools list for pending state - const currentExcludeTools = - (settings.merged.excludeTools as string[]) || []; + const currentExcludeTools = settings.merged + .excludeTools as string[]; // We need to check if there's already a pending change for excludeTools let pendingExcludeTools = currentExcludeTools; if (globalPendingChanges.has('excludeTools')) { @@ -406,8 +403,8 @@ export function SettingsDialog({ setShowRestartPrompt(true); // Calculate new excludeTools list for pending state - const currentExcludeTools = - (settings.merged.excludeTools as string[]) || []; + const currentExcludeTools = settings.merged + .excludeTools as string[]; // We need to check if there's already a pending change for excludeTools let pendingExcludeTools = currentExcludeTools; if (globalPendingChanges.has('excludeTools')) { @@ -470,10 +467,7 @@ export function SettingsDialog({ if (!requiresRestart(fullKey)) { saveSingleSetting(fullKey, newValue, settings, selectedScope); } else { - setModifiedSettings((prev) => { - const updated = new Set(prev).add(fullKey); - return updated; - }); + setModifiedSettings((prev) => new Set(prev).add(fullKey)); } }, }; @@ -490,7 +484,7 @@ export function SettingsDialog({ return { description: definition?.description, - label: definition?.label || key, + label: definition?.label ?? key, value: key, type: definition?.type, toggle: () => { @@ -736,7 +730,7 @@ export function SettingsDialog({ let showScopeSelection = true; // If we have limited height, prioritize showing more settings over scope selection - if (availableTerminalHeight && availableTerminalHeight < 25) { + if (availableTerminalHeight !== undefined && availableTerminalHeight < 25) { // For very limited height, hide scope selection to show more settings const totalWithScope = totalFixedHeight + SCOPE_SELECTION_HEIGHT; const availableWithScope = Math.max( @@ -768,9 +762,10 @@ export function SettingsDialog({ } // Use the calculated maxVisibleItems or fall back to the original maxItemsToShow - const effectiveMaxItemsToShow = availableTerminalHeight - ? Math.min(maxVisibleItems, items.length) - : maxItemsToShow; + const effectiveMaxItemsToShow = + availableTerminalHeight !== undefined + ? Math.min(maxVisibleItems, items.length) + : maxItemsToShow; // Ensure focus stays on settings when scope selection is hidden React.useEffect(() => { @@ -784,9 +779,6 @@ export function SettingsDialog({ scrollOffset, scrollOffset + effectiveMaxItemsToShow, ); - // Always show arrows for consistent UI and to indicate circular navigation - const showScrollUp = true; - const showScrollDown = true; const saveRestartRequiredSettings = () => { const restartRequiredSettings = @@ -848,7 +840,7 @@ export function SettingsDialog({ return; } - const ch = stripUnsafeCharacters(key.sequence ?? ''); + const ch = stripUnsafeCharacters(key.sequence); if ( ch.length === 1 && !key.ctrl && @@ -864,8 +856,7 @@ export function SettingsDialog({ } if ( focusSection === 'settings' && - !isSearching && - !editingKey && + editingKey === null && key.sequence === '/' && !key.ctrl && !key.meta @@ -996,18 +987,19 @@ export function SettingsDialog({ } } else if (keyMatchers[Command.RETURN](key)) { const currentItem = items[activeSettingIndex]; - const currentDefinition = getSettingDefinition( - currentItem?.value || '', - ); + const currentDefinition = getSettingDefinition(currentItem.value); // Check if this item has sub-settings (special case for coreToolSettings) let hasSubSettings = false; if ( - currentDefinition?.subSettings && + currentDefinition?.subSettings != null && Object.keys(currentDefinition.subSettings).length > 0 ) { hasSubSettings = true; - } else if (currentItem?.value === 'coreToolSettings' && config) { + } else if ( + currentItem.value === 'coreToolSettings' && + config != null + ) { // Special case: coreToolSettings always has sub-settings // Avoid unnecessary computation by directly setting to true hasSubSettings = true; @@ -1023,8 +1015,8 @@ export function SettingsDialog({ // Switch to sub-settings mode setSubSettingsMode({ isActive: true, - parentKey: currentItem?.value || '', - parentLabel: currentDefinition?.label || currentItem?.value || '', + parentKey: currentItem.value, + parentLabel: currentDefinition?.label ?? currentItem.value, }); // Reset sub-settings page state @@ -1034,16 +1026,16 @@ export function SettingsDialog({ } // For boolean type, use toggle() function (simple on/off) - if (currentItem?.type === 'boolean') { - currentItem?.toggle(); + if (currentItem.type === 'boolean') { + currentItem.toggle(); } // For enum types, handle cycle through options else if ( currentDefinition?.type === 'enum' && - currentDefinition.options + currentDefinition.options != null ) { const options = currentDefinition.options; - const path = (currentItem?.value || '').split('.'); + const path = currentItem.value.split('.'); // Get current value from multiple places in order let currentValue = getNestedValue(pendingSettings, path); @@ -1051,9 +1043,9 @@ export function SettingsDialog({ // If there's a global pending change for this key, use that first if ( currentValue === undefined && - globalPendingChanges.has(currentItem?.value || '') + globalPendingChanges.has(currentItem.value) ) { - currentValue = globalPendingChanges.get(currentItem?.value || ''); + currentValue = globalPendingChanges.get(currentItem.value); } // If still undefined, try to get from merged settings @@ -1063,7 +1055,7 @@ export function SettingsDialog({ // If still undefined, use the default value if (currentValue === undefined) { - currentValue = getDefaultValue(currentItem?.value || ''); + currentValue = getDefaultValue(currentItem.value); } const currentIndex = options.findIndex( @@ -1075,18 +1067,14 @@ export function SettingsDialog({ // Update pending settings setPendingSettings((prev) => - setPendingSettingValueAny( - currentItem?.value || '', - newValue, - prev, - ), + setPendingSettingValueAny(currentItem.value, newValue, prev), ); // Handle the setting change based on whether it requires restart - if (!requiresRestart(currentItem?.value || '')) { + if (!requiresRestart(currentItem.value)) { // Save immediately for settings that don't require restart saveSingleSetting( - currentItem?.value || '', + currentItem.value, newValue, settings, selectedScope, @@ -1095,32 +1083,32 @@ export function SettingsDialog({ // Remove from modifiedSets since it's now saved setModifiedSettings((prev) => { const updated = new Set(prev); - updated.delete(currentItem?.value || ''); + updated.delete(currentItem.value); return updated; }); setRestartRequiredSettings((prev) => { const updated = new Set(prev); - updated.delete(currentItem?.value || ''); + updated.delete(currentItem.value); return updated; }); // Remove from global pending changes if present setGlobalPendingChanges((prev) => { - if (!prev.has(currentItem?.value || '')) return prev; + if (!prev.has(currentItem.value)) return prev; const next = new Map(prev); - next.delete(currentItem?.value || ''); + next.delete(currentItem.value); return next; }); } else { // Mark as modified and needing restart setModifiedSettings((prev) => { - const updated = new Set(prev).add(currentItem?.value || ''); + const updated = new Set(prev).add(currentItem.value); const needsRestart = hasRestartRequiredSettings(updated); if (needsRestart) { setShowRestartPrompt(true); setRestartRequiredSettings((prevRestart) => - new Set(prevRestart).add(currentItem?.value || ''), + new Set(prevRestart).add(currentItem.value), ); } return updated; @@ -1129,23 +1117,23 @@ export function SettingsDialog({ // Store in globalPendingChanges setGlobalPendingChanges((prev) => { const next = new Map(prev); - next.set(currentItem?.value || '', newValue as PendingValue); + next.set(currentItem.value, newValue as PendingValue); return next; }); } } // For numbers and strings, use edit mode else if ( - currentItem?.type === 'number' || - currentItem?.type === 'string' + currentItem.type === 'number' || + currentItem.type === 'string' ) { startEditing(currentItem.value); } else { - currentItem?.toggle(); + currentItem.toggle(); } - } else if (/^[0-9]$/.test(key.sequence || '') && !editingKey) { + } else if (/^[0-9]$/.test(key.sequence) && editingKey === null) { const currentItem = items[activeSettingIndex]; - if (currentItem?.type === 'number') { + if (currentItem.type === 'number') { startEditing(currentItem.value, key.sequence); } } else if ( @@ -1154,7 +1142,7 @@ export function SettingsDialog({ ) { // Ctrl+C or Ctrl+L: Clear current setting and reset to default const currentSetting = items[activeSettingIndex]; - if (currentSetting) { + { const defaultValue = getDefaultValue(currentSetting.value); const defType = currentSetting.type; if (defType === 'boolean') { @@ -1266,7 +1254,7 @@ export function SettingsDialog({ setShowRestartPrompt(false); setRestartRequiredSettings(new Set()); // Clear restart-required settings - if (onRestartRequest) onRestartRequest(); + if (onRestartRequest != null) onRestartRequest(); } if (keyMatchers[Command.ESCAPE](key)) { if (editingKey) { @@ -1297,7 +1285,7 @@ export function SettingsDialog({ let max = 0; for (const key of allKeys) { const def = getSettingDefinition(key); - if (!def) continue; + if (def == null) continue; const scopeMessage = getScopeMessageForSetting( key, @@ -1364,11 +1352,9 @@ export function SettingsDialog({ ) : ( <> - {showScrollUp && ( - - - - )} + + + {visibleItems.map((item, idx) => { const isActive = focusSection === 'settings' && @@ -1412,17 +1398,12 @@ export function SettingsDialog({ displayValue = String(currentValue); } else { displayValue = - defaultValue !== undefined && defaultValue !== null - ? String(defaultValue) - : ''; + defaultValue !== undefined ? String(defaultValue) : ''; } // Add * if value differs from default OR if currently being modified const isModified = modifiedSettings.has(item.value); - const effectiveCurrentValue = - currentValue !== undefined && currentValue !== null - ? currentValue - : defaultValue; + const effectiveCurrentValue = currentValue ?? defaultValue; const isDifferentFromDefault = effectiveCurrentValue !== defaultValue; @@ -1468,12 +1449,10 @@ export function SettingsDialog({ const toolName = item.value.replace('coreToolSettings.', ''); // Check pending settings first for excludeTools - let excludeTools = - (pendingSettings.excludeTools as string[]) || []; + let excludeTools = pendingSettings.excludeTools as string[]; // If not in pending, fall back to merged settings (handled by getToolCurrentState but we want pending awareness) - if (!pendingSettings.excludeTools) { - excludeTools = - (settings.merged.excludeTools as string[]) || []; + if (pendingSettings.excludeTools == null) { + excludeTools = settings.merged.excludeTools as string[]; } const currentState = getToolCurrentState( @@ -1569,11 +1548,9 @@ export function SettingsDialog({ ); })} - {showScrollDown && ( - - - - )} + + + )} diff --git a/packages/cli/src/ui/components/ShellModeIndicator.tsx b/packages/cli/src/ui/components/ShellModeIndicator.tsx index 512eebe362..e9c0780d54 100644 --- a/packages/cli/src/ui/components/ShellModeIndicator.tsx +++ b/packages/cli/src/ui/components/ShellModeIndicator.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx index 5d8717c88a..c073ef65af 100644 --- a/packages/cli/src/ui/components/StatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx @@ -8,7 +8,7 @@ import { render } from 'ink-testing-library'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { StatsDisplay } from './StatsDisplay.js'; import * as SessionContext from '../contexts/SessionContext.js'; -import { SessionMetrics } from '../contexts/SessionContext.js'; +import type { SessionMetrics } from '../contexts/SessionContext.js'; import * as RuntimeContext from '../contexts/RuntimeContext.js'; // Mock the SessionContext to provide controlled data for testing diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx index 6a4f211aec..b5707da4e0 100644 --- a/packages/cli/src/ui/components/StatsDisplay.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.tsx @@ -74,7 +74,7 @@ const buildModelRows = (models: Record) => { const getBaseModelName = (name: string) => name.replace('-001', ''); // Models with active usage - const activeRows = Object.entries(models).map( + return Object.entries(models).map( ([name, metrics]: [string, ModelMetrics]) => { const modelName = getBaseModelName(name); const cachedTokens = metrics.tokens.cached; @@ -83,16 +83,14 @@ const buildModelRows = (models: Record) => { const inputTokens = metrics.tokens.input ?? promptTokens - cachedTokens; return { key: name, - modelName, requests: metrics.api.totalRequests, cachedTokens: cachedTokens.toLocaleString(), inputTokens: inputTokens.toLocaleString(), outputTokens: metrics.tokens.candidates.toLocaleString(), + modelName, }; }, ); - - return activeRows; }; const ModelUsageTable: React.FC<{ diff --git a/packages/cli/src/ui/components/StatusDisplay.tsx b/packages/cli/src/ui/components/StatusDisplay.tsx index 130ec86836..1c46d1ef02 100644 --- a/packages/cli/src/ui/components/StatusDisplay.tsx +++ b/packages/cli/src/ui/components/StatusDisplay.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { HookStatusDisplay } from './HookStatusDisplay.js'; import { ContextSummaryDisplay } from './ContextSummaryDisplay.js'; import type { ActiveHook } from '../types.js'; diff --git a/packages/cli/src/ui/components/SubagentManagement/ProfileAttachmentWizard.tsx b/packages/cli/src/ui/components/SubagentManagement/ProfileAttachmentWizard.tsx index 7788aa5b74..ef2ad0d8a6 100644 --- a/packages/cli/src/ui/components/SubagentManagement/ProfileAttachmentWizard.tsx +++ b/packages/cli/src/ui/components/SubagentManagement/ProfileAttachmentWizard.tsx @@ -52,7 +52,7 @@ export const ProfileAttachmentWizard: React.FC< // Load profile info when selection changes React.useEffect(() => { let cancelled = false; - if (getProfileInfo && selectedProfile) { + if (getProfileInfo != null && selectedProfile) { getProfileInfo(selectedProfile) .then((info) => { if (!cancelled) setPreviewInfo(info); @@ -60,9 +60,7 @@ export const ProfileAttachmentWizard: React.FC< .catch(() => { if (!cancelled) setPreviewInfo(null); }); - } else { - if (!cancelled) setPreviewInfo(null); - } + } else if (!cancelled) setPreviewInfo(null); return () => { cancelled = true; }; @@ -114,7 +112,6 @@ export const ProfileAttachmentWizard: React.FC< if (key.name === 'return') { // eslint-disable-next-line @typescript-eslint/no-floating-promises handleConfirm(); - return; } }, { isActive: isFocused && !isConfirming }, @@ -190,7 +187,7 @@ export const ProfileAttachmentWizard: React.FC< ────────────────────────────────────────────────────── - {previewInfo ? ( + {previewInfo != null ? ( <> {previewInfo.provider && ( diff --git a/packages/cli/src/ui/components/SubagentManagement/SubagentCreationWizard.tsx b/packages/cli/src/ui/components/SubagentManagement/SubagentCreationWizard.tsx index da2e4d8f6e..06b527d73f 100644 --- a/packages/cli/src/ui/components/SubagentManagement/SubagentCreationWizard.tsx +++ b/packages/cli/src/ui/components/SubagentManagement/SubagentCreationWizard.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback, useMemo } from 'react'; +import type React from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; @@ -224,7 +225,6 @@ export const SubagentCreationWizard: React.FC = ({ if (input === 's') { // eslint-disable-next-line @typescript-eslint/no-floating-promises handleSave(); - return; } }, { isActive: isFocused && !isSaving }, diff --git a/packages/cli/src/ui/components/SubagentManagement/SubagentDeleteDialog.tsx b/packages/cli/src/ui/components/SubagentManagement/SubagentDeleteDialog.tsx index 0119bb85ff..3e74288a40 100644 --- a/packages/cli/src/ui/components/SubagentManagement/SubagentDeleteDialog.tsx +++ b/packages/cli/src/ui/components/SubagentManagement/SubagentDeleteDialog.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback } from 'react'; +import type React from 'react'; +import { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; @@ -49,7 +50,6 @@ export const SubagentDeleteDialog: React.FC = ({ if (key.name === 'return') { // eslint-disable-next-line @typescript-eslint/no-floating-promises handleConfirm(); - return; } }, { isActive: isFocused && !isDeleting }, diff --git a/packages/cli/src/ui/components/SubagentManagement/SubagentEditForm.tsx b/packages/cli/src/ui/components/SubagentManagement/SubagentEditForm.tsx index 54ec404d59..18ab8611d5 100644 --- a/packages/cli/src/ui/components/SubagentManagement/SubagentEditForm.tsx +++ b/packages/cli/src/ui/components/SubagentManagement/SubagentEditForm.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback, useMemo } from 'react'; +import type React from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { Box, Text } from 'ink'; import { useTextBuffer } from '../shared/text-buffer.js'; import { Colors } from '../../colors.js'; diff --git a/packages/cli/src/ui/components/SubagentManagement/SubagentListMenu.tsx b/packages/cli/src/ui/components/SubagentManagement/SubagentListMenu.tsx index d1e4437cb5..81437c0c7b 100644 --- a/packages/cli/src/ui/components/SubagentManagement/SubagentListMenu.tsx +++ b/packages/cli/src/ui/components/SubagentManagement/SubagentListMenu.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useMemo, useCallback } from 'react'; +import type React from 'react'; +import { useState, useMemo, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; diff --git a/packages/cli/src/ui/components/SubagentManagement/SubagentMainMenu.tsx b/packages/cli/src/ui/components/SubagentManagement/SubagentMainMenu.tsx index 6722e38fea..e645655690 100644 --- a/packages/cli/src/ui/components/SubagentManagement/SubagentMainMenu.tsx +++ b/packages/cli/src/ui/components/SubagentManagement/SubagentMainMenu.tsx @@ -4,12 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; -import { SubagentView, MENU_ACTIONS } from './types.js'; +import { type SubagentView, MENU_ACTIONS } from './types.js'; interface SubagentMainMenuProps { onSelect: (view: SubagentView) => void; diff --git a/packages/cli/src/ui/components/SubagentManagement/SubagentManagerDialog.tsx b/packages/cli/src/ui/components/SubagentManagement/SubagentManagerDialog.tsx index 8e53adaf83..86afab8d5a 100644 --- a/packages/cli/src/ui/components/SubagentManagement/SubagentManagerDialog.tsx +++ b/packages/cli/src/ui/components/SubagentManagement/SubagentManagerDialog.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback, useEffect } from 'react'; +import type React from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { useUIState } from '../../contexts/UIStateContext.js'; @@ -29,8 +30,8 @@ export const SubagentManagerDialog: React.FC = ({ }) => { const uiState = useUIState(); const { commandContext } = uiState; - const subagentManager = commandContext?.services?.subagentManager; - const profileManager = commandContext?.services?.profileManager; + const subagentManager = commandContext.services.subagentManager; + const profileManager = commandContext.services.profileManager; const activeProfileName = uiState.activeProfileName; const [state, setState] = useState({ @@ -53,7 +54,7 @@ export const SubagentManagerDialog: React.FC = ({ // Load subagents and profiles const loadData = useCallback(async () => { - if (!subagentManager) { + if (subagentManager == null) { setState((prev) => ({ ...prev, isLoading: false, @@ -81,24 +82,24 @@ export const SubagentManagerDialog: React.FC = ({ setState((prev) => { let selectedSubagent = prev.selectedSubagent; - if (initialSubagentName && !selectedSubagent) { + if (initialSubagentName && selectedSubagent == null) { selectedSubagent = subagents.find((s) => s.name === initialSubagentName) ?? null; - } else if (selectedSubagent) { + } else if (selectedSubagent != null) { selectedSubagent = subagents.find((s) => s.name === selectedSubagent?.name) ?? null; } return { ...prev, - subagents, profiles: profileNames, - selectedSubagent, currentView: - selectedSubagent || prev.currentView !== initialView + selectedSubagent != null || prev.currentView !== initialView ? prev.currentView : initialView, isLoading: false, + subagents, + selectedSubagent, }; }); } catch (err) { @@ -160,7 +161,7 @@ export const SubagentManagerDialog: React.FC = ({ const handleEdit = useCallback( (subagent?: SubagentInfo) => { const target = subagent ?? state.selectedSubagent; - if (target) { + if (target != null) { navigateTo(SubagentView.EDIT, target); } }, @@ -177,7 +178,7 @@ export const SubagentManagerDialog: React.FC = ({ // Handle select profile from edit form (uses current selected subagent) const handleSelectProfileFromEdit = useCallback(() => { - if (state.selectedSubagent) { + if (state.selectedSubagent != null) { navigateTo(SubagentView.ATTACH_PROFILE, state.selectedSubagent); } }, [navigateTo, state.selectedSubagent]); @@ -199,7 +200,7 @@ export const SubagentManagerDialog: React.FC = ({ // Handle save (edit) const handleSave = useCallback( async (systemPrompt: string, profile: string) => { - if (!subagentManager || !state.selectedSubagent) return; + if (subagentManager == null || state.selectedSubagent == null) return; try { await subagentManager.saveSubagent( @@ -229,15 +230,15 @@ export const SubagentManagerDialog: React.FC = ({ profile: string, mode: 'auto' | 'manual' = 'auto', ) => { - if (!subagentManager) { + if (subagentManager == null) { throw new Error('SubagentManager not available'); } let finalPrompt = systemPrompt; if (mode === 'auto') { - const config = commandContext?.services?.config; - if (!config) { + const config = commandContext.services.config; + if (config == null) { throw new Error( 'Configuration service unavailable. Set up the CLI before using auto mode.', ); @@ -258,7 +259,7 @@ export const SubagentManagerDialog: React.FC = ({ // Handle profile attachment - check if we came from EDIT view const handleProfileAttach = useCallback( async (profileName: string) => { - if (!state.selectedSubagent) return; + if (state.selectedSubagent == null) return; // Check if previous view was EDIT - if so, just set pending profile and go back const prevView = @@ -274,7 +275,7 @@ export const SubagentManagerDialog: React.FC = ({ } // Direct profile attachment from list - save immediately - if (!subagentManager) return; + if (subagentManager == null) return; try { await subagentManager.saveSubagent( @@ -303,7 +304,7 @@ export const SubagentManagerDialog: React.FC = ({ // Handle delete confirmation const handleDeleteConfirm = useCallback(async () => { - if (!subagentManager || !state.selectedSubagent) return; + if (subagentManager == null || state.selectedSubagent == null) return; try { await subagentManager.deleteSubagent(state.selectedSubagent.name); @@ -351,7 +352,7 @@ export const SubagentManagerDialog: React.FC = ({ ); case SubagentView.SHOW: - if (!state.selectedSubagent) { + if (state.selectedSubagent == null) { return No subagent selected; } return ( @@ -364,7 +365,7 @@ export const SubagentManagerDialog: React.FC = ({ ); case SubagentView.EDIT: - if (!state.selectedSubagent) { + if (state.selectedSubagent == null) { return No subagent selected; } return ( @@ -391,7 +392,7 @@ export const SubagentManagerDialog: React.FC = ({ ); case SubagentView.ATTACH_PROFILE: - if (!state.selectedSubagent) { + if (state.selectedSubagent == null) { return No subagent selected; } return ( @@ -405,7 +406,7 @@ export const SubagentManagerDialog: React.FC = ({ ); case SubagentView.DELETE: - if (!state.selectedSubagent) { + if (state.selectedSubagent == null) { return No subagent selected; } return ( diff --git a/packages/cli/src/ui/components/SubagentManagement/SubagentShowView.tsx b/packages/cli/src/ui/components/SubagentManagement/SubagentShowView.tsx index b9cf6cf522..45197a93b7 100644 --- a/packages/cli/src/ui/components/SubagentManagement/SubagentShowView.tsx +++ b/packages/cli/src/ui/components/SubagentManagement/SubagentShowView.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useEffect, useMemo } from 'react'; +import type React from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { Box, Text } from 'ink'; import { type Direction, diff --git a/packages/cli/src/ui/components/Table.tsx b/packages/cli/src/ui/components/Table.tsx index 6993c513d1..27aead1d45 100644 --- a/packages/cli/src/ui/components/Table.tsx +++ b/packages/cli/src/ui/components/Table.tsx @@ -71,7 +71,7 @@ export function Table({ data, columns }: TableProps) { flexBasis={col.flexBasis ?? (col.width ? undefined : 0)} paddingRight={1} > - {col.renderCell ? ( + {col.renderCell != null ? ( col.renderCell(item) ) : ( diff --git a/packages/cli/src/ui/components/ThemeDialog.test.tsx b/packages/cli/src/ui/components/ThemeDialog.test.tsx index 790ae12027..a9d5297e36 100644 --- a/packages/cli/src/ui/components/ThemeDialog.test.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.test.tsx @@ -7,7 +7,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render } from 'ink-testing-library'; import { ThemeDialog } from './ThemeDialog.js'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; +import { type LoadedSettings, SettingScope } from '../../config/settings.js'; import { UIStateProvider } from '../contexts/UIStateContext.js'; import type { UIState } from '../contexts/UIStateContext.js'; import { KeypressProvider } from '../contexts/KeypressContext.js'; @@ -144,7 +144,7 @@ describe('ThemeDialog', () => { ): LoadedSettings => { const mockSettingsFile = { settings: { - ui: customThemes ? { customThemes } : {}, + ui: customThemes != null ? { customThemes } : {}, }, path: '/mock/user/settings.json', exists: true, @@ -172,7 +172,7 @@ describe('ThemeDialog', () => { merged: { ui: { theme: 'Green Screen', - ...(customThemes ? { customThemes } : {}), + ...(customThemes != null ? { customThemes } : {}), }, }, user: mockSettingsFile, @@ -393,7 +393,7 @@ describe('ThemeDialog', () => { }); it('should work without terminalBackgroundColor (no labels shown)', () => { - const { lastFrame } = renderThemeDialog(undefined); + const { lastFrame } = renderThemeDialog(); const output = lastFrame(); // All themes should be shown diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx index c9597dbbca..547c6a97fd 100644 --- a/packages/cli/src/ui/components/ThemeDialog.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback, useState, useMemo } from 'react'; +import type React from 'react'; +import { useCallback, useState, useMemo } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { themeManager, DEFAULT_THEME } from '../themes/theme-manager.js'; @@ -15,7 +16,7 @@ import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; import type { RenderItemContext } from './shared/BaseSelectionList.js'; import { DiffRenderer } from './messages/DiffRenderer.js'; import { colorizeCode } from '../utils/CodeColorizer.js'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; +import { type LoadedSettings, SettingScope } from '../../config/settings.js'; import { getScopeItems, getScopeMessageForSetting, @@ -74,8 +75,8 @@ export function ThemeDialog({ // Generate theme items filtered by selected scope const customThemes = selectedScope === SettingScope.User - ? settings.user.settings.ui?.customThemes || {} - : settings.merged.ui.customThemes || {}; + ? (settings.user.settings.ui?.customThemes ?? {}) + : (settings.merged.ui.customThemes ?? {}); const builtInThemes = themeManager .getAvailableThemes() .filter((theme) => theme.type !== 'custom'); @@ -100,8 +101,8 @@ export function ThemeDialog({ themeNameDisplay: theme.name, themeTypeDisplay: capitalize(theme.type), key: theme.name, - isCompatible, themeType: theme.type, + isCompatible, }; }), ...customThemeNames.map((name) => ({ @@ -260,9 +261,7 @@ export function ThemeDialog({ const diffHeight = Math.floor(availableHeightForPanes * 0.4); const previewTheme = useMemo( - () => - themeManager.getTheme(highlightedThemeName || DEFAULT_THEME.name) || - DEFAULT_THEME, + () => themeManager.getTheme(highlightedThemeName) ?? DEFAULT_THEME, [highlightedThemeName], ); diff --git a/packages/cli/src/ui/components/ThemedGradient.tsx b/packages/cli/src/ui/components/ThemedGradient.tsx index c53793ebff..3ba2159696 100644 --- a/packages/cli/src/ui/components/ThemedGradient.tsx +++ b/packages/cli/src/ui/components/ThemedGradient.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Text } from 'ink'; import Gradient from 'ink-gradient'; import { theme } from '../semantic-colors.js'; @@ -29,7 +29,7 @@ export const ThemedGradient: React.FC = ({ }) => { const gradient = colors ?? theme.ui.gradient; - if (gradient && gradient.length >= 2) { + if (gradient != null && gradient.length >= 2) { return ( {children} @@ -37,7 +37,7 @@ export const ThemedGradient: React.FC = ({ ); } - if (gradient && gradient.length === 1) { + if (gradient != null && gradient.length === 1) { return {children}; } diff --git a/packages/cli/src/ui/components/Tips.tsx b/packages/cli/src/ui/components/Tips.tsx index 61c83eb260..5ddb579bec 100644 --- a/packages/cli/src/ui/components/Tips.tsx +++ b/packages/cli/src/ui/components/Tips.tsx @@ -4,10 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; -import { type Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; interface TipsProps { config: Config; diff --git a/packages/cli/src/ui/components/TodoPanel.responsive.test.tsx b/packages/cli/src/ui/components/TodoPanel.responsive.test.tsx index d66c562748..1de2eab5e9 100644 --- a/packages/cli/src/ui/components/TodoPanel.responsive.test.tsx +++ b/packages/cli/src/ui/components/TodoPanel.responsive.test.tsx @@ -17,7 +17,7 @@ import { TodoPanel } from './TodoPanel.js'; import { TodoContext } from '../contexts/TodoContext.js'; import { ToolCallContext } from '../contexts/ToolCallContext.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; -import { Todo } from '@vybestack/llxprt-code-core'; +import type { Todo } from '@vybestack/llxprt-code-core'; vi.mock('../hooks/useTerminalSize.js'); @@ -138,9 +138,9 @@ describe('TodoPanel Responsive Behavior', () => { const hasFullContent = output!.includes( 'This is a very long todo item that should be truncated at different widths', ); - const hasTruncatedContent = output!.match( + const hasTruncatedContent = RegExp( /This is a very long todo.*\.\.\./, - ); + ).exec(output!); // Either full content is shown or it's truncated but with much more content visible expect(hasFullContent || hasTruncatedContent).toBe(true); @@ -282,7 +282,9 @@ describe('TodoPanel Responsive Behavior', () => { // This means more of the content should be visible before truncation // Count visible characters before truncation - const contentMatch = output!.match(/○\s+([^.]+(?:\.\.\.)?)(?:\s|$)/); + const contentMatch = RegExp(/○\s+([^.]+(?:\.\.\.)?)(?:\s|$)/).exec( + output!, + ); expect(contentMatch).toBeDefined(); const visibleContent = contentMatch![1]; @@ -328,7 +330,9 @@ describe('TodoPanel Responsive Behavior', () => { ); const output = lastFrame(); - const contentMatch = output!.match(/○\s+([^.]+(?:\.\.\.)?)(?:\s|$)/); + const contentMatch = RegExp(/○\s+([^.]+(?:\.\.\.)?)(?:\s|$)/).exec( + output!, + ); expect(contentMatch).toBeDefined(); const visibleContent = contentMatch![1]; diff --git a/packages/cli/src/ui/components/TodoPanel.semantic.test.tsx b/packages/cli/src/ui/components/TodoPanel.semantic.test.tsx index 230dcb351b..c7ae7e415f 100644 --- a/packages/cli/src/ui/components/TodoPanel.semantic.test.tsx +++ b/packages/cli/src/ui/components/TodoPanel.semantic.test.tsx @@ -18,7 +18,7 @@ import { TodoPanel } from './TodoPanel.js'; import { TodoContext } from '../contexts/TodoContext.js'; import { ToolCallContext } from '../contexts/ToolCallContext.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; -import { Todo } from '@vybestack/llxprt-code-core'; +import type { Todo } from '@vybestack/llxprt-code-core'; import { themeManager } from '../themes/theme-manager.js'; import { DefaultDark } from '../themes/default.js'; import { DefaultLight } from '../themes/default-light.js'; diff --git a/packages/cli/src/ui/components/TodoPanel.tsx b/packages/cli/src/ui/components/TodoPanel.tsx index 1b8912e956..49789623df 100644 --- a/packages/cli/src/ui/components/TodoPanel.tsx +++ b/packages/cli/src/ui/components/TodoPanel.tsx @@ -4,12 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useEffect, useMemo, useState } from 'react'; +import type React from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Box, Text } from 'ink'; import { useTodoContext } from '../contexts/TodoContext.js'; import { useToolCallContext } from '../contexts/ToolCallContext.js'; import { SemanticColors } from '../colors.js'; -import { Todo as CoreTodo, Subtask } from '@vybestack/llxprt-code-core'; +import type { Todo as CoreTodo, Subtask } from '@vybestack/llxprt-code-core'; import { truncateEnd } from '../utils/responsive.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; @@ -88,12 +89,13 @@ const TodoPanelComponent: React.FC = ({ }, [todos]); // Subscribe to tool call updates to re-render when they change - useEffect(() => { - const unsubscribe = subscribe(() => { - setContentKey((prev) => prev + 1); - }); - return unsubscribe; - }, [subscribe]); + useEffect( + () => + subscribe(() => { + setContentKey((prev) => prev + 1); + }), + [subscribe], + ); const maxVisibleItems = useMemo(() => calculateMaxVisibleItems(rows), [rows]); const currentTodoIndex = todos.findIndex( diff --git a/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx b/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx index 75b8b22a8c..b74cf889fa 100644 --- a/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx @@ -8,7 +8,7 @@ import { render } from 'ink-testing-library'; import { describe, it, expect, vi } from 'vitest'; import { ToolStatsDisplay } from './ToolStatsDisplay.js'; import * as SessionContext from '../contexts/SessionContext.js'; -import { SessionMetrics } from '../contexts/SessionContext.js'; +import type { SessionMetrics } from '../contexts/SessionContext.js'; // Mock the context to provide controlled data for testing vi.mock('../contexts/SessionContext.js', async (importOriginal) => { @@ -25,9 +25,9 @@ const renderWithMockedStats = (metrics: SessionMetrics) => { useSessionStatsMock.mockReturnValue({ stats: { sessionStartTime: new Date(), - metrics, lastPromptTokenCount: 0, promptCount: 5, + metrics, }, getPromptCount: () => 5, diff --git a/packages/cli/src/ui/components/ToolStatsDisplay.tsx b/packages/cli/src/ui/components/ToolStatsDisplay.tsx index ae19283929..0749ce6cae 100644 --- a/packages/cli/src/ui/components/ToolStatsDisplay.tsx +++ b/packages/cli/src/ui/components/ToolStatsDisplay.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { formatDuration } from '../utils/formatters.js'; @@ -16,7 +16,7 @@ import { USER_AGREEMENT_RATE_MEDIUM, } from '../utils/displayUtils.js'; import { useSessionStats } from '../contexts/SessionContext.js'; -import { ToolCallStats } from '@vybestack/llxprt-code-core'; +import type { ToolCallStats } from '@vybestack/llxprt-code-core'; const TOOL_NAME_COL_WIDTH = 25; const CALLS_COL_WIDTH = 8; diff --git a/packages/cli/src/ui/components/ToolsDialog.tsx b/packages/cli/src/ui/components/ToolsDialog.tsx index a6f7e11b60..900eef61e9 100644 --- a/packages/cli/src/ui/components/ToolsDialog.tsx +++ b/packages/cli/src/ui/components/ToolsDialog.tsx @@ -4,10 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback } from 'react'; +import type React from 'react'; +import { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; -import { AnyDeclarativeTool } from '@vybestack/llxprt-code-core'; +import type { AnyDeclarativeTool } from '@vybestack/llxprt-code-core'; import { Colors } from '../colors.js'; import { useKeypress } from '../hooks/useKeypress.js'; @@ -32,9 +33,8 @@ export const ToolsDialog: React.FC = ({ const availableTools = tools.filter((tool) => { if (action === 'disable') { return !disabledTools.includes(tool.name); - } else { - return disabledTools.includes(tool.name); } + return disabledTools.includes(tool.name); }); // Create items for RadioButtonSelect diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/AuthMethodStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/AuthMethodStep.tsx index d0948ed5f9..1071d9abcc 100644 --- a/packages/cli/src/ui/components/WelcomeOnboarding/AuthMethodStep.tsx +++ b/packages/cli/src/ui/components/WelcomeOnboarding/AuthMethodStep.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useCallback } from 'react'; +import type React from 'react'; +import { useMemo, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/CompletionStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/CompletionStep.tsx index 1a4bef5027..087d428317 100644 --- a/packages/cli/src/ui/components/WelcomeOnboarding/CompletionStep.tsx +++ b/packages/cli/src/ui/components/WelcomeOnboarding/CompletionStep.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useCallback } from 'react'; +import type React from 'react'; +import { useState, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/ModelSelectStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/ModelSelectStep.tsx index 804812734a..227f4a4987 100644 --- a/packages/cli/src/ui/components/WelcomeOnboarding/ModelSelectStep.tsx +++ b/packages/cli/src/ui/components/WelcomeOnboarding/ModelSelectStep.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useCallback } from 'react'; +import type React from 'react'; +import { useMemo, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/ProviderSelectStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/ProviderSelectStep.tsx index 72f12d87d9..3e8b5c1b28 100644 --- a/packages/cli/src/ui/components/WelcomeOnboarding/ProviderSelectStep.tsx +++ b/packages/cli/src/ui/components/WelcomeOnboarding/ProviderSelectStep.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useCallback } from 'react'; +import type React from 'react'; +import { useMemo, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/SkipExitStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/SkipExitStep.tsx index e9eea32bd8..4909b43792 100644 --- a/packages/cli/src/ui/components/WelcomeOnboarding/SkipExitStep.tsx +++ b/packages/cli/src/ui/components/WelcomeOnboarding/SkipExitStep.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeDialog.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeDialog.tsx index 0b58eeadef..48314b8b96 100644 --- a/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeDialog.tsx +++ b/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeDialog.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useCallback } from 'react'; +import type React from 'react'; +import { useCallback } from 'react'; import { Box } from 'ink'; import { Colors } from '../../colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; diff --git a/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeStep.tsx b/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeStep.tsx index 67c02adde8..a30b6c41eb 100644 --- a/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeStep.tsx +++ b/packages/cli/src/ui/components/WelcomeOnboarding/WelcomeStep.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { diff --git a/packages/cli/src/ui/components/__tests__/SessionBrowserDialog.spec.tsx b/packages/cli/src/ui/components/__tests__/SessionBrowserDialog.spec.tsx index fa2ae7657f..65b148e25b 100644 --- a/packages/cli/src/ui/components/__tests__/SessionBrowserDialog.spec.tsx +++ b/packages/cli/src/ui/components/__tests__/SessionBrowserDialog.spec.tsx @@ -115,7 +115,7 @@ const renderWithProviders = ( props: Partial = {}, ) => { // Cleanup previous render if any - if (activeRender) { + if (activeRender != null) { activeRender.unmount(); activeRender = null; } @@ -165,7 +165,7 @@ describe('SessionBrowserDialog', () => { afterEach(() => { // Cleanup render instance - if (activeRender) { + if (activeRender != null) { activeRender.unmount(); activeRender = null; } diff --git a/packages/cli/src/ui/components/messages/CompressionMessage.tsx b/packages/cli/src/ui/components/messages/CompressionMessage.tsx index b814da8525..028f7da5fb 100644 --- a/packages/cli/src/ui/components/messages/CompressionMessage.tsx +++ b/packages/cli/src/ui/components/messages/CompressionMessage.tsx @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; -import { CompressionProps } from '../../types.js'; +import type { CompressionProps } from '../../types.js'; import Spinner from 'ink-spinner'; import { Colors } from '../../colors.js'; import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js'; diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.tsx index 58d6f06063..af921c1f62 100644 --- a/packages/cli/src/ui/components/messages/DiffRenderer.tsx +++ b/packages/cli/src/ui/components/messages/DiffRenderer.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text, useIsScreenReaderEnabled } from 'ink'; import { Colors, SemanticColors } from '../../colors.js'; import crypto from 'node:crypto'; @@ -28,7 +28,7 @@ function parseDiffWithLineNumbers(diffContent: string): DiffLine[] { for (const line of lines) { const hunkMatch = line.match(hunkHeaderRegex); - if (hunkMatch) { + if (hunkMatch != null) { currentOldLine = parseInt(hunkMatch[1], 10); currentNewLine = parseInt(hunkMatch[2], 10); inHunk = true; diff --git a/packages/cli/src/ui/components/messages/ErrorMessage.tsx b/packages/cli/src/ui/components/messages/ErrorMessage.tsx index 5dfde9c46d..eadd34d862 100644 --- a/packages/cli/src/ui/components/messages/ErrorMessage.tsx +++ b/packages/cli/src/ui/components/messages/ErrorMessage.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Text, Box } from 'ink'; import { Colors } from '../../colors.js'; diff --git a/packages/cli/src/ui/components/messages/GeminiMessage.test.tsx b/packages/cli/src/ui/components/messages/GeminiMessage.test.tsx index d34532825a..46bb339f53 100644 --- a/packages/cli/src/ui/components/messages/GeminiMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/GeminiMessage.test.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Text } from 'ink'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { GeminiMessage } from './GeminiMessage.js'; diff --git a/packages/cli/src/ui/components/messages/GeminiMessage.tsx b/packages/cli/src/ui/components/messages/GeminiMessage.tsx index c9d0f4ed72..8731825c21 100644 --- a/packages/cli/src/ui/components/messages/GeminiMessage.tsx +++ b/packages/cli/src/ui/components/messages/GeminiMessage.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Text, Box } from 'ink'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { Colors } from '../../colors.js'; diff --git a/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx b/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx index e5e4e5498f..1f2f0f2567 100644 --- a/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx +++ b/packages/cli/src/ui/components/messages/GeminiMessageContent.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box } from 'ink'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { useUIState } from '../../contexts/UIStateContext.js'; diff --git a/packages/cli/src/ui/components/messages/OAuthUrlMessage.test.tsx b/packages/cli/src/ui/components/messages/OAuthUrlMessage.test.tsx index c34cce554c..460c89c0f2 100644 --- a/packages/cli/src/ui/components/messages/OAuthUrlMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/OAuthUrlMessage.test.tsx @@ -10,8 +10,8 @@ import { createOsc8Link } from '../../utils/terminalLinks.js'; // Helper function to extract provider from text (mirrors component logic) function extractProvider(text: string): string { - const providerMatch = text.match(/authorize with ([^\n:]+)/i); - return providerMatch ? providerMatch[1] : 'the service'; + const providerMatch = RegExp(/authorize with ([^\n:]+)/i).exec(text); + return providerMatch != null ? providerMatch[1] : 'the service'; } describe('', () => { diff --git a/packages/cli/src/ui/components/messages/OAuthUrlMessage.tsx b/packages/cli/src/ui/components/messages/OAuthUrlMessage.tsx index 41682568c3..c58fd90164 100644 --- a/packages/cli/src/ui/components/messages/OAuthUrlMessage.tsx +++ b/packages/cli/src/ui/components/messages/OAuthUrlMessage.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Text, Box } from 'ink'; import { Colors, SemanticColors } from '../../colors.js'; import { createOsc8Link } from '../../utils/terminalLinks.js'; @@ -22,8 +22,8 @@ export const OAuthUrlMessage: React.FC = ({ const prefixWidth = prefixText.length; // Extract provider name from text if available - const providerMatch = text.match(/authorize with ([^\n:]+)/i); - const provider = providerMatch ? providerMatch[1] : 'the service'; + const providerMatch = RegExp(/authorize with ([^\n:]+)/i).exec(text); + const provider = providerMatch != null ? providerMatch[1] : 'the service'; const osc8Link = createOsc8Link( `Click here to authorize with ${provider}`, diff --git a/packages/cli/src/ui/components/messages/ThinkingBlockDisplay.tsx b/packages/cli/src/ui/components/messages/ThinkingBlockDisplay.tsx index 3e030e09bc..e7847d72d7 100644 --- a/packages/cli/src/ui/components/messages/ThinkingBlockDisplay.tsx +++ b/packages/cli/src/ui/components/messages/ThinkingBlockDisplay.tsx @@ -14,7 +14,7 @@ * @requirement:REQ-THINK-UI-003 - Toggle via visible prop */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import type { ThinkingBlock } from '@vybestack/llxprt-code-core'; import { Colors } from '../../colors.js'; diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.responsive.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.responsive.test.tsx index 226e733f97..31439363c0 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.responsive.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.responsive.test.tsx @@ -14,7 +14,7 @@ import { type MockedFunction, } from 'vitest'; import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; -import { +import type { ToolCallConfirmationDetails, Config, } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx index b37a7e541e..20327528d4 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx @@ -9,8 +9,8 @@ import { render } from 'ink-testing-library'; import React from 'react'; import { Text } from 'ink'; import { ToolGroupMessage } from './ToolGroupMessage.js'; -import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js'; -import { +import { type IndividualToolCallDisplay, ToolCallStatus } from '../../types.js'; +import type { Config, ToolCallConfirmationDetails, Todo, diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx index 789d458586..da1a46ee3a 100644 --- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx @@ -4,15 +4,16 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo } from 'react'; +import type React from 'react'; +import { useMemo } from 'react'; import { Box, Text } from 'ink'; -import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js'; +import { type IndividualToolCallDisplay, ToolCallStatus } from '../../types.js'; import { ToolMessage } from './ToolMessage.js'; import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; import { Colors } from '../../colors.js'; import { theme } from '../../semantic-colors.js'; import { - Config, + type Config, DEFAULT_AGENT_ID, formatTodoListForDisplay, } from '@vybestack/llxprt-code-core'; @@ -37,8 +38,8 @@ const extractCountFromText = (text?: string): number | undefined => { if (!text) { return undefined; } - const match = text.match(/(\d+)\s+(tasks?|items?)/i); - if (!match) { + const match = RegExp(/(\d+)\s+(tasks?|items?)/i).exec(text); + if (match == null) { return undefined; } return Number(match[1]); @@ -228,7 +229,11 @@ export const ToolGroupMessage: React.FC = ({ availableTerminalHeight={availableTerminalHeightPerToolMessage} terminalWidth={innerWidth} emphasis={ - isConfirming ? 'high' : toolAwaitingApproval ? 'low' : 'medium' + isConfirming + ? 'high' + : toolAwaitingApproval != null + ? 'low' + : 'medium' } renderOutputAsMarkdown={tool.renderOutputAsMarkdown} activeShellPtyId={activeShellPtyId} diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx index 99823e5f47..e3143938bf 100644 --- a/packages/cli/src/ui/components/messages/ToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx @@ -4,9 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useMemo, useState } from 'react'; +import type React from 'react'; +import { useMemo, useState } from 'react'; import { Box, Text } from 'ink'; -import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js'; +import { type IndividualToolCallDisplay, ToolCallStatus } from '../../types.js'; import { Colors } from '../../colors.js'; import { theme } from '../../semantic-colors.js'; import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js'; @@ -121,8 +122,8 @@ export const ToolMessage: React.FC = ({ .pop() ?? ''; const extractEchoText = (cmd: string): string | null => { - const m = cmd.match(/^\s*echo\s+(.*)$/i); - if (!m) return null; + const m = RegExp(/^\s*echo\s+(.*)$/i).exec(cmd); + if (m == null) return null; let text = m[1].trim(); if ( (text.startsWith('"') && text.endsWith('"')) || diff --git a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx index 524fdb5cfb..5ba368e7f3 100644 --- a/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx +++ b/packages/cli/src/ui/components/messages/ToolResultDisplay.tsx @@ -4,14 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { DiffRenderer } from './DiffRenderer.js'; import { Colors } from '../../colors.js'; import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js'; import { AnsiOutputText } from '../AnsiOutput.js'; import { MaxSizedBox } from '../shared/MaxSizedBox.js'; -import { type AnsiOutput } from '@vybestack/llxprt-code-core'; +import type { AnsiOutput } from '@vybestack/llxprt-code-core'; import { useUIState } from '../../contexts/UIStateContext.js'; import { STATUS_INDICATOR_WIDTH } from './ToolShared.js'; @@ -111,7 +111,7 @@ export const ToolResultDisplay: React.FC = ({ ).metadata?.astValidation as | { valid: boolean; errors: string[] } | undefined; - if (!astValidation) return null; + if (astValidation == null) return null; return ( diff --git a/packages/cli/src/ui/components/messages/ToolShared.tsx b/packages/cli/src/ui/components/messages/ToolShared.tsx index 85e4315d9b..d409acd86f 100644 --- a/packages/cli/src/ui/components/messages/ToolShared.tsx +++ b/packages/cli/src/ui/components/messages/ToolShared.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { ToolCallStatus } from '../../types.js'; import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js'; diff --git a/packages/cli/src/ui/components/messages/UserMessage.tsx b/packages/cli/src/ui/components/messages/UserMessage.tsx index 3c90eaaadb..5bd732b785 100644 --- a/packages/cli/src/ui/components/messages/UserMessage.tsx +++ b/packages/cli/src/ui/components/messages/UserMessage.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Text, Box } from 'ink'; import { Colors, SemanticColors } from '../../colors.js'; import { SCREEN_READER_USER_PREFIX } from '../../textConstants.js'; diff --git a/packages/cli/src/ui/components/messages/UserShellMessage.tsx b/packages/cli/src/ui/components/messages/UserShellMessage.tsx index d7aaa8b196..90805e4c3a 100644 --- a/packages/cli/src/ui/components/messages/UserShellMessage.tsx +++ b/packages/cli/src/ui/components/messages/UserShellMessage.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx index a33978ad4d..6c9c547997 100644 --- a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx +++ b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx @@ -326,17 +326,15 @@ function visitBoxRow(element: React.ReactNode): Row { if (parentProps === undefined || parentProps.wrap === 'wrap') { hasSeenWrapped = true; row.segments.push(segment); + } else if (!hasSeenWrapped) { + row.noWrapSegments.push(segment); } else { - if (!hasSeenWrapped) { - row.noWrapSegments.push(segment); - } else { - // put in the wrapped segment as the row is already stuck in wrapped mode. - row.segments.push(segment); - debugReportError( - 'Text elements without wrapping cannot appear after elements with wrapping in the same row.', - element, - ); - } + // put in the wrapped segment as the row is already stuck in wrapped mode. + row.segments.push(segment); + debugReportError( + 'Text elements without wrapping cannot appear after elements with wrapping in the same row.', + element, + ); } return; } @@ -526,15 +524,13 @@ function layoutInkElementAsStyledText( function addWrappingPartToLines() { if (lines.length === 0) { lines.push([...nonWrappingContent, ...wrappingPart]); + } else if (noWrappingWidth > 0) { + lines.push([ + ...[{ text: ' '.repeat(noWrappingWidth), props: {} }], + ...wrappingPart, + ]); } else { - if (noWrappingWidth > 0) { - lines.push([ - ...[{ text: ' '.repeat(noWrappingWidth), props: {} }], - ...wrappingPart, - ]); - } else { - lines.push(wrappingPart); - } + lines.push(wrappingPart); } wrappingPart = []; wrappingPartWidth = 0; diff --git a/packages/cli/src/ui/components/shared/ScrollableList.tsx b/packages/cli/src/ui/components/shared/ScrollableList.tsx index b46b068090..6ea63c0458 100644 --- a/packages/cli/src/ui/components/shared/ScrollableList.tsx +++ b/packages/cli/src/ui/components/shared/ScrollableList.tsx @@ -191,11 +191,11 @@ function ScrollableList( const scrollableEntry = useMemo( () => ({ ref: containerRef, - getScrollState, - scrollBy, scrollTo: (scrollTop: number) => virtualizedListRef.current?.scrollTo(scrollTop), hasFocus: hasFocusCallback, + getScrollState, + scrollBy, flashScrollbar, }), [getScrollState, hasFocusCallback, flashScrollbar, scrollBy], diff --git a/packages/cli/src/ui/components/shared/VirtualizedList.tsx b/packages/cli/src/ui/components/shared/VirtualizedList.tsx index cdfb4befa6..853a482ec7 100644 --- a/packages/cli/src/ui/components/shared/VirtualizedList.tsx +++ b/packages/cli/src/ui/components/shared/VirtualizedList.tsx @@ -109,14 +109,13 @@ function VirtualizedList( return { index: 0, offset: 0 }; }); - const [isStickingToBottom, setIsStickingToBottom] = useState(() => { - const scrollToEnd = + const [isStickingToBottom, setIsStickingToBottom] = useState( + () => initialScrollIndex === SCROLL_TO_ITEM_END || (typeof initialScrollIndex === 'number' && initialScrollIndex >= data.length - 1 && - initialScrollOffsetInIndex === SCROLL_TO_ITEM_END); - return scrollToEnd; - }); + initialScrollOffsetInIndex === SCROLL_TO_ITEM_END), + ); const containerRef = useRef(null); const [containerHeight, setContainerHeight] = useState(0); const itemRefs = useRef>([]); @@ -154,7 +153,7 @@ function VirtualizedList( // eslint-disable-next-line react-hooks/exhaustive-deps useLayoutEffect(() => { - if (containerRef.current) { + if (containerRef.current != null) { const height = Math.round(measureElement(containerRef.current).height); if (containerHeight !== height) { setContainerHeight(height); @@ -164,24 +163,25 @@ function VirtualizedList( let newHeights: number[] | null = null; for (let i = startIndex; i <= endIndex; i++) { const itemRef = itemRefs.current[i]; - if (itemRef) { + if (itemRef != null) { const height = Math.round(measureElement(itemRef).height); if (height !== heights[i]) { - if (!newHeights) { + if (newHeights == null) { newHeights = [...heights]; } newHeights[i] = height; } } } - if (newHeights) { + if (newHeights != null) { setHeights(newHeights); } }); - const scrollableContainerHeight = containerRef.current - ? Math.round(measureElement(containerRef.current).height) - : containerHeight; + const scrollableContainerHeight = + containerRef.current != null + ? Math.round(measureElement(containerRef.current).height) + : containerHeight; const getAnchorForScrollTop = useCallback( ( diff --git a/packages/cli/src/ui/components/shared/buffer-operations.test.ts b/packages/cli/src/ui/components/shared/buffer-operations.test.ts index 5763ef900c..c9d0cdd656 100644 --- a/packages/cli/src/ui/components/shared/buffer-operations.test.ts +++ b/packages/cli/src/ui/components/shared/buffer-operations.test.ts @@ -45,32 +45,32 @@ describe('buffer-operations', () => { it('should replace text in same line', () => { const state = createState(['hello world']); const result = replaceRangeInternal(state, 0, 0, 0, 5, 'hi'); - expect(result.lines).toEqual(['hi world']); + expect(result.lines).toStrictEqual(['hi world']); expect(result.cursorCol).toBe(2); }); it('should replace across multiple lines', () => { const state = createState(['hello', 'world', 'test']); const result = replaceRangeInternal(state, 0, 2, 2, 2, 'X'); - expect(result.lines).toEqual(['heXst']); + expect(result.lines).toStrictEqual(['heXst']); }); it('should insert newlines', () => { const state = createState(['hello world']); const result = replaceRangeInternal(state, 0, 5, 0, 6, '\n'); - expect(result.lines).toEqual(['hello', 'world']); + expect(result.lines).toStrictEqual(['hello', 'world']); }); it('should handle empty replacement', () => { const state = createState(['hello world']); const result = replaceRangeInternal(state, 0, 5, 0, 6, ''); - expect(result.lines).toEqual(['helloworld']); + expect(result.lines).toStrictEqual(['helloworld']); }); it('should handle replacement at line boundaries', () => { const state = createState(['hello']); const result = replaceRangeInternal(state, 0, 0, 0, 5, 'X'); - expect(result.lines).toEqual(['X']); + expect(result.lines).toStrictEqual(['X']); }); }); @@ -81,10 +81,10 @@ describe('buffer-operations', () => { state.cursorCol = 3; const result = pushUndo(state); expect(result.undoStack).toHaveLength(1); - expect(result.undoStack[0].lines).toEqual(['hello']); + expect(result.undoStack[0].lines).toStrictEqual(['hello']); expect(result.undoStack[0].cursorRow).toBe(0); expect(result.undoStack[0].cursorCol).toBe(3); - expect(result.redoStack).toEqual([]); + expect(result.redoStack).toStrictEqual([]); }); it('should limit undo stack size', () => { diff --git a/packages/cli/src/ui/components/shared/buffer-reducer.test.ts b/packages/cli/src/ui/components/shared/buffer-reducer.test.ts index 438403e178..9153bde720 100644 --- a/packages/cli/src/ui/components/shared/buffer-reducer.test.ts +++ b/packages/cli/src/ui/components/shared/buffer-reducer.test.ts @@ -44,7 +44,7 @@ describe('buffer-reducer', () => { const state = createState(); const action: TextBufferAction = { type: 'set_text', payload: 'hello' }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['hello']); + expect(result.lines).toStrictEqual(['hello']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(5); }); @@ -56,7 +56,7 @@ describe('buffer-reducer', () => { payload: 'hello\nworld', }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['hello', 'world']); + expect(result.lines).toStrictEqual(['hello', 'world']); expect(result.cursorRow).toBe(1); expect(result.cursorCol).toBe(5); }); @@ -78,7 +78,7 @@ describe('buffer-reducer', () => { const state = createState(); const action: TextBufferAction = { type: 'insert', payload: 'a' }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['a']); + expect(result.lines).toStrictEqual(['a']); expect(result.cursorCol).toBe(1); }); @@ -86,7 +86,7 @@ describe('buffer-reducer', () => { const state = createState(); const action: TextBufferAction = { type: 'insert', payload: 'hello' }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['hello']); + expect(result.lines).toStrictEqual(['hello']); expect(result.cursorCol).toBe(5); }); @@ -94,7 +94,7 @@ describe('buffer-reducer', () => { const state = createState({ lines: ['hello'] }); const action: TextBufferAction = { type: 'insert', payload: '\n' }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['', 'hello']); + expect(result.lines).toStrictEqual(['', 'hello']); expect(result.cursorRow).toBe(1); }); @@ -111,7 +111,7 @@ describe('buffer-reducer', () => { const state = createState({ lines: ['hello'], cursorCol: 5 }); const action: TextBufferAction = { type: 'backspace' }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['hell']); + expect(result.lines).toStrictEqual(['hell']); expect(result.cursorCol).toBe(4); }); @@ -123,7 +123,7 @@ describe('buffer-reducer', () => { }); const action: TextBufferAction = { type: 'backspace' }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['helloworld']); + expect(result.lines).toStrictEqual(['helloworld']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(5); }); @@ -132,7 +132,7 @@ describe('buffer-reducer', () => { const state = createState(); const action: TextBufferAction = { type: 'backspace' }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['']); + expect(result.lines).toStrictEqual(['']); }); }); @@ -141,7 +141,7 @@ describe('buffer-reducer', () => { const state = createState({ lines: ['hello'], cursorCol: 0 }); const action: TextBufferAction = { type: 'delete' }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['ello']); + expect(result.lines).toStrictEqual(['ello']); }); it('should join with next line at line end', () => { @@ -152,7 +152,7 @@ describe('buffer-reducer', () => { }); const action: TextBufferAction = { type: 'delete' }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['helloworld']); + expect(result.lines).toStrictEqual(['helloworld']); }); it('should do nothing at document end', () => { @@ -163,7 +163,7 @@ describe('buffer-reducer', () => { }); const action: TextBufferAction = { type: 'delete' }; const result = textBufferReducer(state, action); - expect(result.lines).toEqual(['hello']); + expect(result.lines).toStrictEqual(['hello']); }); }); @@ -187,18 +187,18 @@ describe('buffer-reducer', () => { it('should undo insert', () => { let state = createState(); state = textBufferReducer(state, { type: 'insert', payload: 'hello' }); - expect(state.lines).toEqual(['hello']); + expect(state.lines).toStrictEqual(['hello']); state = textBufferReducer(state, { type: 'undo' }); - expect(state.lines).toEqual(['']); + expect(state.lines).toStrictEqual(['']); }); it('should redo undone action', () => { let state = createState(); state = textBufferReducer(state, { type: 'insert', payload: 'hello' }); state = textBufferReducer(state, { type: 'undo' }); - expect(state.lines).toEqual(['']); + expect(state.lines).toStrictEqual(['']); state = textBufferReducer(state, { type: 'redo' }); - expect(state.lines).toEqual(['hello']); + expect(state.lines).toStrictEqual(['hello']); }); it('should clear redo stack on new action', () => { diff --git a/packages/cli/src/ui/components/shared/buffer-reducer.ts b/packages/cli/src/ui/components/shared/buffer-reducer.ts index 5cbd0812b1..5f004a899a 100644 --- a/packages/cli/src/ui/components/shared/buffer-reducer.ts +++ b/packages/cli/src/ui/components/shared/buffer-reducer.ts @@ -51,7 +51,7 @@ function findPrevWordBoundary(line: string, cursorCol: number): number { for (const seg of segmenter.segment(line)) { if (seg.index >= cursorIdx) break; - if (seg.isWordLike) { + if (seg.isWordLike === true) { targetIdx = seg.index; } } @@ -70,7 +70,7 @@ function findNextWordBoundary(line: string, cursorCol: number): number { const segEnd = seg.index + seg.segment.length; if (segEnd > cursorIdx) { - if (seg.isWordLike) { + if (seg.isWordLike === true) { targetIdx = segEnd; break; } @@ -120,10 +120,10 @@ function handleSetText( const lastIdx = lines.length - 1; return { ...nextState, - lines, cursorRow: lastIdx, cursorCol: cpLen(lines[lastIdx] ?? ''), preferredCol: null, + lines, }; } @@ -133,10 +133,10 @@ function handleInsertAction( options: TextBufferOptions, ): TextBufferState { let payload = rawPayload; - if (options.singleLine) { + if (options.singleLine === true) { payload = payload.replace(/[\r\n]/g, ''); } - if (options.inputFilter) { + if (options.inputFilter != null) { payload = options.inputFilter(payload); } if (payload.length === 0) return state; @@ -302,7 +302,7 @@ function handleVisualMove( visualLines, ); - if (visualToLogicalMap[pos.row]) { + if (visualToLogicalMap[pos.row] != null) { const [logRow, logStartCol] = visualToLogicalMap[pos.row]; return { ...state, @@ -409,7 +409,7 @@ function handleDeleteWordLeftAction(state: TextBufferState): TextBufferState { if (newCursorCol > 0) { const lineContent = lines[cursorRow] ?? ''; const prevWordStart = findPrevWordStartInLine(lineContent, newCursorCol); - const start = prevWordStart === null ? 0 : prevWordStart; + const start = prevWordStart ?? 0; newLines[newCursorRow] = cpSlice(lineContent, 0, start) + cpSlice(lineContent, newCursorCol); newCursorCol = start; @@ -448,7 +448,7 @@ function handleDeleteWordRightAction(state: TextBufferState): TextBufferState { newLines.splice(cursorRow + 1, 1); } else { const nextWordStart = findNextWordStartInLine(lineContent, cursorCol); - const end = nextWordStart === null ? lineLen : nextWordStart; + const end = nextWordStart ?? lineLen; newLines[cursorRow] = cpSlice(lineContent, 0, cursorCol) + cpSlice(lineContent, end); } @@ -495,7 +495,7 @@ function handleKillLineLeftAction(state: TextBufferState): TextBufferState { function handleUndoAction(state: TextBufferState): TextBufferState { const stateToRestore = state.undoStack[state.undoStack.length - 1]; - if (!stateToRestore) return state; + if (state.undoStack.length === 0) return state; const currentSnapshot = { lines: [...state.lines], @@ -512,7 +512,7 @@ function handleUndoAction(state: TextBufferState): TextBufferState { function handleRedoAction(state: TextBufferState): TextBufferState { const stateToRestore = state.redoStack[state.redoStack.length - 1]; - if (!stateToRestore) return state; + if (state.redoStack.length === 0) return state; const currentSnapshot = { lines: [...state.lines], diff --git a/packages/cli/src/ui/components/shared/buffer-types.test.ts b/packages/cli/src/ui/components/shared/buffer-types.test.ts index 10141ee522..7c08e3b9fd 100644 --- a/packages/cli/src/ui/components/shared/buffer-types.test.ts +++ b/packages/cli/src/ui/components/shared/buffer-types.test.ts @@ -5,7 +5,7 @@ */ import { describe, it, expect } from 'vitest'; -import { type Direction, type TextBufferAction } from './buffer-types.js'; +import type { Direction, TextBufferAction } from './buffer-types.js'; /** * Phase 2.1: Buffer Types Tests diff --git a/packages/cli/src/ui/components/shared/golden-snapshot.test.ts b/packages/cli/src/ui/components/shared/golden-snapshot.test.ts index 908b1234df..d805bc75da 100644 --- a/packages/cli/src/ui/components/shared/golden-snapshot.test.ts +++ b/packages/cli/src/ui/components/shared/golden-snapshot.test.ts @@ -224,18 +224,18 @@ describe('golden snapshot', () => { const sequence = actionCorpus.sequences[0]; const result1 = applySequence(sequence); const result2 = applySequence(sequence); - expect(result1).toEqual(result2); + expect(result1).toStrictEqual(result2); }); it('should handle insert and newlines correctly', () => { const state = applySequence(['insert:hello', 'insert:\n', 'insert:world']); - expect(state.lines).toEqual(['hello', 'world']); + expect(state.lines).toStrictEqual(['hello', 'world']); expect(state.cursorRow).toBe(1); expect(state.cursorCol).toBe(5); }); it('should handle undo/redo correctly', () => { const state = applySequence(['insert:hello', 'undo', 'redo']); - expect(state.lines).toEqual(['hello']); + expect(state.lines).toStrictEqual(['hello']); }); }); diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts index a732f100ac..de40150c1b 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.test.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts @@ -27,8 +27,6 @@ import { findWordEndInLine, findNextWordStartInLine, isWordCharStrict, -} from './text-buffer.js'; -import { getTransformedImagePath, calculateTransformationsForLine, getTransformUnderCursor, @@ -65,7 +63,7 @@ describe('textBufferReducer', () => { const action = { type: 'unknown_action' } as unknown as TextBufferAction; const state = textBufferReducer(initialState, action); expect(state).toHaveOnlyValidCharacters(); - expect(state).toEqual(initialState); + expect(state).toStrictEqual(initialState); }); describe('set_text action', () => { @@ -76,7 +74,7 @@ describe('textBufferReducer', () => { }; const state = textBufferReducer(initialState, action); expect(state).toHaveOnlyValidCharacters(); - expect(state.lines).toEqual(['hello', 'world']); + expect(state.lines).toStrictEqual(['hello', 'world']); expect(state.cursorRow).toBe(1); expect(state.cursorCol).toBe(5); expect(state.undoStack.length).toBe(1); @@ -90,7 +88,7 @@ describe('textBufferReducer', () => { }; const state = textBufferReducer(initialState, action); expect(state).toHaveOnlyValidCharacters(); - expect(state.lines).toEqual(['no undo']); + expect(state.lines).toStrictEqual(['no undo']); expect(state.undoStack.length).toBe(0); }); }); @@ -100,7 +98,7 @@ describe('textBufferReducer', () => { const action: TextBufferAction = { type: 'insert', payload: 'a' }; const state = textBufferReducer(initialState, action); expect(state).toHaveOnlyValidCharacters(); - expect(state.lines).toEqual(['a']); + expect(state.lines).toStrictEqual(['a']); expect(state.cursorCol).toBe(1); }); @@ -109,7 +107,7 @@ describe('textBufferReducer', () => { const action: TextBufferAction = { type: 'insert', payload: '\n' }; const state = textBufferReducer(stateWithText, action); expect(state).toHaveOnlyValidCharacters(); - expect(state.lines).toEqual(['', 'hello']); + expect(state.lines).toStrictEqual(['', 'hello']); expect(state.cursorRow).toBe(1); expect(state.cursorCol).toBe(0); }); @@ -122,7 +120,7 @@ describe('textBufferReducer', () => { inputFilter: (text) => text.replace(/[0-9]/g, ''), }; const state = textBufferReducer(initialState, action, options); - expect(state.lines).toEqual(['abc']); + expect(state.lines).toStrictEqual(['abc']); expect(state.cursorCol).toBe(3); }); @@ -133,7 +131,7 @@ describe('textBufferReducer', () => { }; const options: TextBufferOptions = { singleLine: true }; const state = textBufferReducer(initialState, action, options); - expect(state.lines).toEqual(['helloworld']); + expect(state.lines).toStrictEqual(['helloworld']); expect(state.cursorCol).toBe(10); }); @@ -147,7 +145,7 @@ describe('textBufferReducer', () => { inputFilter: (text) => text.replace(/[0-9]/g, ''), }; const state = textBufferReducer(initialState, action, options); - expect(state.lines).toEqual(['hello']); + expect(state.lines).toStrictEqual(['hello']); expect(state.cursorCol).toBe(5); }); }); @@ -163,7 +161,7 @@ describe('textBufferReducer', () => { const action: TextBufferAction = { type: 'backspace' }; const state = textBufferReducer(stateWithText, action); expect(state).toHaveOnlyValidCharacters(); - expect(state.lines).toEqual(['']); + expect(state.lines).toStrictEqual(['']); expect(state.cursorCol).toBe(0); }); @@ -177,7 +175,7 @@ describe('textBufferReducer', () => { const action: TextBufferAction = { type: 'backspace' }; const state = textBufferReducer(stateWithText, action); expect(state).toHaveOnlyValidCharacters(); - expect(state.lines).toEqual(['helloworld']); + expect(state.lines).toStrictEqual(['helloworld']); expect(state.cursorRow).toBe(0); expect(state.cursorCol).toBe(5); }); @@ -192,14 +190,14 @@ describe('textBufferReducer', () => { }; const stateAfterInsert = textBufferReducer(initialState, insertAction); expect(stateAfterInsert).toHaveOnlyValidCharacters(); - expect(stateAfterInsert.lines).toEqual(['test']); + expect(stateAfterInsert.lines).toStrictEqual(['test']); expect(stateAfterInsert.undoStack.length).toBe(1); // 2. Undo const undoAction: TextBufferAction = { type: 'undo' }; const stateAfterUndo = textBufferReducer(stateAfterInsert, undoAction); expect(stateAfterUndo).toHaveOnlyValidCharacters(); - expect(stateAfterUndo.lines).toEqual(['']); + expect(stateAfterUndo.lines).toStrictEqual(['']); expect(stateAfterUndo.undoStack.length).toBe(0); expect(stateAfterUndo.redoStack.length).toBe(1); @@ -207,7 +205,7 @@ describe('textBufferReducer', () => { const redoAction: TextBufferAction = { type: 'redo' }; const stateAfterRedo = textBufferReducer(stateAfterUndo, redoAction); expect(stateAfterRedo).toHaveOnlyValidCharacters(); - expect(stateAfterRedo.lines).toEqual(['test']); + expect(stateAfterRedo.lines).toStrictEqual(['test']); expect(stateAfterRedo.undoStack.length).toBe(1); expect(stateAfterRedo.redoStack.length).toBe(0); }); @@ -225,11 +223,11 @@ describe('textBufferReducer', () => { const state = textBufferReducer(stateWithText, action); expect(state).toHaveOnlyValidCharacters(); - expect(state.lines).toEqual(['hello']); + expect(state.lines).toStrictEqual(['hello']); expect(state.cursorRow).toBe(0); expect(state.cursorCol).toBe(5); expect(state.undoStack.length).toBe(1); - expect(state.undoStack[0].lines).toEqual(['hello']); + expect(state.undoStack[0].lines).toStrictEqual(['hello']); expect(state.undoStack[0].cursorRow).toBe(0); expect(state.undoStack[0].cursorCol).toBe(5); }); @@ -275,7 +273,7 @@ describe('textBufferReducer', () => { createSingleLineState(input, cursorCol), { type: 'delete_word_left' }, ); - expect(state.lines).toEqual(expectedLines); + expect(state.lines).toStrictEqual(expectedLines); expect(state.cursorCol).toBe(expectedCol); }, ); @@ -290,7 +288,7 @@ describe('textBufferReducer', () => { const state = textBufferReducer(stateWithText, { type: 'delete_word_left', }); - expect(state.lines).toEqual(['helloworld']); + expect(state.lines).toStrictEqual(['helloworld']); expect(state.cursorRow).toBe(0); expect(state.cursorCol).toBe(5); }); @@ -329,7 +327,7 @@ describe('textBufferReducer', () => { createSingleLineState(input, cursorCol), { type: 'delete_word_right' }, ); - expect(state.lines).toEqual(expectedLines); + expect(state.lines).toStrictEqual(expectedLines); expect(state.cursorCol).toBe(expectedCol); }, ); @@ -344,9 +342,9 @@ describe('textBufferReducer', () => { let state = textBufferReducer(stateWithText, { type: 'delete_word_right', }); - expect(state.lines).toEqual(['/to/file']); + expect(state.lines).toStrictEqual(['/to/file']); state = textBufferReducer(state, { type: 'delete_word_right' }); - expect(state.lines).toEqual(['to/file']); + expect(state.lines).toStrictEqual(['to/file']); }); it('should act like delete at the end of a line', () => { @@ -359,7 +357,7 @@ describe('textBufferReducer', () => { const state = textBufferReducer(stateWithText, { type: 'delete_word_right', }); - expect(state.lines).toEqual(['helloworld']); + expect(state.lines).toStrictEqual(['helloworld']); expect(state.cursorRow).toBe(0); expect(state.cursorCol).toBe(5); }); @@ -398,11 +396,11 @@ describe('useTextBuffer', () => { ); const state = getBufferState(result); expect(state.text).toBe(''); - expect(state.lines).toEqual(['']); - expect(state.cursor).toEqual([0, 0]); - expect(state.allVisualLines).toEqual(['']); - expect(state.viewportVisualLines).toEqual(['']); - expect(state.visualCursor).toEqual([0, 0]); + expect(state.lines).toStrictEqual(['']); + expect(state.cursor).toStrictEqual([0, 0]); + expect(state.allVisualLines).toStrictEqual(['']); + expect(state.viewportVisualLines).toStrictEqual(['']); + expect(state.visualCursor).toStrictEqual([0, 0]); expect(state.visualScrollRow).toBe(0); }); @@ -410,34 +408,34 @@ describe('useTextBuffer', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'hello', - viewport, isValidPath: () => false, + viewport, }), ); const state = getBufferState(result); expect(state.text).toBe('hello'); - expect(state.lines).toEqual(['hello']); - expect(state.cursor).toEqual([0, 0]); // Default cursor if offset not given - expect(state.allVisualLines).toEqual(['hello']); - expect(state.viewportVisualLines).toEqual(['hello']); - expect(state.visualCursor).toEqual([0, 0]); + expect(state.lines).toStrictEqual(['hello']); + expect(state.cursor).toStrictEqual([0, 0]); // Default cursor if offset not given + expect(state.allVisualLines).toStrictEqual(['hello']); + expect(state.viewportVisualLines).toStrictEqual(['hello']); + expect(state.visualCursor).toStrictEqual([0, 0]); }); it('should initialize with initialText and initialCursorOffset', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'hello\nworld', - initialCursorOffset: 7, // Should be at 'o' in 'world' - viewport, + initialCursorOffset: 7, isValidPath: () => false, + viewport, }), ); const state = getBufferState(result); expect(state.text).toBe('hello\nworld'); - expect(state.lines).toEqual(['hello', 'world']); - expect(state.cursor).toEqual([1, 1]); // Logical cursor at 'o' in "world" - expect(state.allVisualLines).toEqual(['hello', 'world']); - expect(state.viewportVisualLines).toEqual(['hello', 'world']); + expect(state.lines).toStrictEqual(['hello', 'world']); + expect(state.cursor).toStrictEqual([1, 1]); // Logical cursor at 'o' in "world" + expect(state.allVisualLines).toStrictEqual(['hello', 'world']); + expect(state.viewportVisualLines).toStrictEqual(['hello', 'world']); expect(state.visualCursor[0]).toBe(1); // On the second visual line expect(state.visualCursor[1]).toBe(1); // At 'o' in "world" }); @@ -452,7 +450,7 @@ describe('useTextBuffer', () => { }), ); const state = getBufferState(result); - expect(state.allVisualLines).toEqual([ + expect(state.allVisualLines).toStrictEqual([ 'The quick', 'brown fox', 'jumps over the', @@ -472,7 +470,7 @@ describe('useTextBuffer', () => { // Including multiple spaces at the end of the lines like this is // consistent with Google docs behavior and makes it intuitive to edit // the spaces as needed. - expect(state.allVisualLines).toEqual([ + expect(state.allVisualLines).toStrictEqual([ 'The quick ', 'brown fox ', 'jumps over the', @@ -492,7 +490,10 @@ describe('useTextBuffer', () => { // Including multiple spaces at the end of the lines like this is // consistent with Google docs behavior and makes it intuitive to edit // the spaces as needed. - expect(state.allVisualLines).toEqual(['123456789012345', 'ABCDEFG']); + expect(state.allVisualLines).toStrictEqual([ + '123456789012345', + 'ABCDEFG', + ]); }); it('should initialize with multi-byte unicode characters and correct cursor offset', () => { @@ -506,11 +507,11 @@ describe('useTextBuffer', () => { ); const state = getBufferState(result); expect(state.text).toBe('你好世界'); - expect(state.lines).toEqual(['你好世界']); - expect(state.cursor).toEqual([0, 2]); + expect(state.lines).toStrictEqual(['你好世界']); + expect(state.cursor).toStrictEqual([0, 2]); // Visual: "你好" (width 4), "世"界" (width 4) with viewport width 5 - expect(state.allVisualLines).toEqual(['你好', '世界']); - expect(state.visualCursor).toEqual([1, 0]); + expect(state.allVisualLines).toStrictEqual(['你好', '世界']); + expect(state.visualCursor).toStrictEqual([1, 0]); }); }); @@ -522,56 +523,56 @@ describe('useTextBuffer', () => { act(() => result.current.insert('a')); let state = getBufferState(result); expect(state.text).toBe('a'); - expect(state.cursor).toEqual([0, 1]); - expect(state.visualCursor).toEqual([0, 1]); + expect(state.cursor).toStrictEqual([0, 1]); + expect(state.visualCursor).toStrictEqual([0, 1]); act(() => result.current.insert('b')); state = getBufferState(result); expect(state.text).toBe('ab'); - expect(state.cursor).toEqual([0, 2]); - expect(state.visualCursor).toEqual([0, 2]); + expect(state.cursor).toStrictEqual([0, 2]); + expect(state.visualCursor).toStrictEqual([0, 2]); }); it('insert: should insert text in the middle of a line', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'abc', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.move('right')); act(() => result.current.insert('-NEW-')); const state = getBufferState(result); expect(state.text).toBe('a-NEW-bc'); - expect(state.cursor).toEqual([0, 6]); + expect(state.cursor).toStrictEqual([0, 6]); }); it('newline: should create a new line and move cursor', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'ab', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.move('end')); // cursor at [0,2] act(() => result.current.newline()); const state = getBufferState(result); expect(state.text).toBe('ab\n'); - expect(state.lines).toEqual(['ab', '']); - expect(state.cursor).toEqual([1, 0]); - expect(state.allVisualLines).toEqual(['ab', '']); - expect(state.viewportVisualLines).toEqual(['ab', '']); // viewport height 3 - expect(state.visualCursor).toEqual([1, 0]); // On the new visual line + expect(state.lines).toStrictEqual(['ab', '']); + expect(state.cursor).toStrictEqual([1, 0]); + expect(state.allVisualLines).toStrictEqual(['ab', '']); + expect(state.viewportVisualLines).toStrictEqual(['ab', '']); // viewport height 3 + expect(state.visualCursor).toStrictEqual([1, 0]); // On the new visual line }); it('backspace: should delete char to the left or merge lines', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'a\nb', - viewport, isValidPath: () => false, + viewport, }), ); act(() => { @@ -583,38 +584,38 @@ describe('useTextBuffer', () => { act(() => result.current.backspace()); // delete 'b' let state = getBufferState(result); expect(state.text).toBe('a\n'); - expect(state.cursor).toEqual([1, 0]); + expect(state.cursor).toStrictEqual([1, 0]); act(() => result.current.backspace()); // merge lines state = getBufferState(result); expect(state.text).toBe('a'); - expect(state.cursor).toEqual([0, 1]); // cursor after 'a' - expect(state.allVisualLines).toEqual(['a']); - expect(state.viewportVisualLines).toEqual(['a']); - expect(state.visualCursor).toEqual([0, 1]); + expect(state.cursor).toStrictEqual([0, 1]); // cursor after 'a' + expect(state.allVisualLines).toStrictEqual(['a']); + expect(state.viewportVisualLines).toStrictEqual(['a']); + expect(state.visualCursor).toStrictEqual([0, 1]); }); it('del: should delete char to the right or merge lines', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'a\nb', - viewport, isValidPath: () => false, + viewport, }), ); // cursor at [0,0] act(() => result.current.del()); // delete 'a' let state = getBufferState(result); expect(state.text).toBe('\nb'); - expect(state.cursor).toEqual([0, 0]); + expect(state.cursor).toStrictEqual([0, 0]); act(() => result.current.del()); // merge lines (deletes newline) state = getBufferState(result); expect(state.text).toBe('b'); - expect(state.cursor).toEqual([0, 0]); - expect(state.allVisualLines).toEqual(['b']); - expect(state.viewportVisualLines).toEqual(['b']); - expect(state.visualCursor).toEqual([0, 0]); + expect(state.cursor).toStrictEqual([0, 0]); + expect(state.allVisualLines).toStrictEqual(['b']); + expect(state.viewportVisualLines).toStrictEqual(['b']); + expect(state.visualCursor).toStrictEqual([0, 0]); }); }); @@ -766,19 +767,19 @@ describe('useTextBuffer', () => { // Initial cursor [0,0] logical, visual [0,0] ("l" of "long ") act(() => result.current.move('right')); // visual [0,1] ("o") - expect(getBufferState(result).visualCursor).toEqual([0, 1]); + expect(getBufferState(result).visualCursor).toStrictEqual([0, 1]); act(() => result.current.move('right')); // visual [0,2] ("n") act(() => result.current.move('right')); // visual [0,3] ("g") act(() => result.current.move('right')); // visual [0,4] (" ") - expect(getBufferState(result).visualCursor).toEqual([0, 4]); + expect(getBufferState(result).visualCursor).toStrictEqual([0, 4]); act(() => result.current.move('right')); // visual [1,0] ("l" of "line1") - expect(getBufferState(result).visualCursor).toEqual([1, 0]); - expect(getBufferState(result).cursor).toEqual([0, 5]); // logical cursor + expect(getBufferState(result).visualCursor).toStrictEqual([1, 0]); + expect(getBufferState(result).cursor).toStrictEqual([0, 5]); // logical cursor act(() => result.current.move('left')); // visual [0,4] (" " of "long ") - expect(getBufferState(result).visualCursor).toEqual([0, 4]); - expect(getBufferState(result).cursor).toEqual([0, 4]); // logical cursor + expect(getBufferState(result).visualCursor).toStrictEqual([0, 4]); + expect(getBufferState(result).cursor).toStrictEqual([0, 4]); // logical cursor }); it('move: up/down should preserve preferred visual column', () => { @@ -786,11 +787,15 @@ describe('useTextBuffer', () => { const { result } = renderHook(() => useTextBuffer({ initialText: text, - viewport, isValidPath: () => false, + viewport, }), ); - expect(result.current.allVisualLines).toEqual(['abcde', 'xy', '12345']); + expect(result.current.allVisualLines).toStrictEqual([ + 'abcde', + 'xy', + '12345', + ]); // Place cursor at the end of "abcde" -> logical [0,5] act(() => { result.current.move('home'); // to [0,0] @@ -800,22 +805,22 @@ describe('useTextBuffer', () => { result.current.move('right'); // to [0,5] }); } - expect(getBufferState(result).cursor).toEqual([0, 5]); - expect(getBufferState(result).visualCursor).toEqual([0, 5]); + expect(getBufferState(result).cursor).toStrictEqual([0, 5]); + expect(getBufferState(result).visualCursor).toStrictEqual([0, 5]); // Set preferredCol by moving up then down to the same spot, then test. act(() => { result.current.move('down'); // to xy, logical [1,2], visual [1,2], preferredCol should be 5 }); let state = getBufferState(result); - expect(state.cursor).toEqual([1, 2]); // Logical cursor at end of 'xy' - expect(state.visualCursor).toEqual([1, 2]); // Visual cursor at end of 'xy' + expect(state.cursor).toStrictEqual([1, 2]); // Logical cursor at end of 'xy' + expect(state.visualCursor).toStrictEqual([1, 2]); // Visual cursor at end of 'xy' expect(state.preferredCol).toBe(5); act(() => result.current.move('down')); // to '12345', preferredCol=5. state = getBufferState(result); - expect(state.cursor).toEqual([2, 5]); // Logical cursor at end of '12345' - expect(state.visualCursor).toEqual([2, 5]); // Visual cursor at end of '12345' + expect(state.cursor).toStrictEqual([2, 5]); // Logical cursor at end of '12345' + expect(state.visualCursor).toStrictEqual([2, 5]); // Visual cursor at end of '12345' expect(state.preferredCol).toBe(5); // Preferred col is maintained act(() => result.current.move('left')); // preferredCol should reset @@ -832,7 +837,7 @@ describe('useTextBuffer', () => { isValidPath: () => false, }), ); - expect(result.current.allVisualLines).toEqual([ + expect(result.current.allVisualLines).toStrictEqual([ 'line', 'one', 'secon', @@ -842,13 +847,13 @@ describe('useTextBuffer', () => { // Initial cursor [0,0] (start of "line") act(() => result.current.move('down')); // visual cursor from [0,0] to [1,0] ("o" of "one") act(() => result.current.move('right')); // visual cursor to [1,1] ("n" of "one") - expect(getBufferState(result).visualCursor).toEqual([1, 1]); + expect(getBufferState(result).visualCursor).toStrictEqual([1, 1]); act(() => result.current.move('home')); // visual cursor to [1,0] (start of "one") - expect(getBufferState(result).visualCursor).toEqual([1, 0]); + expect(getBufferState(result).visualCursor).toStrictEqual([1, 0]); act(() => result.current.move('end')); // visual cursor to [1,3] (end of "one") - expect(getBufferState(result).visualCursor).toEqual([1, 3]); // "one" is 3 chars + expect(getBufferState(result).visualCursor).toStrictEqual([1, 3]); // "one" is 3 chars }); }); @@ -884,14 +889,14 @@ describe('useTextBuffer', () => { ); // Initial: l1, l2, l3 visible. visualScrollRow = 0. visualCursor = [0,0] expect(getBufferState(result).visualScrollRow).toBe(0); - expect(getBufferState(result).allVisualLines).toEqual([ + expect(getBufferState(result).allVisualLines).toStrictEqual([ 'l1', 'l2', 'l3', 'l4', 'l5', ]); - expect(getBufferState(result).viewportVisualLines).toEqual([ + expect(getBufferState(result).viewportVisualLines).toStrictEqual([ 'l1', 'l2', 'l3', @@ -905,9 +910,15 @@ describe('useTextBuffer', () => { // Now: l2, l3, l4 visible. visualScrollRow = 1. let state = getBufferState(result); expect(state.visualScrollRow).toBe(1); - expect(state.allVisualLines).toEqual(['l1', 'l2', 'l3', 'l4', 'l5']); - expect(state.viewportVisualLines).toEqual(['l2', 'l3', 'l4']); - expect(state.visualCursor).toEqual([3, 0]); + expect(state.allVisualLines).toStrictEqual([ + 'l1', + 'l2', + 'l3', + 'l4', + 'l5', + ]); + expect(state.viewportVisualLines).toStrictEqual(['l2', 'l3', 'l4']); + expect(state.visualCursor).toStrictEqual([3, 0]); act(() => result.current.move('up')); // vc=[2,0] (l3) act(() => result.current.move('up')); // vc=[1,0] (l2) @@ -917,9 +928,15 @@ describe('useTextBuffer', () => { // Now: l1, l2, l3 visible. visualScrollRow = 0 state = getBufferState(result); // Assign to the existing `state` variable expect(state.visualScrollRow).toBe(0); - expect(state.allVisualLines).toEqual(['l1', 'l2', 'l3', 'l4', 'l5']); - expect(state.viewportVisualLines).toEqual(['l1', 'l2', 'l3']); - expect(state.visualCursor).toEqual([0, 0]); + expect(state.allVisualLines).toStrictEqual([ + 'l1', + 'l2', + 'l3', + 'l4', + 'l5', + ]); + expect(state.viewportVisualLines).toStrictEqual(['l1', 'l2', 'l3']); + expect(state.visualCursor).toStrictEqual([0, 0]); }); }); @@ -933,19 +950,19 @@ describe('useTextBuffer', () => { act(() => result.current.undo()); expect(getBufferState(result).text).toBe(''); - expect(getBufferState(result).cursor).toEqual([0, 0]); + expect(getBufferState(result).cursor).toStrictEqual([0, 0]); act(() => result.current.redo()); expect(getBufferState(result).text).toBe('a'); - expect(getBufferState(result).cursor).toEqual([0, 1]); + expect(getBufferState(result).cursor).toStrictEqual([0, 1]); }); it('should undo and redo a newline operation', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'test', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.move('end')); @@ -954,11 +971,11 @@ describe('useTextBuffer', () => { act(() => result.current.undo()); expect(getBufferState(result).text).toBe('test'); - expect(getBufferState(result).cursor).toEqual([0, 4]); + expect(getBufferState(result).cursor).toStrictEqual([0, 4]); act(() => result.current.redo()); expect(getBufferState(result).text).toBe('test\n'); - expect(getBufferState(result).cursor).toEqual([1, 0]); + expect(getBufferState(result).cursor).toStrictEqual([1, 0]); }); }); @@ -970,28 +987,28 @@ describe('useTextBuffer', () => { act(() => result.current.insert('你好')); const state = getBufferState(result); expect(state.text).toBe('你好'); - expect(state.cursor).toEqual([0, 2]); // Cursor is 2 (char count) - expect(state.visualCursor).toEqual([0, 2]); + expect(state.cursor).toStrictEqual([0, 2]); // Cursor is 2 (char count) + expect(state.visualCursor).toStrictEqual([0, 2]); }); it('backspace: should correctly delete multi-byte unicode characters', () => { const { result } = renderHook(() => useTextBuffer({ initialText: '你好', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.move('end')); // cursor at [0,2] act(() => result.current.backspace()); // delete '好' let state = getBufferState(result); expect(state.text).toBe('你'); - expect(state.cursor).toEqual([0, 1]); + expect(state.cursor).toStrictEqual([0, 1]); act(() => result.current.backspace()); // delete '你' state = getBufferState(result); expect(state.text).toBe(''); - expect(state.cursor).toEqual([0, 0]); + expect(state.cursor).toStrictEqual([0, 0]); }); it('move: left/right should treat multi-byte chars as single units for visual cursor', () => { @@ -1005,18 +1022,18 @@ describe('useTextBuffer', () => { // Initial: visualCursor [0,0] act(() => result.current.move('right')); // visualCursor [0,1] (after 🐶) let state = getBufferState(result); - expect(state.cursor).toEqual([0, 1]); - expect(state.visualCursor).toEqual([0, 1]); + expect(state.cursor).toStrictEqual([0, 1]); + expect(state.visualCursor).toStrictEqual([0, 1]); act(() => result.current.move('right')); // visualCursor [0,2] (after 🐱) state = getBufferState(result); - expect(state.cursor).toEqual([0, 2]); - expect(state.visualCursor).toEqual([0, 2]); + expect(state.cursor).toStrictEqual([0, 2]); + expect(state.visualCursor).toStrictEqual([0, 2]); act(() => result.current.move('left')); // visualCursor [0,1] (before 🐱 / after 🐶) state = getBufferState(result); - expect(state.cursor).toEqual([0, 1]); - expect(state.visualCursor).toEqual([0, 1]); + expect(state.cursor).toStrictEqual([0, 1]); + expect(state.visualCursor).toStrictEqual([0, 1]); }); it('moveToVisualPosition: should correctly handle wide characters (Chinese)', () => { @@ -1032,19 +1049,19 @@ describe('useTextBuffer', () => { // Click on '你' (first half, x=0) -> index 0 act(() => result.current.moveToVisualPosition(0, 0)); - expect(getBufferState(result).cursor).toEqual([0, 0]); + expect(getBufferState(result).cursor).toStrictEqual([0, 0]); // Click on '你' (second half, x=1) -> index 1 (after first char) act(() => result.current.moveToVisualPosition(0, 1)); - expect(getBufferState(result).cursor).toEqual([0, 1]); + expect(getBufferState(result).cursor).toStrictEqual([0, 1]); // Click on '好' (first half, x=2) -> index 1 (before second char) act(() => result.current.moveToVisualPosition(0, 2)); - expect(getBufferState(result).cursor).toEqual([0, 1]); + expect(getBufferState(result).cursor).toStrictEqual([0, 1]); // Click on '好' (second half, x=3) -> index 2 (after second char) act(() => result.current.moveToVisualPosition(0, 3)); - expect(getBufferState(result).cursor).toEqual([0, 2]); + expect(getBufferState(result).cursor).toStrictEqual([0, 2]); }); }); @@ -1087,7 +1104,7 @@ describe('useTextBuffer', () => { sequence: '\r', }), ); - expect(getBufferState(result).lines).toEqual(['', '']); + expect(getBufferState(result).lines).toStrictEqual(['', '']); }); it('should handle Ctrl+J as newline', () => { @@ -1143,8 +1160,8 @@ describe('useTextBuffer', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'a', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.move('end')); @@ -1164,12 +1181,12 @@ describe('useTextBuffer', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'abcde', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.move('end')); // cursor at the end - expect(getBufferState(result).cursor).toEqual([0, 5]); + expect(getBufferState(result).cursor).toStrictEqual([0, 5]); act(() => { result.current.handleInput({ @@ -1195,51 +1212,51 @@ describe('useTextBuffer', () => { }); }); expect(getBufferState(result).text).toBe('ab'); - expect(getBufferState(result).cursor).toEqual([0, 2]); + expect(getBufferState(result).cursor).toStrictEqual([0, 2]); }); it('should handle inserts that contain delete characters', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'abcde', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.move('end')); // cursor at the end - expect(getBufferState(result).cursor).toEqual([0, 5]); + expect(getBufferState(result).cursor).toStrictEqual([0, 5]); act(() => { result.current.insert('\x7f\x7f\x7f'); }); expect(getBufferState(result).text).toBe('ab'); - expect(getBufferState(result).cursor).toEqual([0, 2]); + expect(getBufferState(result).cursor).toStrictEqual([0, 2]); }); it('should handle inserts with a mix of regular and delete characters', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'abcde', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.move('end')); // cursor at the end - expect(getBufferState(result).cursor).toEqual([0, 5]); + expect(getBufferState(result).cursor).toStrictEqual([0, 5]); act(() => { result.current.insert('\x7fI\x7f\x7fNEW'); }); expect(getBufferState(result).text).toBe('abcNEW'); - expect(getBufferState(result).cursor).toEqual([0, 6]); + expect(getBufferState(result).cursor).toStrictEqual([0, 6]); }); it('should handle arrow keys for movement', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'ab', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.move('end')); // cursor [0,2] @@ -1252,7 +1269,7 @@ describe('useTextBuffer', () => { sequence: '\x1b[D', }), ); // cursor [0,1] - expect(getBufferState(result).cursor).toEqual([0, 1]); + expect(getBufferState(result).cursor).toStrictEqual([0, 1]); act(() => result.current.handleInput({ name: 'right', @@ -1262,15 +1279,15 @@ describe('useTextBuffer', () => { sequence: '\x1b[C', }), ); // cursor [0,2] - expect(getBufferState(result).cursor).toEqual([0, 2]); + expect(getBufferState(result).cursor).toStrictEqual([0, 2]); }); it('should handle up/down arrow keys for vertical movement via handleInput', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'abc\ndef\nghi', - viewport, isValidPath: () => false, + viewport, }), ); // Start at [0,0], move down twice then up @@ -1312,8 +1329,8 @@ describe('useTextBuffer', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'abc\ndef', - viewport, isValidPath: () => false, + viewport, }), ); // Move to line 1 @@ -1402,7 +1419,7 @@ describe('useTextBuffer', () => { sequence: '\r', }), ); // Simulates Shift+Enter in VSCode terminal - expect(getBufferState(result).lines).toEqual(['', '']); + expect(getBufferState(result).lines).toStrictEqual(['', '']); }); it('should correctly handle repeated pasting of long text', () => { @@ -1434,7 +1451,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots state.text, state.text.length, ); - expect(state.cursor).toEqual(expectedCursorPos); + expect(state.cursor).toStrictEqual(expectedCursorPos); }); }); @@ -1451,106 +1468,106 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots const { result } = renderHook(() => useTextBuffer({ initialText: '@pac', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.replaceRange(0, 1, 0, 4, 'packages')); const state = getBufferState(result); expect(state.text).toBe('@packages'); - expect(state.cursor).toEqual([0, 9]); // cursor after 'typescript' + expect(state.cursor).toStrictEqual([0, 9]); // cursor after 'typescript' }); it('should replace a multi-line range with single-line text', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'hello\nworld\nagain', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.replaceRange(0, 2, 1, 3, ' new ')); // replace 'llo\nwor' with ' new ' const state = getBufferState(result); expect(state.text).toBe('he new ld\nagain'); - expect(state.cursor).toEqual([0, 7]); // cursor after ' new ' + expect(state.cursor).toStrictEqual([0, 7]); // cursor after ' new ' }); it('should delete a range when replacing with an empty string', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'hello world', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.replaceRange(0, 5, 0, 11, '')); // delete ' world' const state = getBufferState(result); expect(state.text).toBe('hello'); - expect(state.cursor).toEqual([0, 5]); + expect(state.cursor).toStrictEqual([0, 5]); }); it('should handle replacing at the beginning of the text', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'world', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.replaceRange(0, 0, 0, 0, 'hello ')); const state = getBufferState(result); expect(state.text).toBe('hello world'); - expect(state.cursor).toEqual([0, 6]); + expect(state.cursor).toStrictEqual([0, 6]); }); it('should handle replacing at the end of the text', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'hello', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.replaceRange(0, 5, 0, 5, ' world')); const state = getBufferState(result); expect(state.text).toBe('hello world'); - expect(state.cursor).toEqual([0, 11]); + expect(state.cursor).toStrictEqual([0, 11]); }); it('should handle replacing the entire buffer content', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'old text', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.replaceRange(0, 0, 0, 8, 'new text')); const state = getBufferState(result); expect(state.text).toBe('new text'); - expect(state.cursor).toEqual([0, 8]); + expect(state.cursor).toStrictEqual([0, 8]); }); it('should correctly replace with unicode characters', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'hello *** world', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.replaceRange(0, 6, 0, 9, '你好')); const state = getBufferState(result); expect(state.text).toBe('hello 你好 world'); - expect(state.cursor).toEqual([0, 8]); // after '你好' + expect(state.cursor).toStrictEqual([0, 8]); // after '你好' }); it('should handle invalid range by returning false and not changing text', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'test', - viewport, isValidPath: () => false, + viewport, }), ); act(() => { @@ -1569,30 +1586,30 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots const { result } = renderHook(() => useTextBuffer({ initialText: 'first\nsecond\nthird', - viewport, isValidPath: () => false, + viewport, }), ); act(() => result.current.replaceRange(0, 2, 2, 3, 'X')); // Replace 'rst\nsecond\nthi' const state = getBufferState(result); expect(state.text).toBe('fiXrd'); - expect(state.cursor).toEqual([0, 3]); // After 'X' + expect(state.cursor).toStrictEqual([0, 3]); // After 'X' }); it('should replace a single-line range with multi-line text', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'one two three', - viewport, isValidPath: () => false, + viewport, }), ); // Replace "two" with "new\nline" act(() => result.current.replaceRange(0, 4, 0, 7, 'new\nline')); const state = getBufferState(result); - expect(state.lines).toEqual(['one new', 'line three']); + expect(state.lines).toStrictEqual(['one new', 'line three']); expect(state.text).toBe('one new\nline three'); - expect(state.cursor).toEqual([1, 4]); // cursor after 'line' + expect(state.cursor).toStrictEqual([1, 4]); // cursor after 'line' }); }); @@ -1871,24 +1888,24 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots act(() => result.current.insert('\n')); const state = getBufferState(result); expect(state.text).toBe(''); - expect(state.lines).toEqual(['']); + expect(state.lines).toStrictEqual(['']); }); it('should not create a new line when newline() is called and singleLine is true', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'ab', - viewport, isValidPath: () => false, singleLine: true, + viewport, }), ); act(() => result.current.move('end')); // cursor at [0,2] act(() => result.current.newline()); const state = getBufferState(result); expect(state.text).toBe('ab'); - expect(state.lines).toEqual(['ab']); - expect(state.cursor).toEqual([0, 2]); + expect(state.lines).toStrictEqual(['ab']); + expect(state.cursor).toStrictEqual([0, 2]); }); it('should not handle "Enter" key as newline when singleLine is true', () => { @@ -1908,7 +1925,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots sequence: '\r', }), ); - expect(getBufferState(result).lines).toEqual(['']); + expect(getBufferState(result).lines).toStrictEqual(['']); }); it('should strip newlines from pasted text when singleLine is true', () => { @@ -1922,7 +1939,7 @@ Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots act(() => result.current.insert('hello\nworld', { paste: true })); const state = getBufferState(result); expect(state.text).toBe('helloworld'); - expect(state.lines).toEqual(['helloworld']); + expect(state.lines).toStrictEqual(['helloworld']); }); }); }); @@ -2060,7 +2077,7 @@ describe('offsetToLogicalPos', () => { { text: '🐶🐱', offset: 1, expected: [0, 1], desc: 'emoji - middle' }, { text: '🐶🐱', offset: 2, expected: [0, 2], desc: 'emoji - end' }, ])('should handle $desc', ({ text, offset, expected }) => { - expect(offsetToLogicalPos(text, offset)).toEqual(expected); + expect(offsetToLogicalPos(text, offset)).toStrictEqual(expected); }); describe('multi-line text', () => { @@ -2080,7 +2097,7 @@ describe('offsetToLogicalPos', () => { ])( 'should return $expected for $desc (offset $offset)', ({ offset, expected }) => { - expect(offsetToLogicalPos(text, offset)).toEqual(expected); + expect(offsetToLogicalPos(text, offset)).toStrictEqual(expected); }, ); }); @@ -2182,7 +2199,7 @@ describe('textBufferReducer vim operations', () => { expect(result).toHaveOnlyValidCharacters(); // After deleting line2, we should have line1 and line3, with cursor on line3 (now at index 1) - expect(result.lines).toEqual(['line1', 'line3']); + expect(result.lines).toStrictEqual(['line1', 'line3']); expect(result.cursorRow).toBe(1); expect(result.cursorCol).toBe(0); }); @@ -2199,7 +2216,7 @@ describe('textBufferReducer vim operations', () => { expect(result).toHaveOnlyValidCharacters(); // Should delete line2 and line3, leaving line1 and line4 - expect(result.lines).toEqual(['line1', 'line4']); + expect(result.lines).toStrictEqual(['line1', 'line4']); expect(result.cursorRow).toBe(1); expect(result.cursorCol).toBe(0); }); @@ -2216,7 +2233,7 @@ describe('textBufferReducer vim operations', () => { expect(result).toHaveOnlyValidCharacters(); // Should clear the line content but keep the line - expect(result.lines).toEqual(['']); + expect(result.lines).toStrictEqual(['']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(0); }); @@ -2233,7 +2250,7 @@ describe('textBufferReducer vim operations', () => { expect(result).toHaveOnlyValidCharacters(); // Should delete the last line completely, not leave empty line - expect(result.lines).toEqual(['line1']); + expect(result.lines).toStrictEqual(['line1']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(0); }); @@ -2251,7 +2268,7 @@ describe('textBufferReducer vim operations', () => { expect(afterDelete).toHaveOnlyValidCharacters(); // After deleting all lines, should have one empty line - expect(afterDelete.lines).toEqual(['']); + expect(afterDelete.lines).toStrictEqual(['']); expect(afterDelete.cursorRow).toBe(0); expect(afterDelete.cursorCol).toBe(0); @@ -2265,7 +2282,7 @@ describe('textBufferReducer vim operations', () => { expect(afterPaste).toHaveOnlyValidCharacters(); // All lines including the first one should be present - expect(afterPaste.lines).toEqual(['new1', 'new2', 'new3', 'new4']); + expect(afterPaste.lines).toStrictEqual(['new1', 'new2', 'new3', 'new4']); expect(afterPaste.cursorRow).toBe(3); expect(afterPaste.cursorCol).toBe(4); }); @@ -2369,9 +2386,9 @@ describe('Unicode helper functions', () => { const { result } = renderHook(() => useTextBuffer({ initialText: '你好世界', - initialCursorOffset: 4, // End of string - viewport, + initialCursorOffset: 4, isValidPath: () => false, + viewport, }), ); @@ -2428,9 +2445,9 @@ describe('Unicode helper functions', () => { const { result } = renderHook(() => useTextBuffer({ initialText: 'Hello你好World', - initialCursorOffset: 10, // End - viewport, + initialCursorOffset: 10, isValidPath: () => false, + viewport, }), ); @@ -2665,8 +2682,8 @@ describe('Transformation Utilities', () => { const { result } = renderHookWithProviders(() => useTextBuffer({ initialText: 'original line', - viewport, isValidPath: () => true, + viewport, }), ); @@ -2709,8 +2726,8 @@ describe('Transformation Utilities', () => { const { result } = renderHookWithProviders(() => useTextBuffer({ initialText: text, - viewport, isValidPath: () => true, + viewport, }), ); diff --git a/packages/cli/src/ui/components/shared/text-buffer.ts b/packages/cli/src/ui/components/shared/text-buffer.ts index 14502c9bb5..54959c2b77 100644 --- a/packages/cli/src/ui/components/shared/text-buffer.ts +++ b/packages/cli/src/ui/components/shared/text-buffer.ts @@ -154,8 +154,8 @@ function processInsertText( opts.paste ) { let potentialPath = textToInsert.trim(); - const quoteMatch = potentialPath.match(/^'(.*)'$/); - if (quoteMatch) { + const quoteMatch = RegExp(/^'(.*)'$/).exec(potentialPath); + if (quoteMatch != null) { potentialPath = quoteMatch[1]; } potentialPath = potentialPath.trim(); @@ -196,9 +196,10 @@ function resolveVisualToLogical( Math.min(visRow, layout.visualLines.length - 1), ); const visualLine = layout.visualLines[clampedVisRow] || ''; - const mapping = layout.visualToLogicalMap[clampedVisRow]; + const mapping: [number, number] | undefined = + layout.visualToLogicalMap[clampedVisRow]; - if (!mapping) return null; + if (mapping == null) return null; const [logRow, logStartCol] = mapping; const codePoints = toCodePoints(visualLine); @@ -248,7 +249,7 @@ async function runExternalEditor(params: { const args = [filePath]; const preferredEditorType = getPrefEditor?.(); - if (!command && preferredEditorType) { + if (preferredEditorType != null) { command = getEditorCommand(preferredEditorType); if (isGuiEditor(preferredEditorType)) { args.unshift('--wait'); @@ -268,7 +269,7 @@ async function runExternalEditor(params: { try { setRawMode?.(false); const { status, error } = spawnSync(command, args, { stdio: 'inherit' }); - if (error) throw error; + if (error != null) throw error; if (typeof status === 'number' && status !== 0) throw new Error(`External editor exited with status ${status}`); @@ -326,7 +327,6 @@ export function useTextBuffer({ lines: lines.length === 0 ? [''] : lines, cursorRow: initialCursorRow, cursorCol: initialCursorCol, - transformationsByLine, preferredCol: null, undoStack: [], redoStack: [], @@ -334,6 +334,7 @@ export function useTextBuffer({ selectionAnchor: null, viewportWidth: viewport.width, viewportHeight: viewport.height, + transformationsByLine, visualLayout, }; }, [initialText, initialCursorOffset, viewport.width, viewport.height]); @@ -364,7 +365,7 @@ export function useTextBuffer({ const [visualScrollRow, setVisualScrollRow] = useState(0); useEffect(() => { - if (onChange) { + if (onChange != null) { onChange(text); } }, [text, onChange]); @@ -509,7 +510,7 @@ export function useTextBuffer({ else if (keyMatchers[Command.DELETE_CHAR_RIGHT](key)) del(); else if (keyMatchers[Command.UNDO](key)) undo(); else if (keyMatchers[Command.REDO](key)) redo(); - else if (key.insertable) { + else if (key.insertable === true) { insert(input, { paste: false }); } }, @@ -564,7 +565,7 @@ export function useTextBuffer({ const moveToVisualPosition = useCallback( (visRow: number, visCol: number): void => { const pos = resolveVisualToLogical(visualLayout, visRow, visCol); - if (pos) { + if (pos != null) { dispatch({ type: 'set_cursor', payload: pos }); } }, @@ -578,20 +579,19 @@ export function useTextBuffer({ const returnValue: TextBuffer = useMemo( () => ({ - lines, - text, cursor: [cursorRow, cursorCol], - preferredCol, - selectionAnchor, - allVisualLines: visualLines, viewportVisualLines: renderedVisualLines, - visualCursor, - visualScrollRow, visualToLogicalMap: visualLayout.visualToLogicalMap, transformationsByLine: state.transformationsByLine, visualToTransformedMap: visualLayout.visualToTransformedMap, - + ...vimCallbacks, + lines, + text, + preferredCol, + selectionAnchor, + visualCursor, + visualScrollRow, setText, insert, newline, @@ -611,7 +611,6 @@ export function useTextBuffer({ killLineLeft, handleInput, openInExternalEditor, - ...vimCallbacks, }), [ lines, diff --git a/packages/cli/src/ui/components/shared/transformations.test.ts b/packages/cli/src/ui/components/shared/transformations.test.ts index 136082e872..d672c425c2 100644 --- a/packages/cli/src/ui/components/shared/transformations.test.ts +++ b/packages/cli/src/ui/components/shared/transformations.test.ts @@ -73,7 +73,7 @@ describe('transformations', () => { it('should return empty array for no matches', () => { const line = 'Just plain text'; const result = calculateTransformationsForLine(line); - expect(result).toEqual([]); + expect(result).toStrictEqual([]); }); it('should calculate correct positions', () => { @@ -103,13 +103,13 @@ describe('transformations', () => { it('should return empty arrays for lines without images', () => { const lines = ['line 1', 'line 2']; const result = calculateTransformations(lines); - expect(result[0]).toEqual([]); - expect(result[1]).toEqual([]); + expect(result[0]).toStrictEqual([]); + expect(result[1]).toStrictEqual([]); }); it('should handle empty lines array', () => { const result = calculateTransformations([]); - expect(result).toEqual([]); + expect(result).toStrictEqual([]); }); }); diff --git a/packages/cli/src/ui/components/shared/vim-action-handlers.ts b/packages/cli/src/ui/components/shared/vim-action-handlers.ts index f698384c54..2c09f4002c 100644 --- a/packages/cli/src/ui/components/shared/vim-action-handlers.ts +++ b/packages/cli/src/ui/components/shared/vim-action-handlers.ts @@ -67,7 +67,7 @@ export function handleDeleteWordForward( for (let i = 0; i < count; i++) { const nextWord = findNextWordAcrossLines(lines, endRow, endCol, true); - if (nextWord) { + if (nextWord != null) { endRow = nextWord.row; endCol = nextWord.col; } else { @@ -105,7 +105,7 @@ export function handleDeleteWordBackward( for (let i = 0; i < count; i++) { const prevWord = findPrevWordAcrossLines(lines, startRow, startCol); - if (!prevWord) break; + if (prevWord == null) break; startRow = prevWord.row; startCol = prevWord.col; } @@ -148,7 +148,7 @@ function advanceWordEnd( let remaining = count; while (remaining > 0) { const wordEnd = nextWordEndPos(lines, r, c); - if (!wordEnd) { + if (wordEnd == null) { remaining = 0; } else { r = wordEnd.row; @@ -551,7 +551,7 @@ export function handleMoveWordForward( for (let i = 0; i < count; i++) { const nextWord = findNextWordAcrossLines(lines, row, col, true); - if (!nextWord) break; + if (nextWord == null) break; row = nextWord.row; col = nextWord.col; } @@ -569,7 +569,7 @@ export function handleMoveWordBackward( for (let i = 0; i < count; i++) { const prevWord = findPrevWordAcrossLines(lines, row, col); - if (!prevWord) break; + if (prevWord == null) break; row = prevWord.row; col = prevWord.col; } @@ -616,7 +616,7 @@ export function handleMoveWordEnd( for (let i = 0; i < count; i++) { const wordEnd = stepWordEnd(lines, row, col, i === 0); - if (!wordEnd) break; + if (wordEnd == null) break; row = wordEnd.row; col = wordEnd.col; } @@ -690,7 +690,7 @@ export function handleOpenLineAbove(state: TextBufferState): TextBufferState { 0, '\n', ); - return { ...resultState, cursorRow, cursorCol: 0 }; + return { ...resultState, cursorCol: 0, cursorRow }; } export function handleAppendAtLineEnd(state: TextBufferState): TextBufferState { diff --git a/packages/cli/src/ui/components/shared/vim-buffer-actions.test.ts b/packages/cli/src/ui/components/shared/vim-buffer-actions.test.ts index bc725740af..b1dad671dc 100644 --- a/packages/cli/src/ui/components/shared/vim-buffer-actions.test.ts +++ b/packages/cli/src/ui/components/shared/vim-buffer-actions.test.ts @@ -612,7 +612,7 @@ describe('vim-buffer-actions', () => { const result = handleVimAction(state, action); expect(result).toHaveOnlyValidCharacters(); - expect(result.lines).toEqual(['line1', 'line3']); + expect(result.lines).toStrictEqual(['line1', 'line3']); expect(result.cursorRow).toBe(1); expect(result.cursorCol).toBe(0); }); @@ -626,7 +626,7 @@ describe('vim-buffer-actions', () => { const result = handleVimAction(state, action); expect(result).toHaveOnlyValidCharacters(); - expect(result.lines).toEqual(['line3']); + expect(result.lines).toStrictEqual(['line3']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(0); }); @@ -640,7 +640,7 @@ describe('vim-buffer-actions', () => { const result = handleVimAction(state, action); expect(result).toHaveOnlyValidCharacters(); - expect(result.lines).toEqual(['']); + expect(result.lines).toStrictEqual(['']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(0); }); @@ -739,7 +739,7 @@ describe('vim-buffer-actions', () => { const result = handleVimAction(state, action); expect(result).toHaveOnlyValidCharacters(); - expect(result.lines).toEqual(['hello world', '']); + expect(result.lines).toStrictEqual(['hello world', '']); expect(result.cursorRow).toBe(1); expect(result.cursorCol).toBe(0); }); @@ -752,7 +752,7 @@ describe('vim-buffer-actions', () => { const result = handleVimAction(state, action); expect(result).toHaveOnlyValidCharacters(); - expect(result.lines).toEqual(['hello', '', 'world']); + expect(result.lines).toStrictEqual(['hello', '', 'world']); expect(result.cursorRow).toBe(1); expect(result.cursorCol).toBe(0); }); @@ -848,7 +848,7 @@ describe('vim-buffer-actions', () => { expect(result).toHaveOnlyValidCharacters(); // The movement 'j' with count 2 changes 2 lines starting from cursor row // Since we're at cursor position 2, it changes lines starting from current row - expect(result.lines).toEqual(['line1', 'line2', 'line3']); // No change because count > available lines + expect(result.lines).toStrictEqual(['line1', 'line2', 'line3']); // No change because count > available lines expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(2); }); diff --git a/packages/cli/src/ui/components/shared/visual-layout.ts b/packages/cli/src/ui/components/shared/visual-layout.ts index 4696fd2086..3b4c1ac55c 100644 --- a/packages/cli/src/ui/components/shared/visual-layout.ts +++ b/packages/cli/src/ui/components/shared/visual-layout.ts @@ -263,8 +263,8 @@ function appendLogicalLineLayout( visualLines: lineVisualLines, logicalToVisualMap: lineLogicalToVisualMap, visualToLogicalMap: lineVisualToLogicalMap, - transformedToLogMap, visualToTransformedMap: lineVisualToTransformedMap, + transformedToLogMap, }); } diff --git a/packages/cli/src/ui/components/shared/word-navigation.test.ts b/packages/cli/src/ui/components/shared/word-navigation.test.ts index de08ca366e..e059527a4b 100644 --- a/packages/cli/src/ui/components/shared/word-navigation.test.ts +++ b/packages/cli/src/ui/components/shared/word-navigation.test.ts @@ -162,13 +162,13 @@ describe('word-navigation', () => { it('should find next word on same line', () => { const lines = ['hello world', 'second line']; const result = findNextWordAcrossLines(lines, 0, 0, true); - expect(result).toEqual({ row: 0, col: 6 }); + expect(result).toStrictEqual({ row: 0, col: 6 }); }); it('should find word on next line', () => { const lines = ['hello ', 'world']; const result = findNextWordAcrossLines(lines, 0, 6, true); - expect(result).toEqual({ row: 1, col: 0 }); + expect(result).toStrictEqual({ row: 1, col: 0 }); }); it('should return null if no more words', () => { @@ -182,13 +182,13 @@ describe('word-navigation', () => { it('should find previous word on same line', () => { const lines = ['hello world']; const result = findPrevWordAcrossLines(lines, 0, 8); - expect(result).toEqual({ row: 0, col: 6 }); // Start of 'world' + expect(result).toStrictEqual({ row: 0, col: 6 }); // Start of 'world' }); it('should find word on previous line', () => { const lines = ['hello', ' world']; const result = findPrevWordAcrossLines(lines, 1, 1); - expect(result).toEqual({ row: 0, col: 0 }); // Start of 'hello' + expect(result).toStrictEqual({ row: 0, col: 0 }); // Start of 'hello' }); it('should return null at document start', () => { diff --git a/packages/cli/src/ui/components/shared/word-navigation.ts b/packages/cli/src/ui/components/shared/word-navigation.ts index 1f4b4f2989..faa1333422 100644 --- a/packages/cli/src/ui/components/shared/word-navigation.ts +++ b/packages/cli/src/ui/components/shared/word-navigation.ts @@ -168,13 +168,12 @@ export const findPrevWordStartInLine = ( i--; } return i + 1; - } else { - // We're in punctuation, move to its beginning - while (i >= 0 && !isWordCharStrict(chars[i]) && !isWhitespace(chars[i])) { - i--; - } - return i + 1; } + // We're in punctuation, move to its beginning + while (i >= 0 && !isWordCharStrict(chars[i]) && !isWhitespace(chars[i])) { + i--; + } + return i + 1; }; /** diff --git a/packages/cli/src/ui/components/views/ChatList.tsx b/packages/cli/src/ui/components/views/ChatList.tsx index 6f5ea1adf2..bbc59b882e 100644 --- a/packages/cli/src/ui/components/views/ChatList.tsx +++ b/packages/cli/src/ui/components/views/ChatList.tsx @@ -29,12 +29,11 @@ export const ChatList: React.FC = ({ chats }) => { {chats.map((chat) => { const isoString = chat.mtime; - const match = isoString.match( - /(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/, + const match = RegExp(/(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/).exec( + isoString, ); - const formattedDate = match - ? `${match[1]} ${match[2]}` - : 'Invalid Date'; + const formattedDate = + match != null ? `${match[1]} ${match[2]}` : 'Invalid Date'; return ( diff --git a/packages/cli/src/ui/components/views/SkillsList.test.tsx b/packages/cli/src/ui/components/views/SkillsList.test.tsx index 2893e5b705..16afb69062 100644 --- a/packages/cli/src/ui/components/views/SkillsList.test.tsx +++ b/packages/cli/src/ui/components/views/SkillsList.test.tsx @@ -7,7 +7,7 @@ import { render } from '../../../test-utils/render.js'; import { describe, it, expect } from 'vitest'; import { SkillsList } from './SkillsList.js'; -import { type SkillDefinition } from '@vybestack/llxprt-code-core'; +import type { SkillDefinition } from '@vybestack/llxprt-code-core'; describe('SkillsList Component', () => { const mockSkills: SkillDefinition[] = [ diff --git a/packages/cli/src/ui/components/views/SkillsList.tsx b/packages/cli/src/ui/components/views/SkillsList.tsx index 28cf198413..6955613aac 100644 --- a/packages/cli/src/ui/components/views/SkillsList.tsx +++ b/packages/cli/src/ui/components/views/SkillsList.tsx @@ -7,7 +7,7 @@ import type React from 'react'; import { Box, Text } from 'ink'; import { theme } from '../../semantic-colors.js'; -import { type SkillDefinition } from '../../types.js'; +import type { SkillDefinition } from '../../types.js'; interface SkillsListProps { skills: readonly SkillDefinition[]; diff --git a/packages/cli/src/ui/constants/phrasesCollections.ts b/packages/cli/src/ui/constants/phrasesCollections.ts index 4646e59d7a..b5247837a4 100644 --- a/packages/cli/src/ui/constants/phrasesCollections.ts +++ b/packages/cli/src/ui/constants/phrasesCollections.ts @@ -232,13 +232,13 @@ export function getPhraseCollection( case 'whimsical': return COMMUNITY_PHRASES; case 'custom': - return customPhrases && customPhrases.length > 0 + return customPhrases != null && customPhrases.length > 0 ? customPhrases : LLXPRT_PHRASES; // Fallback to built-in if custom is empty case 'default': default: // Default: LLxprt + custom override (current behavior) - return customPhrases && customPhrases.length > 0 + return customPhrases != null && customPhrases.length > 0 ? customPhrases : LLXPRT_PHRASES; } diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts index d38fe1d438..ac50441742 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useAppBootstrap.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type React from 'react'; import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { useStdin, useStdout } from 'ink'; import { useResponsive } from '../../../hooks/useResponsive.js'; @@ -18,19 +19,19 @@ import { import { useHistory } from '../../../hooks/useHistoryManager.js'; import { useMemoryMonitor } from '../../../hooks/useMemoryMonitor.js'; import { TodoPausePreserver } from '../../../hooks/useTodoPausePreserver.js'; -import { - type Config, - type IContent, - type IdeInfo, - type MessageBus, - type RecordingIntegration, - type SessionRecordingService, - type LockHandle, +import type { + Config, + IContent, + IdeInfo, + MessageBus, + RecordingIntegration, + SessionRecordingService, + LockHandle, } from '@vybestack/llxprt-code-core'; import { useSessionStats } from '../../../contexts/SessionContext.js'; import { useFocus } from '../../../hooks/useFocus.js'; import type { AppState, AppAction } from '../../../reducers/appReducer.js'; -import { UpdateObject } from '../../../utils/updateCheck.js'; +import type { UpdateObject } from '../../../utils/updateCheck.js'; import { useRuntimeApi } from '../../../contexts/RuntimeContext.js'; import { useTodoContext } from '../../../contexts/TodoContext.js'; import { useRecordingInfrastructure } from './useRecordingInfrastructure.js'; @@ -41,7 +42,6 @@ import { registerCleanup } from '../../../../utils/cleanup.js'; import type { LoadedSettings } from '../../../../config/settings.js'; import type { HistoryItem } from '../../../types.js'; import type { TodoContinuationHook } from './useTodoContinuationFlow.js'; -import React from 'react'; export interface AppBootstrapProps { config: Config; @@ -218,7 +218,7 @@ function useBootstrapEvents( const currentIDE = config.getIdeClient()?.getCurrentIde(); useEffect(() => { const ideClient = config.getIdeClient(); - if (ideClient) { + if (ideClient != null) { registerCleanup(() => ideClient.disconnect()); } }, [config]); diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useAppDialogs.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useAppDialogs.ts index c89df381f5..857ec64309 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useAppDialogs.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useAppDialogs.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type React from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useThemeCommand } from '../../../hooks/useThemeCommand.js'; import { useAuthCommand } from '../../../hooks/useAuthCommand.js'; @@ -37,7 +38,6 @@ import type { import type { LoadedSettings } from '../../../../config/settings.js'; import type { AppState, AppAction } from '../../../reducers/appReducer.js'; import type { HistoryItem, ConsoleMessageItem } from '../../../types.js'; -import React from 'react'; const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000; @@ -234,7 +234,6 @@ function useDialogsAuthProviders( isAuthDialogOpen: auth.isAuthDialogOpen, openAuthDialog: auth.openAuthDialog, handleAuthSelect: auth.handleAuthSelect, - isOAuthCodeDialogOpen, isEditorDialogOpen: editor.isEditorDialogOpen, openEditorDialog: editor.openEditorDialog, handleEditorSelect: editor.handleEditorSelect, @@ -245,6 +244,7 @@ function useDialogsAuthProviders( exitProviderDialog: provider.closeDialog, providerOptions: provider.providers, selectedProvider: provider.currentProvider, + isOAuthCodeDialogOpen, }; } @@ -364,12 +364,12 @@ function useDialogsProfiles(p: AppDialogsParams) { closeProfileEditor: profileMgmt.closeEditor, saveProfileFromEditor: profileMgmt.saveProfile, isToolsDialogOpen: toolsRaw.showDialog, - openToolsDialog, exitToolsDialog: toolsRaw.closeDialog, toolsDialogAction: toolsRaw.action, toolsDialogTools: toolsRaw.availableTools, toolsDialogDisabledTools: toolsRaw.disabledTools, handleToolsSelect: toolsRaw.handleSelect, + openToolsDialog, performMemoryRefresh, useAlternateBuffer, }; diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.ts index 72cbccf18a..6dee6afcd9 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useAppInput.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type React from 'react'; import { useCallback, useMemo, useRef } from 'react'; import { useGeminiStream } from '../../../hooks/geminiStream/index.js'; import { useAutoAcceptIndicator } from '../../../hooks/useAutoAcceptIndicator.js'; @@ -19,16 +20,18 @@ import { useInputHistoryStore } from '../../../hooks/useInputHistoryStore.js'; import { useTodoPausePreserver } from '../../../hooks/useTodoPausePreserver.js'; import { StreamingState, type HistoryItem } from '../../../types.js'; import { submitOAuthCode } from '../../../oauth-submission.js'; -import { isEditorAvailable, EditorType } from '@vybestack/llxprt-code-core'; +import { + isEditorAvailable, + type EditorType, +} from '@vybestack/llxprt-code-core'; import { SettingScope } from '../../../../config/settings.js'; import type { AppState, AppAction } from '../../../reducers/appReducer.js'; -import { IdeIntegrationNudgeResult } from '../../../IdeIntegrationNudge.js'; +import type { IdeIntegrationNudgeResult } from '../../../IdeIntegrationNudge.js'; import { useSlashCommandActions } from './useSlashCommandActions.js'; import { useExitHandling } from './useExitHandling.js'; import { useInputHandling } from './useInputHandling.js'; import { useShellFocusAutoReset } from './useShellFocusAutoReset.js'; import * as fs from 'fs'; -import React from 'react'; import type { AppBootstrapResult } from './useAppBootstrap.js'; import type { AppDialogsResult } from './useAppDialogs.js'; @@ -155,7 +158,6 @@ function useSlashActions( openProfileListDialog: p.openProfileListDialog, viewProfileDetail: p.viewProfileDetail, openProfileEditor: p.openProfileEditor, - quitHandler, setDebugMessage: p.setDebugMessage, toggleCorgiMode: p.toggleCorgiMode, toggleDebugProfiler: p.toggleDebugProfiler, @@ -163,6 +165,7 @@ function useSlashActions( addConfirmUpdateExtensionRequest: p.addConfirmUpdateExtensionRequest, welcomeActions: p.welcomeActions as { resetAndReopen: () => void }, openSessionBrowserDialog: p.openSessionBrowserDialog, + quitHandler, }); } @@ -220,7 +223,7 @@ function useInputCoreProcessors(p: AppInputParams) { ((messages: HistoryItem[]) => void) | null >(null); const quitHandler = useCallback((messages: HistoryItem[]) => { - if (setQuittingMessagesRef.current) + if (setQuittingMessagesRef.current != null) setQuittingMessagesRef.current(messages); }, []); const slashResult = useSlashCommandSetup(p, quitHandler, toggleVimEnabled); @@ -257,10 +260,10 @@ function useInputBuffer( ); const buffer = useTextBuffer({ initialText: '', + isValidPath: core.isValidPath, viewport, stdin, setRawMode, - isValidPath: core.isValidPath, shellModeActive, }); const inputHistoryStore = useInputHistoryStore(); @@ -284,7 +287,7 @@ function useInputBuffer( ); const handleUserCancel = useCallback( (shouldRestorePrompt?: boolean) => { - if (shouldRestorePrompt) { + if (shouldRestorePrompt === true) { const last = lastSubmittedPromptRef.current; if (last) buffer.setText(last); } else buffer.setText(''); @@ -341,8 +344,8 @@ function useInputStreamSetup( refreshStatic, handleUserCancel, setEmbeddedShellFocused, - stdout?.columns, - stdout?.rows, + stdout.columns, + stdout.rows, registerTodoPause, handleExternalEditorOpen, recordingIntegration, @@ -484,7 +487,7 @@ function useInputFinish( settings.merged.wittyPhraseStyle ?? 'default', settings.merged.ui.customWittyPhrases ?? settings.merged.customWittyPhrases, - !!stream.activeShellPtyId && !p.embeddedShellFocused, + stream.activeShellPtyId != null && !p.embeddedShellFocused, stream.lastOutputTime, ); const showAutoAcceptIndicator = useAutoAcceptIndicator({ diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useAppLayout.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useAppLayout.ts index e85e7f4fb7..04733bffb8 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useAppLayout.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useAppLayout.ts @@ -190,10 +190,10 @@ function useLayoutMeasure(p: AppLayoutParams) { useLayoutMeasurement({ enabled: true, copyShortcutEnabled: p.copyModeEnabled, + showErrorDetails: p.showErrorDetails, setFooterHeight, terminalHeight, consoleMessages, - showErrorDetails: p.showErrorDetails, }); const staticExtraHeight = 3; const availableTerminalHeight = useMemo( @@ -246,7 +246,7 @@ function useLayoutContext(p: AppLayoutParams) { const branchName = useGitBranchName(config.getTargetDir()); const contextFileNames = useMemo(() => { const fromSettings = settings.merged.ui.contextFileName; - if (fromSettings) + if (fromSettings != null) return Array.isArray(fromSettings) ? fromSettings : [fromSettings]; return getAllLlxprtMdFilenames(); }, [settings.merged.ui.contextFileName]); @@ -254,7 +254,7 @@ function useLayoutContext(p: AppLayoutParams) { useInitialPromptSubmit({ initialPrompt, submitQuery, - geminiClientPresent: !!config.getGeminiClient(), + geminiClientPresent: config.getGeminiClient() != null, blockedByDialogs: { isAuthDialogOpen, isThemeDialogOpen, diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useConfirmationSelection.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useConfirmationSelection.ts index c83bb4e2da..a89a3baf8d 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useConfirmationSelection.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useConfirmationSelection.ts @@ -28,7 +28,7 @@ export function useConfirmationSelection({ }: UseConfirmationSelectionParams): (value: boolean) => void { return useCallback( (value: boolean) => { - if (!confirmationRequest) { + if (confirmationRequest == null) { return; } diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useDialogOrchestration.test.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useDialogOrchestration.test.ts index 9c49603b87..d7b8724393 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useDialogOrchestration.test.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useDialogOrchestration.test.ts @@ -37,7 +37,9 @@ describe('useDialogOrchestration', () => { }); expect(result.current.isLoggingDialogOpen).toBe(true); - expect(result.current.loggingDialogData).toEqual({ entries: ['a', 'b'] }); + expect(result.current.loggingDialogData).toStrictEqual({ + entries: ['a', 'b'], + }); act(() => { result.current.closeLoggingDialog(); @@ -49,7 +51,7 @@ describe('useDialogOrchestration', () => { result.current.openLoggingDialog(); }); - expect(result.current.loggingDialogData).toEqual({ entries: [] }); + expect(result.current.loggingDialogData).toStrictEqual({ entries: [] }); }); it('resets subagent initial state when closing the subagent dialog', () => { @@ -83,7 +85,7 @@ describe('useDialogOrchestration', () => { }); expect(result.current.isModelsDialogOpen).toBe(true); - expect(result.current.modelsDialogData).toEqual({ + expect(result.current.modelsDialogData).toStrictEqual({ initialSearch: 'claude', includeDeprecated: true, }); diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useDialogOrchestration.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useDialogOrchestration.ts index c42ec14457..f568bd94ed 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useDialogOrchestration.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useDialogOrchestration.ts @@ -6,7 +6,7 @@ import { useCallback, useRef, useState } from 'react'; import type { ModelsDialogData } from '../../../commands/types.js'; -import { SubagentView } from '../../../components/SubagentManagement/types.js'; +import type { SubagentView } from '../../../components/SubagentManagement/types.js'; /** * @hook useDialogOrchestration diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useExitHandling.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useExitHandling.ts index 435f929806..68eff08105 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useExitHandling.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useExitHandling.ts @@ -62,7 +62,7 @@ function useQuitEffect( config: Config, ): void { useEffect(() => { - if (quittingMessages) { + if (quittingMessages != null) { // Allow UI to render the quit message briefly before exiting const timer = setTimeout(() => { // Fire SessionEnd hook before exiting @@ -109,7 +109,7 @@ export function useExitHandling({ timerRef: React.MutableRefObject, ) => { if (pressedOnce) { - if (timerRef.current) { + if (timerRef.current != null) { clearTimeout(timerRef.current); } // Directly invoke the central command handler. @@ -150,10 +150,10 @@ export function useExitHandling({ () => () => { const ctrlCTimer = ctrlCTimerRef.current; const ctrlDTimer = ctrlDTimerRef.current; - if (ctrlCTimer) { + if (ctrlCTimer != null) { clearTimeout(ctrlCTimer); } - if (ctrlDTimer) { + if (ctrlDTimer != null) { clearTimeout(ctrlDTimer); } }, diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useInputHandling.test.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useInputHandling.test.ts index c43c3ce682..dfb53867e3 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useInputHandling.test.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useInputHandling.test.ts @@ -35,17 +35,17 @@ const createHarness = ( return { buffer: { setText: bufferSetText } as unknown as TextBuffer, - bufferSetText, - addInput, - submitQuery, pendingHistoryItems: [], lastSubmittedPromptRef: { current: 'last submitted prompt' }, hadToolCallsRef: { current: true }, - clearPause, todoContinuationRef: { current: { clearPause } }, isMcpReady: true, addMessage: vi.fn(), ...overrides, + bufferSetText, + addInput, + submitQuery, + clearPause, }; }; diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useKeybindings.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useKeybindings.ts index 7f10789697..7941975e1b 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useKeybindings.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useKeybindings.ts @@ -203,7 +203,6 @@ function handleDisplayKeys( !enteringConstrainHeightMode ) { display.setConstrainHeight(false); - return; } } diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useLayoutMeasurement.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useLayoutMeasurement.ts index f7b4ab1387..134b48b467 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useLayoutMeasurement.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useLayoutMeasurement.ts @@ -7,7 +7,7 @@ import { type DOMElement, measureElement } from 'ink'; import { useLayoutEffect, useRef } from 'react'; import { useMouseSelection } from '../../../hooks/useMouseSelection.js'; -import { useKeypress, Key } from '../../../hooks/useKeypress.js'; +import { useKeypress, type Key } from '../../../hooks/useKeypress.js'; import { DebugLogger } from '@vybestack/llxprt-code-core'; import type { ConsoleMessageItem } from '../../../types.js'; @@ -80,7 +80,7 @@ export function useLayoutMeasurement({ ); useLayoutEffect(() => { - if (mainControlsRef.current) { + if (mainControlsRef.current != null) { const fullFooterMeasurement = measureElement(mainControlsRef.current); setFooterHeight(fullFooterMeasurement.height); } diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useModelRuntimeSync.test.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useModelRuntimeSync.test.ts index 8d3a556c74..bf9e3f1ccb 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useModelRuntimeSync.test.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useModelRuntimeSync.test.ts @@ -34,9 +34,9 @@ describe('useModelRuntimeSync', () => { useModelRuntimeSync({ config: config as never, currentModel: 'stale-model', + contextLimit: undefined, setCurrentModel, getActiveModelName, - contextLimit: undefined, setContextLimit, }), ); @@ -60,9 +60,9 @@ describe('useModelRuntimeSync', () => { useModelRuntimeSync({ config: config as never, currentModel: 'old-model', + contextLimit: undefined, setCurrentModel, getActiveModelName, - contextLimit: undefined, setContextLimit, }), ); @@ -86,9 +86,9 @@ describe('useModelRuntimeSync', () => { useModelRuntimeSync({ config: config as never, currentModel: 'provider-model', + contextLimit: undefined, setCurrentModel, getActiveModelName, - contextLimit: undefined, setContextLimit, }), ); @@ -116,9 +116,9 @@ describe('useModelRuntimeSync', () => { useModelRuntimeSync({ config: config as never, currentModel: 'none', + contextLimit: undefined, setCurrentModel, getActiveModelName, - contextLimit: undefined, setContextLimit, }), ); @@ -156,9 +156,9 @@ describe('useModelRuntimeSync', () => { useModelRuntimeSync({ config: config as never, currentModel: 'same-model', + contextLimit: undefined, setCurrentModel, getActiveModelName, - contextLimit: undefined, setContextLimit, }), ); @@ -186,9 +186,9 @@ describe('useModelRuntimeSync', () => { useModelRuntimeSync({ config: config as never, currentModel: 'same-model', + contextLimit: 200000, setCurrentModel, getActiveModelName, - contextLimit: 200000, setContextLimit, }), ); @@ -223,9 +223,9 @@ describe('useModelRuntimeSync', () => { useModelRuntimeSync({ config: config as never, currentModel: 'old-model', + contextLimit: undefined, setCurrentModel, getActiveModelName, - contextLimit: undefined, setContextLimit, }), ); diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useModelTracking.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useModelTracking.ts index be9ca6194d..113e285368 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useModelTracking.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useModelTracking.ts @@ -42,11 +42,11 @@ export function useModelTracking({ const settingsService = getSettingsService(); // Try to get from SettingsService first (same as diagnostics does) - if (settingsService && settingsService.getDiagnosticsData) { + if (settingsService?.getDiagnosticsData) { try { const diagnosticsData = await settingsService.getDiagnosticsData(); if (!disposed && seq === requestSeq) { - if (diagnosticsData && diagnosticsData.model) { + if (diagnosticsData?.model) { setCurrentModel(diagnosticsData.model); return; } diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useRecordingInfrastructure.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useRecordingInfrastructure.ts index 7127be9615..9853f28a51 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useRecordingInfrastructure.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useRecordingInfrastructure.ts @@ -9,9 +9,9 @@ import type { RecordingIntegration, SessionRecordingService, LockHandle, + SessionMetadata, } from '@vybestack/llxprt-code-core'; import type { RecordingSwapCallbacks } from '../../../../services/performResume.js'; -import type { SessionMetadata } from '@vybestack/llxprt-code-core'; /** * @hook useRecordingInfrastructure diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useSessionInitialization.test.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useSessionInitialization.test.ts index bf48d951c2..ce082923c7 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useSessionInitialization.test.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useSessionInitialization.test.ts @@ -109,9 +109,9 @@ describe('useSessionInitialization', () => { renderHook(() => useSessionInitialization({ config: config as never, + resumedHistory: [], addItem, loadHistory, - resumedHistory: [], }), ); diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useSessionInitialization.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useSessionInitialization.ts index 6c58eb7516..69fae8a6f3 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useSessionInitialization.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useSessionInitialization.ts @@ -64,7 +64,7 @@ async function runSessionStartHook( return; } - if (sessionStartOutput) { + if (sessionStartOutput != null) { if (sessionStartOutput.systemMessage) { addItem( { @@ -109,7 +109,7 @@ export function useSessionInitialization({ // This effect is idempotent: resumedHistory is a static prop from mount, // loadHistory replaces (not appends), and StrictMode double-mount is harmless. useEffect(() => { - if (!resumedHistory || resumedHistory.length === 0) { + if (resumedHistory == null || resumedHistory.length === 0) { return; } const uiItems = iContentToHistoryItems(resumedHistory); @@ -133,7 +133,7 @@ export function useSessionInitialization({ }); return () => { - if (abortControllerRef.current) { + if (abortControllerRef.current != null) { abortControllerRef.current.abort(); } }; diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useSlashCommandActions.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useSlashCommandActions.ts index a9248db535..c25d321836 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useSlashCommandActions.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useSlashCommandActions.ts @@ -78,6 +78,8 @@ export function useSlashCommandActions({ }: UseSlashCommandActionsParams) { return useMemo( () => ({ + quit: quitHandler, + openWelcomeDialog: welcomeActions.resetAndReopen, openAuthDialog, openThemeDialog, openEditorDialog, @@ -93,13 +95,11 @@ export function useSlashCommandActions({ openProfileListDialog, viewProfileDetail, openProfileEditor, - quit: quitHandler, setDebugMessage, toggleCorgiMode, toggleDebugProfiler, dispatchExtensionStateUpdate, addConfirmUpdateExtensionRequest, - openWelcomeDialog: welcomeActions.resetAndReopen, openSessionBrowserDialog, }), [ diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useStaticRefreshManager.test.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useStaticRefreshManager.test.ts index d839f16743..e9bafd4e83 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useStaticRefreshManager.test.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useStaticRefreshManager.test.ts @@ -126,8 +126,8 @@ describe('useStaticRefreshManager', () => { streamingState: StreamingState.Idle, terminalWidth: 100, terminalHeight: 30, - refreshStatic, constrainHeight: false, + refreshStatic, setConstrainHeight, }), ); @@ -152,8 +152,8 @@ describe('useStaticRefreshManager', () => { streamingState: StreamingState.Idle, terminalWidth: 100, terminalHeight: 30, - refreshStatic, constrainHeight: true, + refreshStatic, setConstrainHeight, }), ); diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts index 7ba20c02c8..717f6360bc 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useTodoContinuationFlow.ts @@ -21,7 +21,7 @@ import { type HistoryItem, type HistoryItemWithoutId, } from '../../../types.js'; -import { type Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; import { useTodoContinuation, type TodoContinuationHook, diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useTokenMetricsTracking.test.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useTokenMetricsTracking.test.ts index e1e1c16b1e..416514743d 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useTokenMetricsTracking.test.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useTokenMetricsTracking.test.ts @@ -134,12 +134,12 @@ describe('useTokenMetricsTracking', () => { const { result } = renderHook(() => useTokenMetricsTracking({ config: config as never, - updateHistoryTokenCount, recordingIntegrationRef: recordingIntegrationRef as never, + updateHistoryTokenCount, }), ); - expect(result.current.tokenMetrics).toEqual({ + expect(result.current.tokenMetrics).toStrictEqual({ tokensPerMinute: 42, throttleWaitTimeMs: 75, sessionTokenTotal: 15, @@ -174,8 +174,8 @@ describe('useTokenMetricsTracking', () => { renderHook(() => useTokenMetricsTracking({ config: config as never, - updateHistoryTokenCount, recordingIntegrationRef: recordingIntegrationRef as never, + updateHistoryTokenCount, }), ); @@ -226,8 +226,8 @@ describe('useTokenMetricsTracking', () => { const { unmount } = renderHook(() => useTokenMetricsTracking({ config: config as never, - updateHistoryTokenCount, recordingIntegrationRef: recordingIntegrationRef as never, + updateHistoryTokenCount, }), ); @@ -280,8 +280,8 @@ describe('useTokenMetricsTracking', () => { renderHook(() => useTokenMetricsTracking({ config: config as never, - updateHistoryTokenCount, recordingIntegrationRef: recordingIntegrationRef as never, + updateHistoryTokenCount, }), ); @@ -322,8 +322,8 @@ describe('useTokenMetricsTracking', () => { renderHook(() => useTokenMetricsTracking({ config: config as never, - updateHistoryTokenCount, recordingIntegrationRef: recordingIntegrationRef as never, + updateHistoryTokenCount, }), ); diff --git a/packages/cli/src/ui/containers/AppContainer/hooks/useTokenMetricsTracking.ts b/packages/cli/src/ui/containers/AppContainer/hooks/useTokenMetricsTracking.ts index 08fa2212ec..5f491605b2 100644 --- a/packages/cli/src/ui/containers/AppContainer/hooks/useTokenMetricsTracking.ts +++ b/packages/cli/src/ui/containers/AppContainer/hooks/useTokenMetricsTracking.ts @@ -71,13 +71,13 @@ function useHistoryTokenListener( if (historyService !== lastHistoryServiceRef.current) { // Clean up listener from the old service regardless of whether the // new service is truthy — a reset may have cleared the service. - if (historyTokenCleanupRef.current) { + if (historyTokenCleanupRef.current != null) { historyTokenCleanupRef.current(); historyTokenCleanupRef.current = null; } lastHistoryServiceRef.current = historyService ?? null; - if (historyService) { + if (historyService != null) { tokenLogger.debug( () => 'Found new history service, setting up listener', ); @@ -113,7 +113,7 @@ function useHistoryTokenListener( return () => { clearInterval(checkInterval); intervalCleared = true; - if (historyTokenCleanupRef.current) { + if (historyTokenCleanupRef.current != null) { historyTokenCleanupRef.current(); historyTokenCleanupRef.current = null; } @@ -145,7 +145,7 @@ function useRecordingSubscription( // recording integrations. The polling interval below handles this: each tick checks // recordingIntegrationRef.current?.onHistoryServiceReplaced, so a recording // integration that arrives after mount is automatically picked up on the next tick. - if (!recordingIntegrationRef.current) return; + if (recordingIntegrationRef.current == null) return; let intervalCleared = false; const checkInterval = setInterval(() => { @@ -155,7 +155,7 @@ function useRecordingSubscription( if (geminiClient?.hasChatInitialized?.()) { const historyService = geminiClient.getHistoryService?.(); if ( - historyService && + historyService != null && historyService !== recordingSubscribedServiceRef.current ) { recordingSubscribedServiceRef.current = historyService; diff --git a/packages/cli/src/ui/containers/SessionController.test.tsx b/packages/cli/src/ui/containers/SessionController.test.tsx index ebf0128631..d7ecae4923 100644 --- a/packages/cli/src/ui/containers/SessionController.test.tsx +++ b/packages/cli/src/ui/containers/SessionController.test.tsx @@ -28,7 +28,7 @@ import { type SessionContextType, } from './SessionController.js'; import { MessageType } from '../types.js'; -import { Config, IProvider } from '@vybestack/llxprt-code-core'; +import type { Config, IProvider } from '@vybestack/llxprt-code-core'; // import { AppAction } from '../reducers/appReducer.js'; import { useHistory } from '../hooks/useHistoryManager.js'; @@ -140,7 +140,7 @@ describe('SessionController', () => { ); expect(contextValue).toBeDefined(); - expect(contextValue!.history).toEqual([]); + expect(contextValue!.history).toStrictEqual([]); expect(typeof contextValue!.addItem).toBe('function'); expect(typeof contextValue!.updateItem).toBe('function'); expect(typeof contextValue!.clearItems).toBe('function'); @@ -163,7 +163,9 @@ describe('SessionController', () => { return React.createElement( Text, null, - contextValue?.appDispatch ? 'Dispatch available' : 'No dispatch', + contextValue?.appDispatch != null + ? 'Dispatch available' + : 'No dispatch', ); }; diff --git a/packages/cli/src/ui/containers/SessionController.tsx b/packages/cli/src/ui/containers/SessionController.tsx index 4270516774..56291205d2 100644 --- a/packages/cli/src/ui/containers/SessionController.tsx +++ b/packages/cli/src/ui/containers/SessionController.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { +import type React from 'react'; +import { createContext, useMemo, useCallback, @@ -12,11 +13,11 @@ import React, { useRef, useReducer, } from 'react'; -import { HistoryItem, MessageType } from '../types.js'; +import { type HistoryItem, MessageType } from '../types.js'; import { useHistory } from '../hooks/useHistoryManager.js'; import { useRuntimeApi, getRuntimeApi } from '../contexts/RuntimeContext.js'; import { - Config, + type Config, getErrorMessage, loadCoreMemoryContent, debugLogger, @@ -27,13 +28,16 @@ import { SessionStateProvider, useSessionState, } from '../contexts/SessionStateContext.js'; -import { SessionState, SessionAction } from '../reducers/sessionReducer.js'; +import type { + SessionState, + SessionAction, +} from '../reducers/sessionReducer.js'; import { AppDispatchProvider } from '../contexts/AppDispatchContext.js'; import { appReducer, initialAppState, - AppAction, - AppState, + type AppAction, + type AppState, } from '../reducers/appReducer.js'; // Context type @@ -158,7 +162,7 @@ const SessionControllerInner: React.FC = ({ } } - if (warningTimerRef.current) { + if (warningTimerRef.current != null) { clearTimeout(warningTimerRef.current); } @@ -278,7 +282,7 @@ const SessionControllerInner: React.FC = ({ } } - if (warningTimerRef.current) { + if (warningTimerRef.current != null) { clearTimeout(warningTimerRef.current); } @@ -295,7 +299,7 @@ const SessionControllerInner: React.FC = ({ return () => { clearInterval(interval); - if (warningTimerRef.current) { + if (warningTimerRef.current != null) { clearTimeout(warningTimerRef.current); } }; @@ -311,7 +315,7 @@ const SessionControllerInner: React.FC = ({ // Handle ADD_ITEM actions useEffect(() => { - if (appState.lastAddItemAction) { + if (appState.lastAddItemAction != null) { const { itemData, baseTimestamp } = appState.lastAddItemAction; addItem(itemData, baseTimestamp); } diff --git a/packages/cli/src/ui/containers/UIStateShell.tsx b/packages/cli/src/ui/containers/UIStateShell.tsx index 650e254c9b..6a7ec18402 100644 --- a/packages/cli/src/ui/containers/UIStateShell.tsx +++ b/packages/cli/src/ui/containers/UIStateShell.tsx @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import type React from 'react'; import { LayoutManager } from '../components/LayoutManager.js'; interface UIStateShellProps { diff --git a/packages/cli/src/ui/contexts/AppDispatchContext.tsx b/packages/cli/src/ui/contexts/AppDispatchContext.tsx index 98c147c982..c560a97a43 100644 --- a/packages/cli/src/ui/contexts/AppDispatchContext.tsx +++ b/packages/cli/src/ui/contexts/AppDispatchContext.tsx @@ -4,8 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { createContext, useContext } from 'react'; -import { AppAction } from '../reducers/appReducer.js'; +import type React from 'react'; +import { createContext, useContext } from 'react'; +import type { AppAction } from '../reducers/appReducer.js'; const AppDispatchContext = createContext | undefined>( undefined, @@ -22,7 +23,7 @@ export const AppDispatchProvider: React.FC<{ export const useAppDispatch = (): React.Dispatch => { const dispatch = useContext(AppDispatchContext); - if (!dispatch) { + if (dispatch == null) { throw new Error('useAppDispatch must be used within AppDispatchProvider'); } return dispatch; diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx index cdb3b49511..0a28459072 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx @@ -727,23 +727,22 @@ describe('Kitty Sequence Parsing', () => { shift: false, }, }; - } else { - // iTerm2 and VSCode send accented characters (å, ø, µ) - // Note: µ (mu) is sent with meta:false on iTerm2/VSCode but - // gets converted to m with meta:true - return { - terminal, - key, - chunk: accentedChar, - expected: { - name: key, - ctrl: false, - meta: true, // Always expect meta:true after conversion - shift: false, - sequence: accentedChar, - }, - }; } + // iTerm2 and VSCode send accented characters (å, ø, µ) + // Note: µ (mu) is sent with meta:false on iTerm2/VSCode but + // gets converted to m with meta:true + return { + terminal, + key, + chunk: accentedChar, + expected: { + name: key, + ctrl: false, + meta: true, // Always expect meta:true after conversion + shift: false, + sequence: accentedChar, + }, + }; }), ), )( diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index be7d77525a..ed1e0120f4 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -189,7 +189,7 @@ function bufferBackslashEnter( keypressHandler: KeypressHandler, ): KeypressHandler { const bufferer = (function* (): Generator { - while (true) { + for (;;) { const key = yield; if (key == null) { @@ -235,7 +235,7 @@ function bufferBackslashEnter( */ function bufferPaste(keypressHandler: KeypressHandler): KeypressHandler { const bufferer = (function* (): Generator { - while (true) { + for (;;) { let key = yield; if (key === null) { @@ -246,7 +246,7 @@ function bufferPaste(keypressHandler: KeypressHandler): KeypressHandler { } let buffer = ''; - while (true) { + for (;;) { const timeoutId = setTimeout(() => bufferer.next(null), PASTE_TIMEOUT); key = yield; clearTimeout(timeoutId); @@ -309,7 +309,7 @@ function createDataListener(keypressHandler: KeypressHandler) { function* emitKeys( keypressHandler: KeypressHandler, ): Generator { - while (true) { + for (;;) { let ch = yield; let sequence = ch; let escaped = false; @@ -344,7 +344,7 @@ function* emitKeys( let buffer = ''; // Read until BEL, `ESC \`, or timeout (empty string) - while (true) { + for (;;) { const next = yield; if (next === '' || next === '\u0007') { break; @@ -393,7 +393,7 @@ function* emitKeys( } code += ch; - } else if (ch === '[') { + } else { // ESC [ letter // ESC [ modifier letter // ESC [ [ modifier letter @@ -483,10 +483,10 @@ function* emitKeys( * and modifier from it */ const cmd = sequence.slice(cmdStart); - let match; + let match: RegExpExecArray | null; if ((match = /^(\d+)(?:;(\d+))?(?:;(\d+))?([~^$u])$/.exec(cmd))) { - if (match[1] === '27' && match[3] && match[4] === '~') { + if (match[1] === '27' && match[3] != null && match[4] === '~') { // modifyOtherKeys format: CSI 27 ; modifier ; key ~ // Treat as CSI u: key + 'u' code += match[3] + 'u'; @@ -498,24 +498,30 @@ function* emitKeys( } } else if ((match = /^(\d+)?(?:;(\d+))?([A-Za-z])$/.exec(cmd))) { code += match[3]; - modifier = parseInt(match[2] ?? match[1] ?? '1', 10) - 1; + modifier = + parseInt( + (match[2] as string | undefined) ?? + (match[1] as string | undefined) ?? + '1', + 10, + ) - 1; } else { code += cmd; } } // Parse the key modifier - shift = !!(modifier & 1); - meta = !!(modifier & 2); - ctrl = !!(modifier & 4); + shift = (modifier & 1) !== 0; + meta = (modifier & 2) !== 0; + ctrl = (modifier & 4) !== 0; const keyInfo = KEY_INFO_MAP[code]; - if (keyInfo) { + if (keyInfo != null) { name = keyInfo.name; - if (keyInfo.shift) { + if (keyInfo.shift === true) { shift = true; } - if (keyInfo.ctrl) { + if (keyInfo.ctrl === true) { ctrl = true; } if (name === 'space' && !ctrl && !meta) { @@ -587,15 +593,15 @@ function* emitKeys( // Emit first escape key here, then continue processing keypressHandler({ name: 'escape', + sequence: ESC, + insertable: false, shift, meta, ctrl, - sequence: ESC, - insertable: false, }); } else if (escaped) { // Escape sequence timeout - name = ch.length ? undefined : 'escape'; + name = ch.length > 0 ? undefined : 'escape'; meta = true; } else { // Any other character is considered printable. @@ -607,7 +613,7 @@ function* emitKeys( charLengthAt(sequence, 0) === sequence.length ) { keypressHandler({ - name: name || '', + name: name ?? '', shift, meta, ctrl, diff --git a/packages/cli/src/ui/contexts/MouseContext.tsx b/packages/cli/src/ui/contexts/MouseContext.tsx index 3a89251c80..f28322426d 100644 --- a/packages/cli/src/ui/contexts/MouseContext.tsx +++ b/packages/cli/src/ui/contexts/MouseContext.tsx @@ -36,7 +36,7 @@ const MouseContext = createContext(undefined); export function useMouseContext() { const context = useContext(MouseContext); - if (!context) { + if (context == null) { throw new Error('useMouseContext must be used within a MouseProvider'); } return context; @@ -103,7 +103,7 @@ export function MouseProvider({ while (mouseBuffer.length > 0) { const parsed = parseMouseEvent(mouseBuffer); - if (parsed) { + if (parsed != null) { broadcast(parsed.event); mouseBuffer = mouseBuffer.slice(parsed.length); continue; diff --git a/packages/cli/src/ui/contexts/OpenAIProviderContext.tsx b/packages/cli/src/ui/contexts/OpenAIProviderContext.tsx index b6b2fefbac..d4711270ea 100644 --- a/packages/cli/src/ui/contexts/OpenAIProviderContext.tsx +++ b/packages/cli/src/ui/contexts/OpenAIProviderContext.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { +import type React from 'react'; +import { createContext, useContext, useState, @@ -12,7 +13,7 @@ import React, { useEffect, useMemo, } from 'react'; -import { +import type { Config, ProviderMessage as Message, ConversationCache, @@ -71,7 +72,7 @@ export const OpenAIProviderContextProvider: React.FC<{ // Reset stats when provider changes useEffect(() => { - if (!providerInfo.provider) { + if (providerInfo.provider == null) { setRemoteTokenStats({ promptTokenCount: 0, candidatesTokenCount: 0, @@ -87,9 +88,9 @@ export const OpenAIProviderContextProvider: React.FC<{ isResponsesAPI: providerInfo.isResponsesAPI, currentModel: providerInfo.currentModel, conversationCache: providerInfo.conversationCache, + getCachedConversation: providerInfo.getCachedConversation, remoteTokenStats, updateRemoteTokenStats, - getCachedConversation: providerInfo.getCachedConversation, }), [ providerInfo.provider, diff --git a/packages/cli/src/ui/contexts/OverflowContext.tsx b/packages/cli/src/ui/contexts/OverflowContext.tsx index d640ed04a0..428f9180a9 100644 --- a/packages/cli/src/ui/contexts/OverflowContext.tsx +++ b/packages/cli/src/ui/contexts/OverflowContext.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { +import type React from 'react'; +import { createContext, useContext, useState, diff --git a/packages/cli/src/ui/contexts/RuntimeContext.tsx b/packages/cli/src/ui/contexts/RuntimeContext.tsx index caef8f4610..6f5ac45cdf 100644 --- a/packages/cli/src/ui/contexts/RuntimeContext.tsx +++ b/packages/cli/src/ui/contexts/RuntimeContext.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { +import type React from 'react'; +import { createContext, type PropsWithChildren, useContext, @@ -177,7 +178,7 @@ export const RuntimeContextProvider: React.FC> = ({ export function useRuntimeBridge(): RuntimeContextBridge { const context = useContext(RuntimeContext); - if (!context) { + if (context == null) { throw new Error( 'RuntimeContextProvider is missing from the component tree.', ); @@ -190,7 +191,7 @@ export function useRuntimeApi(): RuntimeApi { } export function getRuntimeBridge(): RuntimeContextBridge { - if (latestBridge) { + if (latestBridge != null) { return latestBridge; } diff --git a/packages/cli/src/ui/contexts/ScrollProvider.tsx b/packages/cli/src/ui/contexts/ScrollProvider.tsx index dbadc545cd..0249931a6a 100644 --- a/packages/cli/src/ui/contexts/ScrollProvider.tsx +++ b/packages/cli/src/ui/contexts/ScrollProvider.tsx @@ -49,7 +49,7 @@ const findScrollableCandidates = ( const candidates: Array = []; for (const entry of scrollables.values()) { - if (!entry.ref.current || !entry.hasFocus()) { + if (entry.ref.current == null || !entry.hasFocus()) { continue; } @@ -129,7 +129,7 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({ flushScheduledRef.current = false; for (const [id, delta] of pendingScrollsRef.current.entries()) { const entry = scrollablesRef.current.get(id); - if (entry) { + if (entry != null) { entry.scrollBy(delta); } } @@ -175,7 +175,7 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({ const handleLeftPress = useCallback((mouseEvent: MouseEvent) => { for (const entry of scrollablesRef.current.values()) { - if (!entry.ref.current || !entry.hasFocus()) { + if (entry.ref.current == null || !entry.hasFocus()) { continue; } @@ -234,7 +234,7 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({ const newScrollTop = Math.round( (targetThumbY / maxThumbY) * maxScrollTop, ); - if (entry.scrollTo) { + if (entry.scrollTo != null) { entry.scrollTo(newScrollTop); } else { entry.scrollBy(newScrollTop - scrollTop); @@ -269,7 +269,7 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({ if (!state.active || !state.id) return false; const entry = scrollablesRef.current.get(state.id); - if (!entry || !entry.ref.current) { + if (entry?.ref.current == null) { state.active = false; return false; } @@ -299,7 +299,7 @@ export const ScrollProvider: React.FC<{ children: React.ReactNode }> = ({ (targetThumbY / maxThumbY) * maxScrollTop, ); - if (entry.scrollTo) { + if (entry.scrollTo != null) { entry.scrollTo(targetScrollTop, 0); } else { entry.scrollBy(targetScrollTop - scrollTop); @@ -383,7 +383,7 @@ export const useScrollable = ( export function useScrollProvider(): ScrollContextType { const context = useContext(ScrollContext); - if (!context) { + if (context == null) { throw new Error('useScrollProvider must be used within a ScrollProvider'); } return context; diff --git a/packages/cli/src/ui/contexts/SessionContext.test.tsx b/packages/cli/src/ui/contexts/SessionContext.test.tsx index 940a10ea90..f316377b6a 100644 --- a/packages/cli/src/ui/contexts/SessionContext.test.tsx +++ b/packages/cli/src/ui/contexts/SessionContext.test.tsx @@ -4,13 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { type MutableRefObject } from 'react'; +import type { MutableRefObject } from 'react'; import { render, renderHook } from '../test-utils/render.js'; import { act } from 'react-dom/test-utils'; import { SessionStatsProvider, useSessionStats, - SessionMetrics, + type SessionMetrics, } from './SessionContext.js'; import { describe, it, expect, vi } from 'vitest'; import { uiTelemetryService } from '@vybestack/llxprt-code-core'; @@ -45,7 +45,7 @@ describe('SessionStatsContext', () => { expect(stats?.sessionStartTime).toBeInstanceOf(Date); expect(stats?.metrics).toBeDefined(); - expect(stats?.metrics.models).toEqual({}); + expect(stats?.metrics.models).toStrictEqual({}); }); it('should update metrics when the uiTelemetryService emits an update', () => { @@ -112,7 +112,7 @@ describe('SessionStatsContext', () => { }); const stats = contextRef.current?.stats; - expect(stats?.metrics).toEqual(newMetrics); + expect(stats?.metrics).toStrictEqual(newMetrics); expect(stats?.lastPromptTokenCount).toBe(100); }); diff --git a/packages/cli/src/ui/contexts/SessionContext.tsx b/packages/cli/src/ui/contexts/SessionContext.tsx index c17c445279..6f8fde56c4 100644 --- a/packages/cli/src/ui/contexts/SessionContext.tsx +++ b/packages/cli/src/ui/contexts/SessionContext.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { +import type React from 'react'; +import { createContext, useCallback, useContext, @@ -14,11 +15,11 @@ import React, { useEffect, } from 'react'; -import { - uiTelemetryService, +import type { SessionMetrics, ModelMetrics, - ToolCallStats, + uiTelemetryService, + type ToolCallStats, } from '@vybestack/llxprt-code-core'; export enum ToolCallDecision { diff --git a/packages/cli/src/ui/contexts/SessionStateContext.tsx b/packages/cli/src/ui/contexts/SessionStateContext.tsx index 4cec6a1962..a8f83f24f7 100644 --- a/packages/cli/src/ui/contexts/SessionStateContext.tsx +++ b/packages/cli/src/ui/contexts/SessionStateContext.tsx @@ -4,17 +4,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { +import type React from 'react'; +import { createContext, useContext, useReducer, - ReactNode, + type ReactNode, useMemo, } from 'react'; import { sessionReducer, - SessionState, - SessionAction, + type SessionState, + type SessionAction, } from '../reducers/sessionReducer.js'; // Context type with strict typing for [state, dispatch] @@ -54,7 +55,7 @@ export const SessionStateProvider: React.FC = ({ // Hook to use the session state context export const useSessionState = (): SessionStateContextType => { const context = useContext(SessionStateContext); - if (!context) { + if (context == null) { throw new Error('useSessionState must be used within SessionStateProvider'); } return context; diff --git a/packages/cli/src/ui/contexts/SettingsContext.tsx b/packages/cli/src/ui/contexts/SettingsContext.tsx index 83620a392d..6c2877aba9 100644 --- a/packages/cli/src/ui/contexts/SettingsContext.tsx +++ b/packages/cli/src/ui/contexts/SettingsContext.tsx @@ -5,7 +5,7 @@ */ import React, { useContext } from 'react'; -import { LoadedSettings } from '../../config/settings.js'; +import type { LoadedSettings } from '../../config/settings.js'; export const SettingsContext = React.createContext( undefined, diff --git a/packages/cli/src/ui/contexts/StreamingContext.tsx b/packages/cli/src/ui/contexts/StreamingContext.tsx index c22289d23b..85aa0c13b5 100644 --- a/packages/cli/src/ui/contexts/StreamingContext.tsx +++ b/packages/cli/src/ui/contexts/StreamingContext.tsx @@ -5,7 +5,7 @@ */ import React, { createContext } from 'react'; -import { StreamingState } from '../types.js'; +import type { StreamingState } from '../types.js'; export const StreamingContext = createContext( undefined, diff --git a/packages/cli/src/ui/contexts/TodoContext.tsx b/packages/cli/src/ui/contexts/TodoContext.tsx index f4bf8c1a49..e42b21da2c 100644 --- a/packages/cli/src/ui/contexts/TodoContext.tsx +++ b/packages/cli/src/ui/contexts/TodoContext.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { Todo } from '@vybestack/llxprt-code-core'; +import type { Todo } from '@vybestack/llxprt-code-core'; interface TodoContextType { todos: Todo[]; diff --git a/packages/cli/src/ui/contexts/TodoProvider.tsx b/packages/cli/src/ui/contexts/TodoProvider.tsx index e35b8c30e0..1c1368b392 100644 --- a/packages/cli/src/ui/contexts/TodoProvider.tsx +++ b/packages/cli/src/ui/contexts/TodoProvider.tsx @@ -4,10 +4,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import type React from 'react'; +import { useState, useEffect, useMemo, useCallback } from 'react'; import { TodoStore, - Todo, + type Todo, todoEvents, type TodoUpdateEvent, DEFAULT_AGENT_ID, diff --git a/packages/cli/src/ui/contexts/ToolCallContext.tsx b/packages/cli/src/ui/contexts/ToolCallContext.tsx index f2e417442f..86678450bd 100644 --- a/packages/cli/src/ui/contexts/ToolCallContext.tsx +++ b/packages/cli/src/ui/contexts/ToolCallContext.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { TodoToolCall } from '@vybestack/llxprt-code-core'; +import type { TodoToolCall } from '@vybestack/llxprt-code-core'; export interface ToolCallContextType { /** diff --git a/packages/cli/src/ui/contexts/ToolCallProvider.tsx b/packages/cli/src/ui/contexts/ToolCallProvider.tsx index a0bb160e3b..15e362dc1d 100644 --- a/packages/cli/src/ui/contexts/ToolCallProvider.tsx +++ b/packages/cli/src/ui/contexts/ToolCallProvider.tsx @@ -4,17 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { - useState, - useEffect, - useCallback, - useRef, - useMemo, -} from 'react'; -import { ToolCallContextType, ToolCallContext } from './ToolCallContext.js'; +import type React from 'react'; +import { useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import { + type ToolCallContextType, + ToolCallContext, +} from './ToolCallContext.js'; import { ToolCallTrackerService, - TodoToolCall, + type TodoToolCall, DEFAULT_AGENT_ID, } from '@vybestack/llxprt-code-core'; @@ -71,7 +69,7 @@ export const ToolCallProvider: React.FC = ({ // Set up subscription to tool call updates useEffect(() => { // Unsubscribe from previous subscription if it exists - if (unsubscribeRef.current) { + if (unsubscribeRef.current != null) { unsubscribeRef.current(); unsubscribeRef.current = null; } @@ -89,7 +87,7 @@ export const ToolCallProvider: React.FC = ({ // Clean up subscription on unmount return () => { - if (unsubscribeRef.current) { + if (unsubscribeRef.current != null) { unsubscribeRef.current(); unsubscribeRef.current = null; } @@ -99,17 +97,15 @@ export const ToolCallProvider: React.FC = ({ const contextValue = useMemo( () => ({ getExecutingToolCalls, - subscribe: (callback: () => void) => { - const unsubscribe = ToolCallTrackerService.subscribeToUpdates( + subscribe: (callback: () => void) => + ToolCallTrackerService.subscribeToUpdates( sessionId, () => { updateExecutingToolCalls(); callback(); }, scopedAgentId, - ); - return unsubscribe; - }, + ), }), [getExecutingToolCalls, scopedAgentId, sessionId, updateExecutingToolCalls], ); diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index 945b54bcf5..9b97c803de 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { createContext, useContext } from 'react'; +import type React from 'react'; +import { createContext, useContext } from 'react'; import type { IdeIntegrationNudgeResult } from '../IdeIntegrationNudge.js'; import type { HistoryItem } from '../types.js'; import type { FolderTrustChoice } from '../components/FolderTrustDialog.js'; @@ -214,7 +215,7 @@ export function UIActionsProvider({ export function useUIActions(): UIActions { const context = useContext(UIActionsContext); - if (!context) { + if (context == null) { throw new Error('useUIActions must be used within a UIActionsProvider'); } return context; diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index a8909d6f0d..3f316495e2 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -4,7 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { createContext, useContext } from 'react'; +import type React from 'react'; +import { createContext, useContext } from 'react'; import type { DOMElement } from 'ink'; import type { TextBuffer } from '../components/shared/text-buffer.js'; import type { @@ -258,7 +259,7 @@ export function UIStateProvider({ export function useUIState(): UIState { const context = useContext(UIStateContext); - if (!context) { + if (context == null) { throw new Error('useUIState must be used within a UIStateProvider'); } return context; diff --git a/packages/cli/src/ui/contexts/VimModeContext.tsx b/packages/cli/src/ui/contexts/VimModeContext.tsx index 8fa980109d..158e55461d 100644 --- a/packages/cli/src/ui/contexts/VimModeContext.tsx +++ b/packages/cli/src/ui/contexts/VimModeContext.tsx @@ -12,7 +12,7 @@ import { useState, useMemo, } from 'react'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; +import { type LoadedSettings, SettingScope } from '../../config/settings.js'; export type VimMode = 'NORMAL' | 'INSERT'; diff --git a/packages/cli/src/ui/editors/editorSettingsManager.ts b/packages/cli/src/ui/editors/editorSettingsManager.ts index e8f371ae31..b7273927bb 100644 --- a/packages/cli/src/ui/editors/editorSettingsManager.ts +++ b/packages/cli/src/ui/editors/editorSettingsManager.ts @@ -62,8 +62,8 @@ class EditorSettingsManager { return { name: EDITOR_DISPLAY_NAMES[type] + labelSuffix, - type, disabled: !hasEditor || !isAllowedInSandbox, + type, }; }), ]; diff --git a/packages/cli/src/ui/hooks/__tests__/useSessionBrowser.spec.ts b/packages/cli/src/ui/hooks/__tests__/useSessionBrowser.spec.ts index 600bf65f24..5c80ba06df 100644 --- a/packages/cli/src/ui/hooks/__tests__/useSessionBrowser.spec.ts +++ b/packages/cli/src/ui/hooks/__tests__/useSessionBrowser.spec.ts @@ -63,10 +63,10 @@ function makeConfig( return { sessionId: overrides.sessionId ?? crypto.randomUUID(), projectHash: overrides.projectHash ?? PROJECT_HASH, - chatsDir, workspaceDirs: overrides.workspaceDirs ?? ['/test/workspace'], provider: overrides.provider ?? 'anthropic', model: overrides.model ?? 'claude-4', + chatsDir, }; } @@ -346,9 +346,9 @@ describe('useSessionBrowser @plan:PLAN-20260214-SESSIONBROWSER.P13', () => { await fs.writeFile( lockPath, JSON.stringify({ - pid: 999999999, // Very unlikely to be an actual process + pid: 999999999, + timestamp: Date.now() - 60000, sessionId, - timestamp: Date.now() - 60000, // Old timestamp }), ); diff --git a/packages/cli/src/ui/hooks/__tests__/useSlashCompletion.set.phase09.test.ts b/packages/cli/src/ui/hooks/__tests__/useSlashCompletion.set.phase09.test.ts index 5f560b153d..29ed53d007 100644 --- a/packages/cli/src/ui/hooks/__tests__/useSlashCompletion.set.phase09.test.ts +++ b/packages/cli/src/ui/hooks/__tests__/useSlashCompletion.set.phase09.test.ts @@ -32,7 +32,7 @@ describe('`/set` schema contract for useSlashCompletion @plan:PLAN-20251013-AUTO ?.filter((node) => node.kind === 'literal') ?.map((node) => node.value) ?? []; - expect(literalValues).toEqual( + expect(literalValues).toStrictEqual( expect.arrayContaining(['unset', 'modelparam', 'emojifilter']), ); }); diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts index c56cfd7393..95eb083d6f 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { handleAtCommand } from './atCommandProcessor.js'; import { - Config, + type Config, FileDiscoveryService, GlobTool, type MessageBus, @@ -20,7 +20,6 @@ import { } from '@vybestack/llxprt-code-core'; import * as os from 'os'; import { ToolCallStatus } from '../types.js'; -import { type UseHistoryManagerReturn as _UseHistoryManagerReturn } from './useHistoryManager.js'; import * as fsPromises from 'fs/promises'; import * as fs from 'fs'; @@ -191,7 +190,7 @@ describe('handleAtCommand', () => { `${serverName}:${resourceUri}`, ); expect(readResource).toHaveBeenCalledWith(resourceUri); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: `\nContent from @${serverName}:${resourceUri}:\n` }, @@ -226,7 +225,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [{ text: query }], }); }); @@ -243,7 +242,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [{ text: queryWithSpaces }], }); expect(mockOnDebugMessage).toHaveBeenCalledWith( @@ -275,7 +274,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: `@${relativePath}` }, { text: '\n--- Content from referenced files ---' }, @@ -311,7 +310,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: `@${resolvedGlob}` }, { text: '\n--- Content from referenced files ---' }, @@ -342,7 +341,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: `${textBefore}@${relativePath}${textAfter}` }, { text: '\n--- Content from referenced files ---' }, @@ -369,7 +368,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: `@${relativePath}` }, { text: '\n--- Content from referenced files ---' }, @@ -406,7 +405,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: '\n--- Content from referenced files ---' }, @@ -440,7 +439,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: '\n--- Content from referenced files ---' }, @@ -476,7 +475,7 @@ describe('handleAtCommand', () => { expect(result.error).toBeUndefined(); expect(result.processedQuery).toBeDefined(); const processedQuery = result.processedQuery!; - expect((processedQuery as Array<{ text: string }>)[0]).toEqual({ + expect((processedQuery as Array<{ text: string }>)[0]).toStrictEqual({ text: `Look at @${relativePath1} then @${invalidFile} and also just @ symbol, then @${relativePath2}`, }); @@ -515,7 +514,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [{ text: 'Check @nonexistent.txt and @ also' }], }); }); @@ -548,7 +547,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [{ text: query }], }); expect(mockOnDebugMessage).toHaveBeenCalledWith( @@ -581,7 +580,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: `@${relativePath}` }, { text: '\n--- Content from referenced files ---' }, @@ -612,7 +611,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: `@${relativePath1} @${relativePath2}` }, { text: '\n--- Content from referenced files ---' }, @@ -645,7 +644,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [{ text: query }], }); expect(mockOnDebugMessage).toHaveBeenCalledWith( @@ -678,7 +677,7 @@ describe('handleAtCommand', () => { expect(mockOnDebugMessage).toHaveBeenCalledWith( `Glob tool not found. Path ${invalidFile} will be skipped.`, ); - expect(result.processedQuery).toEqual([{ text: query }]); + expect(result.processedQuery).toStrictEqual([{ text: query }]); expect(result.processedQuery).not.toBeNull(); expect(result.error).toBeUndefined(); }); @@ -705,7 +704,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [{ text: query }], }); expect(mockOnDebugMessage).toHaveBeenCalledWith( @@ -737,7 +736,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: `@${relativePath}` }, { text: '\n--- Content from referenced files ---' }, @@ -774,7 +773,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: `@${relativePath1} @${relativePath2}` }, { text: '\n--- Content from referenced files ---' }, @@ -890,17 +889,17 @@ describe('handleAtCommand', () => { const query = queryTemplate(fileName); const result = await handleAtCommand({ - query, config: mockConfig, addItem: mockAddItem, onDebugMessage: mockOnDebugMessage, - messageId, signal: abortController.signal, + query, + messageId, }); const fileInQuery = fileName; - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: '\n--- Content from referenced files ---' }, @@ -928,7 +927,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: '\n--- Content from referenced files ---' }, @@ -959,7 +958,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: 'Check @spaced file.txt, it has spaces.' }, { text: '\n--- Content from referenced files ---' }, @@ -984,7 +983,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: '\n--- Content from referenced files ---' }, @@ -1009,7 +1008,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: '\n--- Content from referenced files ---' }, @@ -1034,7 +1033,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: '\n--- Content from referenced files ---' }, @@ -1062,7 +1061,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: '\n--- Content from referenced files ---' }, @@ -1087,7 +1086,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: '\n--- Content from referenced files ---' }, @@ -1115,7 +1114,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: 'Check @file$with&special#chars.txt for content.' }, { text: '\n--- Content from referenced files ---' }, @@ -1143,7 +1142,7 @@ describe('handleAtCommand', () => { signal: abortController.signal, }); - expect(result).toEqual({ + expect(result).toStrictEqual({ processedQuery: [ { text: query }, { text: '\n--- Content from referenced files ---' }, diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts index 9be29787a6..a16e1abeb8 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts @@ -7,24 +7,24 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import { Buffer } from 'buffer'; -import { PartListUnion, PartUnion } from '@google/genai'; +import type { PartListUnion, PartUnion } from '@google/genai'; import { - AnyToolInvocation, - Config, + type AnyToolInvocation, + type Config, DEFAULT_AGENT_ID, getErrorMessage, isNodeError, unescapePath, debugLogger, + validatePathWithinWorkspace, + type DiscoveredMCPResource, } from '@vybestack/llxprt-code-core'; -import type { DiscoveredMCPResource } from '@vybestack/llxprt-code-core'; -import { validatePathWithinWorkspace } from '@vybestack/llxprt-code-core'; import { - HistoryItem, - IndividualToolCallDisplay, + type HistoryItem, + type IndividualToolCallDisplay, ToolCallStatus, } from '../types.js'; -import { UseHistoryManagerReturn } from './useHistoryManager.js'; +import type { UseHistoryManagerReturn } from './useHistoryManager.js'; // Detect if running in PowerShell to handle @ symbol conflicts // PowerShell's IntelliSense treats @ as hashtable start and causes severe lag @@ -232,7 +232,7 @@ export async function handleAtCommand({ const readManyFilesTool = toolRegistry.getTool('read_many_files'); const globTool = toolRegistry.getTool('glob'); - if (!readManyFilesTool) { + if (readManyFilesTool == null) { addItem( { type: 'error', text: 'Error: read_many_files tool not found.' }, userMessageTimestamp, @@ -271,7 +271,7 @@ export async function handleAtCommand({ } const resourceMatch = resourceRegistry.findResourceByUri(pathName); - if (resourceMatch) { + if (resourceMatch != null) { resourceAttachments.push(resourceMatch); atPathToResolvedSpecMap.set(originalAtPath, pathName); continue; @@ -341,7 +341,7 @@ export async function handleAtCommand({ resolvedSuccessfully = true; } catch (error) { if (isNodeError(error) && error.code === 'ENOENT') { - if (config.getEnableRecursiveFileSearch() && globTool) { + if (config.getEnableRecursiveFileSearch() && globTool != null) { onDebugMessage( `Path ${pathName} not found directly, attempting glob search.`, ); @@ -523,7 +523,7 @@ export async function handleAtCommand({ } | undefined )?.getClient(resource.serverName); - if (!client) { + if (client == null) { const toolCallDisplay: IndividualToolCallDisplay = { callId: `mcp-resource-${resource.serverName}-${uri}`, name: `resources/read (${resource.serverName})`, @@ -621,7 +621,7 @@ export async function handleAtCommand({ for (const part of result.llmContent) { if (typeof part === 'string') { const match = fileContentRegex.exec(part); - if (match) { + if (match != null) { const filePathSpecInContent = match[1]; const fileActualContent = match[2].trim(); diff --git a/packages/cli/src/ui/hooks/geminiStream/__tests__/checkpointPersistence.test.ts b/packages/cli/src/ui/hooks/geminiStream/__tests__/checkpointPersistence.test.ts index d0acc8a068..f9850d6d6b 100644 --- a/packages/cli/src/ui/hooks/geminiStream/__tests__/checkpointPersistence.test.ts +++ b/packages/cli/src/ui/hooks/geminiStream/__tests__/checkpointPersistence.test.ts @@ -152,7 +152,7 @@ describe('createToolCheckpoint', () => { expect(writtenContent.commitHash).toBe('snap456'); expect(writtenContent.filePath).toBe('/project/src/foo.ts'); expect(writtenContent.toolCall.name).toBe('replace'); - expect(writtenContent.history).toEqual(mockHistory); + expect(writtenContent.history).toStrictEqual(mockHistory); expect(onDebugMessage).not.toHaveBeenCalled(); }); diff --git a/packages/cli/src/ui/hooks/geminiStream/__tests__/streamUtils.test.ts b/packages/cli/src/ui/hooks/geminiStream/__tests__/streamUtils.test.ts index 4e1f6662c3..9fe11d64c8 100644 --- a/packages/cli/src/ui/hooks/geminiStream/__tests__/streamUtils.test.ts +++ b/packages/cli/src/ui/hooks/geminiStream/__tests__/streamUtils.test.ts @@ -55,27 +55,27 @@ vi.mock('../../../utils/markdownUtilities.js', async () => ({ describe('mergePartListUnions', () => { it('merges string items into text parts', () => { const result = mergePartListUnions(['hello', 'world']); - expect(result).toEqual([{ text: 'hello' }, { text: 'world' }]); + expect(result).toStrictEqual([{ text: 'hello' }, { text: 'world' }]); }); it('merges Part objects directly', () => { const part: Part = { text: 'foo' }; const result = mergePartListUnions([part]); - expect(result).toEqual([{ text: 'foo' }]); + expect(result).toStrictEqual([{ text: 'foo' }]); }); it('merges arrays of string/Part', () => { const result = mergePartListUnions([['a', { text: 'b' }]]); - expect(result).toEqual([{ text: 'a' }, { text: 'b' }]); + expect(result).toStrictEqual([{ text: 'a' }, { text: 'b' }]); }); it('returns empty array for empty input', () => { - expect(mergePartListUnions([])).toEqual([]); + expect(mergePartListUnions([])).toStrictEqual([]); }); it('flattens nested arrays', () => { const result = mergePartListUnions([['a', 'b'], ['c'], 'd']); - expect(result).toEqual([ + expect(result).toStrictEqual([ { text: 'a' }, { text: 'b' }, { text: 'c' }, @@ -226,7 +226,7 @@ describe('collectGeminiTools', () => { ]; const result = collectGeminiTools(tools); expect(result).toHaveLength(2); - expect(result.map((t) => t.request.name)).toEqual([ + expect(result.map((t) => t.request.name)).toStrictEqual([ 'gemini-tool', 'no-flag-tool', ]); @@ -297,7 +297,7 @@ describe('deduplicateToolCallRequests', () => { const requests = [makeRequest('a'), makeRequest('b'), makeRequest('a')]; const result = deduplicateToolCallRequests(requests); expect(result).toHaveLength(2); - expect(result.map((r) => r.callId)).toEqual(['a', 'b']); + expect(result.map((r) => r.callId)).toStrictEqual(['a', 'b']); }); it('preserves insertion order', () => { @@ -308,7 +308,7 @@ describe('deduplicateToolCallRequests', () => { makeRequest('a'), ]; const result = deduplicateToolCallRequests(requests); - expect(result.map((r) => r.callId)).toEqual(['c', 'a', 'b']); + expect(result.map((r) => r.callId)).toStrictEqual(['c', 'a', 'b']); }); it('returns empty for empty input', () => { @@ -326,7 +326,7 @@ describe('deduplicateToolCallRequests', () => { describe('buildThinkingBlock', () => { it('creates a ThinkingBlock from thought text', () => { const block = buildThinkingBlock('my thought', []); - expect(block).toEqual({ + expect(block).toStrictEqual({ type: 'thinking', thought: 'my thought', sourceField: 'thought', @@ -575,7 +575,7 @@ describe('showCitations', () => { const config = makeConfig({ getSettingsService: vi.fn(() => mockSettingsService), }); - expect(showCitations(makeSettings(undefined), config)).toBe(true); + expect(showCitations(makeSettings(), config)).toBe(true); }); it('returns false when settingsService.get returns false', () => { @@ -583,7 +583,7 @@ describe('showCitations', () => { const config = makeConfig({ getSettingsService: vi.fn(() => mockSettingsService), }); - expect(showCitations(makeSettings(undefined), config)).toBe(false); + expect(showCitations(makeSettings(), config)).toBe(false); }); it('falls through to settings.merged when settingsService.get returns undefined', () => { @@ -612,19 +612,19 @@ describe('showCitations', () => { const config = makeConfig({ getSettingsService: vi.fn(() => null) }); // Non-FREE tier → true mockGetCodeAssistServer.mockReturnValue({ userTier: 'STANDARD' }); - expect(showCitations(makeSettings(undefined), config)).toBe(true); + expect(showCitations(makeSettings(), config)).toBe(true); }); it('returns false when userTier is FREE', () => { const config = makeConfig({ getSettingsService: vi.fn(() => null) }); mockGetCodeAssistServer.mockReturnValue({ userTier: 'free-tier' }); - expect(showCitations(makeSettings(undefined), config)).toBe(false); + expect(showCitations(makeSettings(), config)).toBe(false); }); it('returns false when getCodeAssistServer returns undefined', () => { const config = makeConfig({ getSettingsService: vi.fn(() => null) }); mockGetCodeAssistServer.mockReturnValue(undefined); - expect(showCitations(makeSettings(undefined), config)).toBe(false); + expect(showCitations(makeSettings(), config)).toBe(false); }); }); diff --git a/packages/cli/src/ui/hooks/geminiStream/__tests__/toolCompletionHandler.test.ts b/packages/cli/src/ui/hooks/geminiStream/__tests__/toolCompletionHandler.test.ts index ab86d45325..f427ad13c7 100644 --- a/packages/cli/src/ui/hooks/geminiStream/__tests__/toolCompletionHandler.test.ts +++ b/packages/cli/src/ui/hooks/geminiStream/__tests__/toolCompletionHandler.test.ts @@ -19,7 +19,7 @@ * - External tools marked even when primaryTools is empty */ -import React from 'react'; +import type React from 'react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { Part } from '@google/genai'; import { DEFAULT_AGENT_ID } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/ui/hooks/geminiStream/checkpointPersistence.ts b/packages/cli/src/ui/hooks/geminiStream/checkpointPersistence.ts index 7e6667e498..25d1b7323e 100644 --- a/packages/cli/src/ui/hooks/geminiStream/checkpointPersistence.ts +++ b/packages/cli/src/ui/hooks/geminiStream/checkpointPersistence.ts @@ -19,14 +19,14 @@ import { useEffect, useRef } from 'react'; import path from 'path'; import { promises as nodeFs } from 'fs'; import { - Config, - GeminiClient, - GitService, + type Config, + type GeminiClient, + type GitService, getErrorMessage, isNodeError, } from '@vybestack/llxprt-code-core'; -import { TrackedToolCall } from '../useReactToolScheduler.js'; -import { HistoryItem } from '../../types.js'; +import type { TrackedToolCall } from '../useReactToolScheduler.js'; +import type { HistoryItem } from '../../types.js'; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -171,7 +171,7 @@ async function saveRestorableToolCalls( } for (const toolCall of restorableToolCalls) { - if (!gitService) { + if (gitService == null) { const filePath = toolCall.request.args['file_path'] as string; onDebugMessage( `Checkpointing is enabled but Git service is not available. Failed to create snapshot for ${filePath ?? toolCall.request.name}. Ensure Git is installed and working properly.`, diff --git a/packages/cli/src/ui/hooks/geminiStream/streamUtils.ts b/packages/cli/src/ui/hooks/geminiStream/streamUtils.ts index 7a62170429..87e5a279de 100644 --- a/packages/cli/src/ui/hooks/geminiStream/streamUtils.ts +++ b/packages/cli/src/ui/hooks/geminiStream/streamUtils.ts @@ -16,29 +16,29 @@ */ import { - Config, + type Config, getCodeAssistServer, UserTierId, UnauthorizedError, getErrorMessage, isNodeError, parseAndFormatApiError, - ToolCallRequestInfo, + type ToolCallRequestInfo, DEFAULT_AGENT_ID, type ThinkingBlock, } from '@vybestack/llxprt-code-core'; import { type Part, type PartListUnion, FinishReason } from '@google/genai'; -import { LoadedSettings } from '../../../config/settings.js'; +import type { LoadedSettings } from '../../../config/settings.js'; import { - HistoryItemWithoutId, - HistoryItemGemini, - HistoryItemGeminiContent, + type HistoryItemWithoutId, + type HistoryItemGemini, + type HistoryItemGeminiContent, MessageType, - SlashCommandProcessorResult, + type SlashCommandProcessorResult, } from '../../types.js'; import { findLastSafeSplitPoint } from '../../utils/markdownUtilities.js'; import { SHELL_COMMAND_NAME, SHELL_NAME } from '../../constants.js'; -import { UseHistoryManagerReturn } from '../useHistoryManager.js'; +import type { UseHistoryManagerReturn } from '../useHistoryManager.js'; // ─── Re-exported constant ──────────────────────────────────────────────────── @@ -151,9 +151,9 @@ export function splitPartsByRole(parts: Part[]): { const otherParts: Part[] = []; for (const part of parts) { - if (part && typeof part === 'object' && 'functionCall' in part) { + if (typeof part === 'object' && 'functionCall' in part) { functionCalls.push(part); - } else if (part && typeof part === 'object' && 'functionResponse' in part) { + } else if (typeof part === 'object' && 'functionResponse' in part) { functionResponses.push(part); } else { otherParts.push(part); @@ -169,7 +169,7 @@ export function splitPartsByRole(parts: Part[]): { export function collectGeminiTools< T extends { request: { isClientInitiated?: boolean } }, >(primaryTools: T[]): T[] { - return primaryTools.filter((t) => !t.request.isClientInitiated); + return primaryTools.filter((t) => t.request.isClientInitiated !== true); } /** @@ -266,7 +266,7 @@ export function buildFullSplitItem( )?.profileName; const profileName = liveProfileName ?? existingProfileName; return { - type: (currentItem?.type as 'gemini' | 'gemini_content') ?? 'gemini', + type: currentItem?.type === 'gemini_content' ? 'gemini_content' : 'gemini', text: sanitizedCombined, ...(profileName != null ? { profileName } : {}), ...(thinkingBlocks.length > 0 @@ -346,8 +346,8 @@ export async function processSlashCommandResult( name: toolName, args: toolArgs, isClientInitiated: true, - prompt_id, agentId: DEFAULT_AGENT_ID, + prompt_id, }; await scheduleToolCalls([toolCallRequest], abortSignal); return { queryToSend: null, shouldProceed: false }; @@ -415,17 +415,15 @@ export function showCitations( ): boolean { try { const settingsService = config.getSettingsService(); - if (settingsService) { - const enabled = settingsService.get('ui.showCitations'); - if (enabled !== undefined) { - return enabled as boolean; - } + const enabled = settingsService.get('ui.showCitations'); + if (enabled !== undefined) { + return enabled as boolean; } } catch { // Fall through to other methods } - const enabled = (settings?.merged as { ui?: { showCitations?: boolean } })?.ui + const enabled = (settings.merged as { ui?: { showCitations?: boolean } })?.ui ?.showCitations; if (enabled !== undefined) { return enabled; @@ -443,10 +441,7 @@ export function showCitations( export function getCurrentProfileName(config: Config): string | null { try { const settingsService = config.getSettingsService(); - if ( - settingsService && - typeof settingsService.getCurrentProfileName === 'function' - ) { + if (typeof settingsService.getCurrentProfileName === 'function') { return settingsService.getCurrentProfileName() ?? null; } } catch { diff --git a/packages/cli/src/ui/hooks/geminiStream/toolCompletionHandler.ts b/packages/cli/src/ui/hooks/geminiStream/toolCompletionHandler.ts index a7a02dafe5..cee07b3b3a 100644 --- a/packages/cli/src/ui/hooks/geminiStream/toolCompletionHandler.ts +++ b/packages/cli/src/ui/hooks/geminiStream/toolCompletionHandler.ts @@ -16,16 +16,18 @@ */ import { useRef, useCallback, useEffect } from 'react'; -import { GeminiClient } from '@vybestack/llxprt-code-core'; -import type { Part } from '@google/genai'; -import { DEFAULT_AGENT_ID, DebugLogger } from '@vybestack/llxprt-code-core'; import { + type GeminiClient, + DEFAULT_AGENT_ID, + DebugLogger, +} from '@vybestack/llxprt-code-core'; +import type { Part, PartListUnion } from '@google/genai'; +import type { TrackedToolCall, TrackedCompletedToolCall, TrackedCancelledToolCall, } from '../useReactToolScheduler.js'; import { splitPartsByRole } from './streamUtils.js'; -import type { PartListUnion } from '@google/genai'; const geminiStreamLogger = new DebugLogger('llxprt:ui:gemini-stream'); diff --git a/packages/cli/src/ui/hooks/geminiStream/types.ts b/packages/cli/src/ui/hooks/geminiStream/types.ts index 58aaffe1b5..b948bc12c8 100644 --- a/packages/cli/src/ui/hooks/geminiStream/types.ts +++ b/packages/cli/src/ui/hooks/geminiStream/types.ts @@ -9,7 +9,7 @@ * Only cross-module stable types belong here. */ -import { type PartListUnion } from '@google/genai'; +import type { PartListUnion } from '@google/genai'; export enum StreamProcessingStatus { Completed, diff --git a/packages/cli/src/ui/hooks/geminiStream/useGeminiStream.ts b/packages/cli/src/ui/hooks/geminiStream/useGeminiStream.ts index e6f66ccc9a..cfd287aeb2 100644 --- a/packages/cli/src/ui/hooks/geminiStream/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/geminiStream/useGeminiStream.ts @@ -5,7 +5,7 @@ */ import { useState, useRef, useCallback, useEffect, useMemo } from 'react'; -import { +import type { Config, GeminiClient, EditorType, @@ -20,25 +20,24 @@ import { debugLogger, } from '@vybestack/llxprt-code-core'; import { type PartListUnion } from '@google/genai'; -import { LoadedSettings } from '../../../config/settings.js'; -import { - StreamingState, +import type { LoadedSettings } from '../../../config/settings.js'; +import type { HistoryItem, HistoryItemWithoutId, - MessageType, SlashCommandProcessorResult, } from '../../types.js'; +import { StreamingState, MessageType } from '../../types.js'; import { isSlashCommand } from '../../utils/commandUtils.js'; import { useShellCommandProcessor } from '../shellCommandProcessor.js'; import { useStateAndRef } from '../useStateAndRef.js'; -import { UseHistoryManagerReturn } from '../useHistoryManager.js'; +import type { UseHistoryManagerReturn } from '../useHistoryManager.js'; import { useLogger } from '../useLogger.js'; -import { - useReactToolScheduler, +import type { TrackedToolCall, TrackedCompletedToolCall, TrackedCancelledToolCall, } from '../useReactToolScheduler.js'; +import { useReactToolScheduler } from '../useReactToolScheduler.js'; import { mapToDisplay as mapTrackedToolCallsToDisplay } from '../toolMapping.js'; import { useSessionStats } from '../../contexts/SessionContext.js'; import { useKeypress, type Key } from '../useKeypress.js'; diff --git a/packages/cli/src/ui/hooks/geminiStream/useStreamEventHandlers.ts b/packages/cli/src/ui/hooks/geminiStream/useStreamEventHandlers.ts index 5020de0645..968f392e6b 100644 --- a/packages/cli/src/ui/hooks/geminiStream/useStreamEventHandlers.ts +++ b/packages/cli/src/ui/hooks/geminiStream/useStreamEventHandlers.ts @@ -13,15 +13,15 @@ import type React from 'react'; import { useCallback } from 'react'; import { - Config, + type Config, GeminiEventType as ServerGeminiEventType, - ServerGeminiStreamEvent as GeminiEvent, - ServerGeminiContentEvent as ContentEvent, - ServerGeminiErrorEvent as ErrorEvent, - ServerGeminiChatCompressedEvent, - ServerGeminiFinishedEvent, + type ServerGeminiStreamEvent as GeminiEvent, + type ServerGeminiContentEvent as ContentEvent, + type ServerGeminiErrorEvent as ErrorEvent, + type ServerGeminiChatCompressedEvent, + type ServerGeminiFinishedEvent, MessageSenderType, - ToolCallRequestInfo, + type ToolCallRequestInfo, logUserPrompt, UserPromptEvent, parseAndFormatApiError, @@ -31,20 +31,20 @@ import { uiTelemetryService, type ThoughtSummary, } from '@vybestack/llxprt-code-core'; -import { type PartListUnion } from '@google/genai'; -import { LoadedSettings } from '../../../config/settings.js'; +import type { PartListUnion } from '@google/genai'; +import type { LoadedSettings } from '../../../config/settings.js'; import { - HistoryItem, - HistoryItemWithoutId, - HistoryItemToolGroup, - HistoryItemGemini, - HistoryItemGeminiContent, + type HistoryItem, + type HistoryItemWithoutId, + type HistoryItemToolGroup, + type HistoryItemGemini, + type HistoryItemGeminiContent, MessageType, - SlashCommandProcessorResult, + type SlashCommandProcessorResult, ToolCallStatus, } from '../../types.js'; import { isAtCommand, isSlashCommand } from '../../utils/commandUtils.js'; -import { UseHistoryManagerReturn } from '../useHistoryManager.js'; +import type { UseHistoryManagerReturn } from '../useHistoryManager.js'; import { SYSTEM_NOTICE_EVENT, showCitations, @@ -133,7 +133,7 @@ function applyThoughtToState( thoughtText, thinkingBlocksRef.current, ); - if (thinkingBlock) { + if (thinkingBlock != null) { thinkingBlocksRef.current.push(thinkingBlock); const liveProfileName = getCurrentProfileName(config); setPendingHistoryItem((item) => { @@ -142,8 +142,8 @@ function applyThoughtToState( )?.profileName; const pn = liveProfileName ?? ep; return { - type: (item?.type as 'gemini' | 'gemini_content') || 'gemini', - text: item?.text || '', + type: (item?.type as 'gemini' | 'gemini_content') ?? 'gemini', + text: item?.text ?? '', ...(pn != null ? { profileName: pn } : {}), thinkingBlocks: [...thinkingBlocksRef.current], }; @@ -220,7 +220,7 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { pendingHistoryItemRef.current?.type !== 'gemini' && pendingHistoryItemRef.current?.type !== 'gemini_content' ) { - if (pendingHistoryItemRef.current) + if (pendingHistoryItemRef.current != null) flushPendingHistoryItem(userMessageTimestamp); setPendingHistoryItem({ type: 'gemini', @@ -299,7 +299,7 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { if (turnCancelledRef.current) { return; } - if (pendingHistoryItemRef.current) { + if (pendingHistoryItemRef.current != null) { if (pendingHistoryItemRef.current.type === 'tool_group') { const updatedTools = pendingHistoryItemRef.current.tools.map( (tool) => @@ -341,7 +341,7 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { const handleErrorEvent = useCallback( (eventValue: ErrorEvent['value'], userMessageTimestamp: number) => { - if (pendingHistoryItemRef.current) { + if (pendingHistoryItemRef.current != null) { flushPendingHistoryItem(userMessageTimestamp); setPendingHistoryItem(null); } @@ -374,7 +374,7 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { return; } - if (pendingHistoryItemRef.current) { + if (pendingHistoryItemRef.current != null) { flushPendingHistoryItem(userMessageTimestamp); setPendingHistoryItem(null); } @@ -408,7 +408,7 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { eventValue: ServerGeminiChatCompressedEvent['value'], userMessageTimestamp: number, ) => { - if (pendingHistoryItemRef.current) { + if (pendingHistoryItemRef.current != null) { addItem(pendingHistoryItemRef.current, userMessageTimestamp); setPendingHistoryItem(null); } @@ -564,11 +564,11 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { addItem( { type: MessageType.INFO, - text: `Execution stopped by hook: ${event.systemMessage?.trim() || event.reason}`, + text: `Execution stopped by hook: ${event.systemMessage?.trim() ?? event.reason}`, }, userMessageTimestamp, ); - if (event.contextCleared) { + if (event.contextCleared === true) { addItem( { type: MessageType.INFO, @@ -582,11 +582,11 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { addItem( { type: MessageType.INFO, - text: `Execution blocked by hook: ${event.systemMessage?.trim() || event.reason}`, + text: `Execution blocked by hook: ${event.systemMessage?.trim() ?? event.reason}`, }, userMessageTimestamp, ); - if (event.contextCleared) { + if (event.contextCleared === true) { addItem( { type: MessageType.INFO, @@ -610,7 +610,7 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { ) { const deduped = deduplicateToolCallRequests(toolCallRequests); if (deduped.length > 0) { - if (pendingHistoryItemRef.current) { + if (pendingHistoryItemRef.current != null) { addItem(pendingHistoryItemRef.current, userMessageTimestamp); setPendingHistoryItem(null); } @@ -649,7 +649,7 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { userMessageTimestamp, ); const showProfileChangeInChat = - settings?.merged?.showProfileChangeInChat ?? true; + settings.merged.showProfileChangeInChat ?? true; const liveProfileName = getCurrentProfileName(config); if ( showProfileChangeInChat && @@ -670,7 +670,7 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { [ addItem, config, - settings?.merged?.showProfileChangeInChat, + settings.merged.showProfileChangeInChat, lastProfileNameRef, ], ); @@ -706,7 +706,7 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { const slashCommandResult = isSlashCommand(trimmedQuery) ? await handleSlashCommand(trimmedQuery) : false; - if (slashCommandResult) { + if (slashCommandResult !== false) { return processSlashCommandResult( slashCommandResult, scheduleToolCalls, @@ -723,11 +723,11 @@ export function useStreamEventHandlers(deps: StreamEventHandlerDeps) { if (isAtCommand(trimmedQuery)) { const atCommandResult = await handleAtCommand({ query: trimmedQuery, + messageId: userMessageTimestamp, + signal: abortSignal, config, addItem, onDebugMessage, - messageId: userMessageTimestamp, - signal: abortSignal, }); if (atCommandResult.error) { onDebugMessage(atCommandResult.error); diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts index 062d0ffbc7..2363fc027b 100644 --- a/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.test.ts @@ -49,11 +49,11 @@ import { useShellCommandProcessor, OUTPUT_UPDATE_INTERVAL_MS, } from './shellCommandProcessor.js'; -import { - type Config, - type GeminiClient, - type ShellExecutionResult, - type ShellOutputEvent, +import type { + Config, + GeminiClient, + ShellExecutionResult, + ShellOutputEvent, } from '@vybestack/llxprt-code-core'; import * as fs from 'fs'; // import os from 'os'; // Not needed - mocked above @@ -233,7 +233,7 @@ describe('useShellCommandProcessor', () => { expect(setPendingHistoryItemMock).toHaveBeenCalledWith(null); expect(addItemToHistoryMock).toHaveBeenCalledTimes(2); // Initial + final - expect(addItemToHistoryMock.mock.calls[1][0]).toEqual( + expect(addItemToHistoryMock.mock.calls[1][0]).toStrictEqual( expect.objectContaining({ tools: [ expect.objectContaining({ @@ -346,7 +346,7 @@ describe('useShellCommandProcessor', () => { }); // Verify second output was cumulative - expect(pendingHistoryItemState).toEqual( + expect(pendingHistoryItemState).toStrictEqual( expect.objectContaining({ tools: [expect.objectContaining({ resultDisplay: 'hello world' })], }), @@ -373,7 +373,7 @@ describe('useShellCommandProcessor', () => { }); // The implementation now uses an updater function, so check the resulting state - expect(pendingHistoryItemState).toEqual( + expect(pendingHistoryItemState).toStrictEqual( expect.objectContaining({ tools: [ expect.objectContaining({ @@ -395,7 +395,7 @@ describe('useShellCommandProcessor', () => { }); // The implementation now uses an updater function, so check the resulting state - expect(pendingHistoryItemState).toEqual( + expect(pendingHistoryItemState).toStrictEqual( expect.objectContaining({ tools: [ expect.objectContaining({ @@ -417,7 +417,6 @@ describe('useShellCommandProcessor', () => { setShellInputFocusedMock, 80, 24, - undefined, ), ); @@ -441,7 +440,7 @@ describe('useShellCommandProcessor', () => { mockShellOutputCallback({ type: 'data', chunk: 'hello' }); }); - expect(pendingHistoryItemState).toEqual( + expect(pendingHistoryItemState).toStrictEqual( expect.objectContaining({ tools: [expect.objectContaining({ resultDisplay: 'hello' })], }), @@ -543,7 +542,7 @@ describe('useShellCommandProcessor', () => { expect(setPendingHistoryItemMock).toHaveBeenCalledWith(null); expect(addItemToHistoryMock).toHaveBeenCalledTimes(2); - expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({ + expect(addItemToHistoryMock.mock.calls[1][0]).toStrictEqual({ type: 'error', text: 'An unexpected error occurred: Unexpected failure', }); @@ -571,7 +570,7 @@ describe('useShellCommandProcessor', () => { expect(setPendingHistoryItemMock).toHaveBeenCalledWith(null); expect(addItemToHistoryMock).toHaveBeenCalledTimes(2); - expect(addItemToHistoryMock.mock.calls[1][0]).toEqual({ + expect(addItemToHistoryMock.mock.calls[1][0]).toStrictEqual({ type: 'error', text: 'An unexpected error occurred: Synchronous spawn error', }); diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.ts index 7b3bf40058..bb2f6d8bf2 100644 --- a/packages/cli/src/ui/hooks/shellCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.ts @@ -5,22 +5,22 @@ */ import { - HistoryItemWithoutId, - IndividualToolCallDisplay, + type HistoryItemWithoutId, + type IndividualToolCallDisplay, ToolCallStatus, } from '../types.js'; import { useCallback, useState } from 'react'; import { - Config, - GeminiClient, + type Config, + type GeminiClient, isBinary, - ShellExecutionResult, + type ShellExecutionResult, ShellExecutionService, DEFAULT_AGENT_ID, type AnsiOutput, } from '@vybestack/llxprt-code-core'; -import { type PartListUnion } from '@google/genai'; -import { UseHistoryManagerReturn } from './useHistoryManager.js'; +import type { PartListUnion } from '@google/genai'; +import type { UseHistoryManagerReturn } from './useHistoryManager.js'; import { SHELL_COMMAND_NAME } from '../constants.js'; import { formatMemoryUsage } from '../utils/formatters.js'; import crypto from 'crypto'; @@ -43,7 +43,7 @@ function addShellCommandToGeminiHistory( ? resultText.substring(0, MAX_OUTPUT_LENGTH) + '\n... (truncated)' : resultText; - if (geminiClient) { + if (geminiClient != null) { // eslint-disable-next-line @typescript-eslint/no-floating-promises geminiClient.addHistory({ role: 'user', @@ -140,7 +140,7 @@ export const useShellCommandProcessor = ( tools: [initialToolDisplay], }; - if (pendingHistoryItemRef) { + if (pendingHistoryItemRef != null) { pendingHistoryItemRef.current = initialPendingItem; } setPendingHistoryItem(initialPendingItem); @@ -275,7 +275,7 @@ export const useShellCommandProcessor = ( tools: [{ ...initialToolDisplay, ptyId: pid }], }; - if (pendingHistoryItemRef) { + if (pendingHistoryItemRef != null) { pendingHistoryItemRef.current = nextItem; } @@ -301,7 +301,7 @@ export const useShellCommandProcessor = ( let finalOutput = mainContent; let finalStatus = ToolCallStatus.Success; - if (result.error) { + if (result.error != null) { finalStatus = ToolCallStatus.Error; finalOutput = `${result.error.message}\n${finalOutput}`; } else if (result.aborted) { diff --git a/packages/cli/src/ui/hooks/shouldClearTodos-fix.test.ts b/packages/cli/src/ui/hooks/shouldClearTodos-fix.test.ts index 0257816889..0884e01d45 100644 --- a/packages/cli/src/ui/hooks/shouldClearTodos-fix.test.ts +++ b/packages/cli/src/ui/hooks/shouldClearTodos-fix.test.ts @@ -5,7 +5,7 @@ */ import { describe, it, expect } from 'vitest'; -import { type Todo } from '@vybestack/llxprt-code-core'; +import type { Todo } from '@vybestack/llxprt-code-core'; import { shouldClearTodos } from './useTodoPausePreserver.js'; /** diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts index fd11fff659..39e2cd81f4 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.ts @@ -94,7 +94,6 @@ const coreMocks = vi.hoisted(() => { Config: class {}, GitService: vi.fn(), Logger: StubLogger, - logSlashCommand, SlashCommandEvent: class { name: string; subcommand?: string; @@ -111,7 +110,6 @@ const coreMocks = vi.hoisted(() => { Storage: class {}, ProfileManager: class {}, SubagentManager: class {}, - uiTelemetryService, SessionMetrics: class {}, ModelMetrics: class {}, DebugLogger: class { @@ -128,6 +126,8 @@ const coreMocks = vi.hoisted(() => { }, addMCPStatusChangeListener: vi.fn(), removeMCPStatusChangeListener: vi.fn(), + logSlashCommand, + uiTelemetryService, }; }); diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 97937da756..25900b843c 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -5,7 +5,7 @@ */ import { useCallback, useMemo, useEffect, useState } from 'react'; -import { type PartListUnion } from '@google/genai'; +import type { PartListUnion } from '@google/genai'; import process from 'node:process'; import * as path from 'node:path'; import * as os from 'node:os'; @@ -50,11 +50,11 @@ import type { } from '../types.js'; import { MessageType, ToolCallStatus } from '../types.js'; import type { LoadedSettings } from '../../config/settings.js'; -import { - type CommandContext, - type SlashCommand, - type SubagentDialogData, - type ModelsDialogData, +import type { + CommandContext, + SlashCommand, + SubagentDialogData, + ModelsDialogData, } from '../commands/types.js'; import { CommandService } from '../../services/CommandService.js'; import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js'; @@ -65,7 +65,7 @@ import type { ExtensionUpdateState, ExtensionUpdateAction, } from '../state/extensions.js'; -import { SubagentView } from '../components/SubagentManagement/types.js'; +import type { SubagentView } from '../components/SubagentManagement/types.js'; import { secureInputHandler } from '../utils/secureInputHandler.js'; const confirmationLogger = new DebugLogger('llxprt:ui:selection'); @@ -137,7 +137,7 @@ export const useSlashCommandProcessor = ( const [reloadTrigger, setReloadTrigger] = useState(0); const alternateBuffer = settings.merged.ui.useAlternateBuffer === true && - !config?.getScreenReader(); + config?.getScreenReader() !== true; const reloadCommands = useCallback(() => { setReloadTrigger((v) => v + 1); @@ -164,15 +164,16 @@ export const useSlashCommandProcessor = ( return new GitService(config.getProjectRoot(), config.storage); }, [config]); - const logger = useMemo(() => { - const l = new Logger( - config?.getSessionId() || '', - config?.storage ?? new Storage(process.cwd()), - ); - // The logger's initialize is async, but we can create the instance - // synchronously. Commands that use it will await its initialization. - return l; - }, [config]); + const logger = useMemo( + () => + // The logger's initialize is async, but we can create the instance + // synchronously. Commands that use it will await its initialization. + new Logger( + config?.getSessionId() ?? '', + config?.storage ?? new Storage(process.cwd()), + ), + [config], + ); /** * Initialize ProfileManager and SubagentManager for command context @@ -181,14 +182,14 @@ export const useSlashCommandProcessor = ( * @requirement:REQ-010 */ const profileManager = useMemo(() => { - if (!config) return undefined; + if (config == null) return undefined; const llxprtDir = path.join(os.homedir(), '.llxprt'); const profilesDir = path.join(llxprtDir, 'profiles'); return new ProfileManager(profilesDir); }, [config]); const subagentManager = useMemo(() => { - if (!config || !profileManager) return undefined; + if (config == null || profileManager == null) return undefined; const llxprtDir = path.join(os.homedir(), '.llxprt'); const subagentsDir = path.join(llxprtDir, 'subagents'); return new SubagentManager(subagentsDir, profileManager); @@ -301,7 +302,6 @@ export const useSlashCommandProcessor = ( subagentManager, // @plan:PLAN-20250117-SUBAGENTCONFIG.P15 @requirement:REQ-010 }, ui: { - addItem, clear: () => { clearItems(); if (!alternateBuffer) { @@ -310,26 +310,27 @@ export const useSlashCommandProcessor = ( } refreshStatic(); }, - loadHistory, setDebugMessage: actions.setDebugMessage, - pendingItem, - setPendingItem, toggleCorgiMode: actions.toggleCorgiMode, toggleDebugProfiler: actions.toggleDebugProfiler, - toggleVimEnabled, setGeminiMdFileCount: setLlxprtMdFileCount, - setLlxprtMdFileCount, updateHistoryTokenCount: session.updateHistoryTokenCount, - reloadCommands, - extensionsUpdateState, dispatchExtensionStateUpdate: actions.dispatchExtensionStateUpdate, addConfirmUpdateExtensionRequest: actions.addConfirmUpdateExtensionRequest, + addItem, + loadHistory, + pendingItem, + setPendingItem, + toggleVimEnabled, + setLlxprtMdFileCount, + reloadCommands, + extensionsUpdateState, }, session: { stats: session.stats, + isProcessing: localIsProcessing, sessionShellAllowlist, - isProcessing: localIsProcessing, // @plan PLAN-20260214-SESSIONBROWSER.P23 }, todoContext, recordingIntegration, @@ -365,7 +366,7 @@ export const useSlashCommandProcessor = ( ); useEffect(() => { - if (!config) { + if (config == null) { return; } @@ -436,7 +437,7 @@ export const useSlashCommandProcessor = ( overwriteConfirmed?: boolean, addToHistory: boolean = true, ): Promise => { - if (!commands) { + if (commands == null) { return false; } if (typeof rawQuery !== 'string') { @@ -474,8 +475,8 @@ export const useSlashCommandProcessor = ( canonicalPath.length > 1 ? canonicalPath.slice(1).join(' ') : undefined; try { - if (commandToExecute) { - if (commandToExecute.action) { + if (commandToExecute != null) { + if (commandToExecute.action != null) { const fullCommandContext: CommandContext = { ...commandContext, invocation: { @@ -488,7 +489,10 @@ export const useSlashCommandProcessor = ( // If a one-time list is provided for a "Proceed" action, temporarily // augment the session allowlist for this single execution. - if (oneTimeShellAllowlist && oneTimeShellAllowlist.size > 0) { + if ( + oneTimeShellAllowlist != null && + oneTimeShellAllowlist.size > 0 + ) { fullCommandContext.session = { ...fullCommandContext.session, sessionShellAllowlist: new Set([ @@ -547,7 +551,7 @@ export const useSlashCommandProcessor = ( return { type: 'handled' }; case 'logging': if ( - result.dialogData && + result.dialogData != null && typeof result.dialogData === 'object' && 'entries' in result.dialogData ) { @@ -578,7 +582,7 @@ export const useSlashCommandProcessor = ( return { type: 'handled' }; case 'profileDetail': if ( - result.dialogData && + result.dialogData != null && typeof result.dialogData === 'object' && 'profileName' in result.dialogData && typeof (result.dialogData as { profileName: unknown }) @@ -596,7 +600,7 @@ export const useSlashCommandProcessor = ( return { type: 'handled' }; case 'profileEditor': if ( - result.dialogData && + result.dialogData != null && typeof result.dialogData === 'object' && 'profileName' in result.dialogData && typeof (result.dialogData as { profileName: unknown }) @@ -675,12 +679,8 @@ export const useSlashCommandProcessor = ( if (typeof part === 'string') { return part; } - if ( - typeof part === 'object' && - part !== null && - 'text' in part - ) { - return (part as { text?: string }).text || ''; + if (typeof part === 'object' && 'text' in part) { + return (part as { text?: string }).text ?? ''; } return ''; }) @@ -748,7 +748,7 @@ export const useSlashCommandProcessor = ( if ( outcome === ToolConfirmationOutcome.Cancel || - !approvedCommands || + approvedCommands == null || approvedCommands.length === 0 ) { addItem( @@ -822,7 +822,7 @@ export const useSlashCommandProcessor = ( * @plan PLAN-20260214-SESSIONBROWSER.P23 * @requirement REQ-PR-001, REQ-PR-002 */ - if (!config) { + if (config == null) { addMessage({ type: MessageType.ERROR, content: @@ -832,7 +832,7 @@ export const useSlashCommandProcessor = ( return { type: 'handled' }; } - if (!recordingSwapCallbacks) { + if (recordingSwapCallbacks == null) { addMessage({ type: MessageType.ERROR, content: @@ -892,7 +892,7 @@ export const useSlashCommandProcessor = ( // Restore IContent[] to gemini client via restoreHistory await config .getGeminiClient() - ?.restoreHistory(resumeResult.history); + .restoreHistory(resumeResult.history); // Convert IContent[] to UI history items and load const uiHistory = iContentToHistoryItems( @@ -915,7 +915,7 @@ export const useSlashCommandProcessor = ( } return { type: 'handled' }; - } else if (commandToExecute.subCommands) { + } else if (commandToExecute.subCommands != null) { const helpText = `Command '/${commandToExecute.name}' requires a subcommand. Available:\n${commandToExecute.subCommands .map((sc) => ` - ${sc.name}: ${sc.description || ''}`) .join('\n')}`; @@ -944,7 +944,7 @@ export const useSlashCommandProcessor = ( return { type: 'handled' }; } catch (e: unknown) { hasError = true; - if (config && commandToExecute) { + if (config != null && commandToExecute != null) { const event = new SlashCommandEvent( commandToExecute.name, subcommand, @@ -962,7 +962,7 @@ export const useSlashCommandProcessor = ( recordingIntegration?.recordSessionEvent('error', errorText); return { type: 'handled' }; } finally { - if (config && commandToExecute && !hasError) { + if (config != null && commandToExecute != null && !hasError) { const event = new SlashCommandEvent( commandToExecute.name, subcommand, diff --git a/packages/cli/src/ui/hooks/useAnimatedScrollbar.ts b/packages/cli/src/ui/hooks/useAnimatedScrollbar.ts index 7432a73b24..c6bf05891e 100644 --- a/packages/cli/src/ui/hooks/useAnimatedScrollbar.ts +++ b/packages/cli/src/ui/hooks/useAnimatedScrollbar.ts @@ -26,11 +26,11 @@ export function useAnimatedScrollbar( debugState.debugNumAnimatedComponents--; isAnimatingRef.current = false; } - if (animationFrame.current) { + if (animationFrame.current != null) { clearInterval(animationFrame.current); animationFrame.current = null; } - if (timeout.current) { + if (timeout.current != null) { clearTimeout(timeout.current); timeout.current = null; } @@ -63,7 +63,7 @@ export function useAnimatedScrollbar( setScrollbarColor(interpolateColor(startColor, focusedColor, progress)); if (progress === 1) { - if (animationFrame.current) { + if (animationFrame.current != null) { clearInterval(animationFrame.current); animationFrame.current = null; } diff --git a/packages/cli/src/ui/hooks/useAtCompletion.test.ts b/packages/cli/src/ui/hooks/useAtCompletion.test.ts index 8c98de195d..8cef6c7d5c 100644 --- a/packages/cli/src/ui/hooks/useAtCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useAtCompletion.test.ts @@ -11,16 +11,16 @@ import { act, useState } from 'react'; import { renderHook, waitFor } from '../../test-utils/render.js'; import { useAtCompletion } from './useAtCompletion.js'; import { - Config, - FileSearch, + type Config, + type FileSearch, FileSearchFactory, } from '@vybestack/llxprt-code-core'; import { createTmpDir, cleanupTmpDir, - FileSystemStructure, + type FileSystemStructure, } from '@vybestack/llxprt-code-test-utils'; -import { Suggestion } from '../components/SuggestionsDisplay.js'; +import type { Suggestion } from '../components/SuggestionsDisplay.js'; // Test harness to capture the state from the hook's callbacks. function useTestHarnessForAtCompletion( @@ -86,7 +86,7 @@ describe('useAtCompletion', () => { expect(result.current.suggestions.length).toBeGreaterThan(0); }); - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'src/', 'src/components/', 'file.txt', @@ -116,7 +116,7 @@ describe('useAtCompletion', () => { expect(result.current.suggestions.length).toBeGreaterThan(0); }); - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'src/', 'src/components/', 'src/index.js', @@ -139,7 +139,7 @@ describe('useAtCompletion', () => { expect(result.current.suggestions.length).toBeGreaterThan(0); }); - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'dir/', 'file.txt', ]); @@ -172,7 +172,7 @@ describe('useAtCompletion', () => { // The hook should find 'cRaZycAsE.txt' even though the pattern is 'CrAzYCaSe'. await waitFor(() => { - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'cRaZycAsE.txt', ]); }); @@ -206,7 +206,7 @@ describe('useAtCompletion', () => { ); await waitFor(() => { - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'a.txt', ]); }); @@ -216,7 +216,7 @@ describe('useAtCompletion', () => { // Wait for the final result await waitFor(() => { - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'b.txt', ]); }); @@ -259,7 +259,7 @@ describe('useAtCompletion', () => { // Wait for the initial search to complete (using real timers) await waitFor(() => { - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'a.txt', ]); }); @@ -282,14 +282,14 @@ describe('useAtCompletion', () => { // Now loading should be true and suggestions should be cleared expect(result.current.isLoadingSuggestions).toBe(true); - expect(result.current.suggestions).toEqual([]); + expect(result.current.suggestions).toStrictEqual([]); // Switch back to real timers for the final waitFor vi.useRealTimers(); // Wait for the search results to be processed await waitFor(() => { - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'b.txt', ]); }); @@ -337,7 +337,9 @@ describe('useAtCompletion', () => { // Wait for the final result, which should be from the second, faster search. await waitFor( () => { - expect(result.current.suggestions.map((s) => s.value)).toEqual(['b']); + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ + 'b', + ]); }, { timeout: 1000 }, ); @@ -363,7 +365,7 @@ describe('useAtCompletion', () => { // Wait for the hook to be ready and have suggestions await waitFor(() => { - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'a.txt', ]); }); @@ -372,7 +374,7 @@ describe('useAtCompletion', () => { rerender({ enabled: false }); // The suggestions should be cleared immediately because of the RESET action - expect(result.current.suggestions).toEqual([]); + expect(result.current.suggestions).toStrictEqual([]); }); it('should reset the state when disabled after being in an ERROR state', async () => { @@ -397,7 +399,7 @@ describe('useAtCompletion', () => { await waitFor(() => { expect(result.current.isLoadingSuggestions).toBe(false); }); - expect(result.current.suggestions).toEqual([]); // No suggestions on error + expect(result.current.suggestions).toStrictEqual([]); // No suggestions on error // Now, disable the hook rerender({ enabled: false }); @@ -405,7 +407,7 @@ describe('useAtCompletion', () => { // The state should still be reset (though visually it's the same) // We can't directly inspect the internal state, but we can ensure it doesn't crash // and the suggestions remain empty. - expect(result.current.suggestions).toEqual([]); + expect(result.current.suggestions).toStrictEqual([]); }); }); @@ -429,7 +431,7 @@ describe('useAtCompletion', () => { expect(result.current.suggestions.length).toBeGreaterThan(0); }); - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'src/', '.gitignore', ]); @@ -450,7 +452,7 @@ describe('useAtCompletion', () => { expect(result.current.suggestions.length).toBeGreaterThan(0); }); - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'node_modules/', 'src/', ]); @@ -475,7 +477,7 @@ describe('useAtCompletion', () => { // Wait for initial suggestions from the first directory await waitFor(() => { - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'file1.txt', ]); }); @@ -488,12 +490,12 @@ describe('useAtCompletion', () => { // After CWD changes, suggestions should be cleared and it should load again. await waitFor(() => { expect(result.current.isLoadingSuggestions).toBe(true); - expect(result.current.suggestions).toEqual([]); + expect(result.current.suggestions).toStrictEqual([]); }); // Wait for the new suggestions from the second directory await waitFor(() => { - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'file2.txt', ]); }); @@ -535,7 +537,7 @@ describe('useAtCompletion', () => { }); // Should only contain top-level items - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'src/', 'file.txt', ]); @@ -600,7 +602,7 @@ describe('useAtCompletion', () => { (s) => s.description === 'subagent', ); expect(subagentSuggestions.length).toBe(3); - expect(subagentSuggestions.map((s) => s.value)).toEqual( + expect(subagentSuggestions.map((s) => s.value)).toStrictEqual( expect.arrayContaining([ 'codeanalyzer', 'deepthinker', diff --git a/packages/cli/src/ui/hooks/useAtCompletion.ts b/packages/cli/src/ui/hooks/useAtCompletion.ts index 2add72567b..526d045a72 100644 --- a/packages/cli/src/ui/hooks/useAtCompletion.ts +++ b/packages/cli/src/ui/hooks/useAtCompletion.ts @@ -8,13 +8,13 @@ import { useEffect, useReducer, useRef } from 'react'; import { setTimeout as setTimeoutPromise } from 'node:timers/promises'; import { AsyncFzf } from 'fzf'; import { - Config, - FileSearch, + type Config, + type FileSearch, FileSearchFactory, escapePath, } from '@vybestack/llxprt-code-core'; import { - Suggestion, + type Suggestion, MAX_SUGGESTIONS_TO_SHOW, } from '../components/SuggestionsDisplay.js'; @@ -111,19 +111,26 @@ interface SubagentSuggestionCandidate { function buildResourceCandidates( config?: Config, ): ResourceSuggestionCandidate[] { - const registry = ( - config as Config & { - getResourceRegistry?: () => { - getAllResources: () => Array<{ - serverName: string; - uri: string; - name?: string; - }>; - }; - } - )?.getResourceRegistry?.(); + if (config == null) { + return []; + } + + const configWithRegistry = config as Config & { + getResourceRegistry?: () => { + getAllResources: () => Array<{ + serverName: string; + uri: string; + name?: string; + }>; + }; + }; + + const registry = + typeof configWithRegistry.getResourceRegistry === 'function' + ? configWithRegistry.getResourceRegistry() + : undefined; - if (!registry) { + if (registry == null) { return []; } @@ -133,7 +140,9 @@ function buildResourceCandidates( .map((resource) => { const prefixedUri = `${resource.serverName}:${resource.uri}`; return { - searchKey: `${prefixedUri} ${resource.name ?? ''}`.toLowerCase(), + searchKey: + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- name is optional + `${prefixedUri} ${resource.name ?? ''}`.toLowerCase(), suggestion: { label: prefixedUri, value: prefixedUri, @@ -145,8 +154,8 @@ function buildResourceCandidates( async function buildSubagentCandidates( config?: Config, ): Promise { - const subagentManager = config?.getSubagentManager?.(); - if (!subagentManager) { + const subagentManager = config?.getSubagentManager(); + if (subagentManager == null) { return []; } @@ -266,6 +275,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void { } return; } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive runtime guard if (pattern === null) { dispatch({ type: 'RESET' }); return; @@ -290,9 +300,9 @@ export function useAtCompletion(props: UseAtCompletionProps): void { projectRoot: cwd, ignoreDirs: [], useGitignore: - config?.getFileFilteringOptions()?.respectGitIgnore ?? true, + config?.getFileFilteringOptions().respectGitIgnore ?? true, useGeminiignore: - config?.getFileFilteringOptions()?.respectGitIgnore ?? true, + config?.getFileFilteringOptions().respectGitIgnore ?? true, cache: true, cacheTtl: 30, // 30 seconds enableRecursiveFileSearch: @@ -300,7 +310,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void { enableFuzzySearch: !( config?.getFileFilteringDisableFuzzySearch() ?? false ), - maxFiles: config?.getFileFilteringOptions()?.maxFileCount, + maxFiles: config?.getFileFilteringOptions().maxFileCount, }); await searcher.initialize(); fileSearch.current = searcher; @@ -314,11 +324,11 @@ export function useAtCompletion(props: UseAtCompletionProps): void { }; const search = async () => { - if (!fileSearch.current || state.pattern === null) { + if (fileSearch.current == null || state.pattern === null) { return; } - if (slowSearchTimer.current) { + if (slowSearchTimer.current != null) { clearTimeout(slowSearchTimer.current); } @@ -330,7 +340,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void { }, 200); const timeoutMs = - config?.getFileFilteringOptions()?.searchTimeout ?? + config?.getFileFilteringOptions().searchTimeout ?? DEFAULT_SEARCH_TIMEOUT_MS; // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -351,6 +361,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void { maxResults: MAX_SUGGESTIONS_TO_SHOW * 3, }); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions -- defensive: ref may be cleared across await if (slowSearchTimer.current) { clearTimeout(slowSearchTimer.current); } @@ -366,13 +377,13 @@ export function useAtCompletion(props: UseAtCompletionProps): void { const resourceCandidates = buildResourceCandidates(config); const resourceSuggestions = await searchResourceCandidates( - state.pattern ?? '', + state.pattern, resourceCandidates, ); const subagentCandidates = await buildSubagentCandidates(config); const subagentSuggestions = await searchSubagentCandidates( - state.pattern ?? '', + state.pattern, subagentCandidates, ); @@ -401,7 +412,7 @@ export function useAtCompletion(props: UseAtCompletionProps): void { return () => { searchAbortController.current?.abort(); - if (slowSearchTimer.current) { + if (slowSearchTimer.current != null) { clearTimeout(slowSearchTimer.current); } }; diff --git a/packages/cli/src/ui/hooks/useAuthCommand.ts b/packages/cli/src/ui/hooks/useAuthCommand.ts index 00ecbd7fd2..4fcf0419bf 100644 --- a/packages/cli/src/ui/hooks/useAuthCommand.ts +++ b/packages/cli/src/ui/hooks/useAuthCommand.ts @@ -5,12 +5,12 @@ */ import { useCallback } from 'react'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; +import type { LoadedSettings, SettingScope } from '../../config/settings.js'; import { useAppDispatch } from '../contexts/AppDispatchContext.js'; -import { AppState } from '../reducers/appReducer.js'; +import type { AppState } from '../reducers/appReducer.js'; export const useAuthCommand = ( - settings: LoadedSettings, + _settings: LoadedSettings, appState: AppState, ) => { const appDispatch = useAppDispatch(); diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.test.ts b/packages/cli/src/ui/hooks/useCommandCompletion.test.ts index 04b53f2bc5..1f5c338fc4 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useCommandCompletion.test.ts @@ -10,16 +10,15 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { act, useEffect, useState, useCallback } from 'react'; import { renderHook, waitFor } from '../../test-utils/render.js'; import { useCommandCompletion } from './useCommandCompletion.js'; -import { CommandContext } from '../commands/types.js'; -import { Config } from '@vybestack/llxprt-code-core'; +import type { CommandContext } from '../commands/types.js'; +import type { Config } from '@vybestack/llxprt-code-core'; import { useTextBuffer } from '../components/shared/text-buffer.js'; -import { Suggestion } from '../components/SuggestionsDisplay.js'; -import { UseAtCompletionProps, useAtCompletion } from './useAtCompletion.js'; +import type { Suggestion } from '../components/SuggestionsDisplay.js'; import { - UseSlashCompletionProps as _UseSlashCompletionProps, - useSlashCompletion, -} from './useSlashCompletion.js'; -import { useCompletion as _useCompletion } from './useCompletion.js'; + type UseAtCompletionProps, + useAtCompletion, +} from './useAtCompletion.js'; +import { useSlashCompletion } from './useSlashCompletion.js'; vi.mock('./useAtCompletion', () => ({ useAtCompletion: vi.fn(), @@ -264,7 +263,7 @@ describe('useCommandCompletion', () => { ), ); - expect(result.current.suggestions).toEqual([]); + expect(result.current.suggestions).toStrictEqual([]); expect(result.current.activeSuggestionIndex).toBe(-1); expect(result.current.visibleStartIndex).toBe(0); expect(result.current.showSuggestions).toBe(false); diff --git a/packages/cli/src/ui/hooks/useCommandCompletion.tsx b/packages/cli/src/ui/hooks/useCommandCompletion.tsx index 08c0eedecf..1b01346ce6 100644 --- a/packages/cli/src/ui/hooks/useCommandCompletion.tsx +++ b/packages/cli/src/ui/hooks/useCommandCompletion.tsx @@ -5,11 +5,11 @@ */ import { useCallback, useMemo, useEffect } from 'react'; -import { Suggestion } from '../components/SuggestionsDisplay.js'; -import { CommandContext, SlashCommand } from '../commands/types.js'; +import type { Suggestion } from '../components/SuggestionsDisplay.js'; +import type { CommandContext, SlashCommand } from '../commands/types.js'; import { logicalPosToOffset, - TextBuffer, + type TextBuffer, } from '../components/shared/text-buffer.js'; import { isSlashCommand } from '../utils/commandUtils.js'; import { toCodePoints } from '../utils/textUtils.js'; @@ -17,10 +17,10 @@ import { useAtCompletion } from './useAtCompletion.js'; import { useSlashCompletion } from './useSlashCompletion.js'; import { usePromptCompletion, - PromptCompletion, + type PromptCompletion, PROMPT_COMPLETION_MIN_LENGTH, } from './usePromptCompletion.js'; -import { Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; import { useCompletion } from './useCompletion.js'; export enum CompletionMode { @@ -170,7 +170,7 @@ export function useCommandCompletion( useAtCompletion({ enabled: completionMode === CompletionMode.AT, - pattern: query || '', + pattern: query ?? '', config, cwd, setSuggestions, @@ -301,22 +301,22 @@ export function useCommandCompletion( } return { + activeHint: '', + getCommandFromSuggestion: () => null, + isArgumentCompletion: false, + leafCommand: null, suggestions, activeSuggestionIndex, visibleStartIndex, showSuggestions, isLoadingSuggestions, isPerfectMatch, - activeHint: '', setActiveSuggestionIndex, setShowSuggestions, resetCompletionState, navigateUp, navigateDown, handleAutocomplete, - getCommandFromSuggestion: () => null, // Not in slash mode, so no command mapping - isArgumentCompletion: false, - leafCommand: null, promptCompletion, }; } diff --git a/packages/cli/src/ui/hooks/useCompletion.ts b/packages/cli/src/ui/hooks/useCompletion.ts index a77915aba0..8abcfeddf4 100644 --- a/packages/cli/src/ui/hooks/useCompletion.ts +++ b/packages/cli/src/ui/hooks/useCompletion.ts @@ -8,7 +8,7 @@ import { useState, useCallback } from 'react'; import { MAX_SUGGESTIONS_TO_SHOW, - Suggestion, + type Suggestion, } from '../components/SuggestionsDisplay.js'; export interface UseCompletionReturn { diff --git a/packages/cli/src/ui/hooks/useConsoleMessages.test.tsx b/packages/cli/src/ui/hooks/useConsoleMessages.test.tsx index 1cb51e45dd..4e4f934e7d 100644 --- a/packages/cli/src/ui/hooks/useConsoleMessages.test.tsx +++ b/packages/cli/src/ui/hooks/useConsoleMessages.test.tsx @@ -24,13 +24,13 @@ describe('useConsoleMessages', () => { const { handleNewMessage, ...rest } = useConsoleMessages(); const log = useCallback( (content: string) => { - handleNewMessage({ type: 'log', content, count: 1 }); + handleNewMessage({ type: 'log', count: 1, content }); }, [handleNewMessage], ); const error = useCallback( (content: string) => { - handleNewMessage({ type: 'error', content, count: 1 }); + handleNewMessage({ type: 'error', count: 1, content }); }, [handleNewMessage], ); @@ -61,7 +61,7 @@ describe('useConsoleMessages', () => { it('should initialize with an empty array of console messages', () => { const { result } = renderConsoleMessagesHook(); - expect(result.current.consoleMessages).toEqual([]); + expect(result.current.consoleMessages).toStrictEqual([]); }); it('should add a new message when log is called', async () => { @@ -75,7 +75,7 @@ describe('useConsoleMessages', () => { await vi.advanceTimersByTimeAsync(20); }); - expect(result.current.consoleMessages).toEqual([ + expect(result.current.consoleMessages).toStrictEqual([ { type: 'log', content: 'Test message', count: 1 }, ]); }); @@ -93,7 +93,7 @@ describe('useConsoleMessages', () => { await vi.advanceTimersByTimeAsync(20); }); - expect(result.current.consoleMessages).toEqual([ + expect(result.current.consoleMessages).toStrictEqual([ { type: 'log', content: 'Test message', count: 3 }, ]); }); @@ -110,7 +110,7 @@ describe('useConsoleMessages', () => { await vi.advanceTimersByTimeAsync(20); }); - expect(result.current.consoleMessages).toEqual([ + expect(result.current.consoleMessages).toStrictEqual([ { type: 'log', content: 'First message', count: 1 }, { type: 'error', content: 'Second message', count: 1 }, ]); diff --git a/packages/cli/src/ui/hooks/useConsoleMessages.ts b/packages/cli/src/ui/hooks/useConsoleMessages.ts index 9e988dc019..0276bb194d 100644 --- a/packages/cli/src/ui/hooks/useConsoleMessages.ts +++ b/packages/cli/src/ui/hooks/useConsoleMessages.ts @@ -11,7 +11,7 @@ import { useRef, useTransition, } from 'react'; -import { ConsoleMessageItem } from '../types.js'; +import type { ConsoleMessageItem } from '../types.js'; // Maximum console messages to keep in memory to prevent unbounded growth const MAX_CONSOLE_MESSAGES = 1000; @@ -84,7 +84,7 @@ export function useConsoleMessages(): UseConsoleMessagesReturn { const handleNewMessage = useCallback( (message: ConsoleMessageItem) => { messageQueueRef.current.push(message); - if (!timeoutRef.current) { + if (timeoutRef.current == null) { // Batch updates using a timeout. 16ms is a reasonable delay to batch // rapid-fire messages without noticeable lag. timeoutRef.current = setTimeout(processQueue, 16); @@ -94,7 +94,7 @@ export function useConsoleMessages(): UseConsoleMessagesReturn { ); const clearConsoleMessages = useCallback(() => { - if (timeoutRef.current) { + if (timeoutRef.current != null) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } @@ -107,7 +107,7 @@ export function useConsoleMessages(): UseConsoleMessagesReturn { // Cleanup on unmount useEffect( () => () => { - if (timeoutRef.current) { + if (timeoutRef.current != null) { clearTimeout(timeoutRef.current); } }, diff --git a/packages/cli/src/ui/hooks/useCreateProfileDialog.ts b/packages/cli/src/ui/hooks/useCreateProfileDialog.ts index c7fbf2f4a2..4fef1cd01a 100644 --- a/packages/cli/src/ui/hooks/useCreateProfileDialog.ts +++ b/packages/cli/src/ui/hooks/useCreateProfileDialog.ts @@ -7,7 +7,7 @@ import { useCallback, useState } from 'react'; import { useAppDispatch } from '../contexts/AppDispatchContext.js'; import { useRuntimeApi } from '../contexts/RuntimeContext.js'; -import { AppState } from '../reducers/appReducer.js'; +import type { AppState } from '../reducers/appReducer.js'; interface UseCreateProfileDialogParams { appState: AppState; diff --git a/packages/cli/src/ui/hooks/useEditorSettings.test.tsx b/packages/cli/src/ui/hooks/useEditorSettings.test.tsx index cdd5012fb2..b6dea0fc7b 100644 --- a/packages/cli/src/ui/hooks/useEditorSettings.test.tsx +++ b/packages/cli/src/ui/hooks/useEditorSettings.test.tsx @@ -13,10 +13,11 @@ import { vi, type MockedFunction, } from 'vitest'; -import React, { act } from 'react'; +import type React from 'react'; +import { act } from 'react'; import { renderHook } from '../../test-utils/render.js'; import { useEditorSettings } from './useEditorSettings.js'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; +import { type LoadedSettings, SettingScope } from '../../config/settings.js'; import { MessageType, type HistoryItem } from '../types.js'; import { type EditorType, @@ -24,7 +25,7 @@ import { allowEditorTypeInSandbox, } from '@vybestack/llxprt-code-core'; import { AppDispatchProvider } from '../contexts/AppDispatchContext.js'; -import { type AppState, type AppAction } from '../reducers/appReducer.js'; +import type { AppState, AppAction } from '../reducers/appReducer.js'; import { SettingPaths } from '../../config/settingPaths.js'; diff --git a/packages/cli/src/ui/hooks/useEditorSettings.ts b/packages/cli/src/ui/hooks/useEditorSettings.ts index acbaf143a8..84049a1e2b 100644 --- a/packages/cli/src/ui/hooks/useEditorSettings.ts +++ b/packages/cli/src/ui/hooks/useEditorSettings.ts @@ -5,15 +5,15 @@ */ import { useCallback } from 'react'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; +import type { LoadedSettings, SettingScope } from '../../config/settings.js'; import { type HistoryItem, MessageType } from '../types.js'; import { allowEditorTypeInSandbox, checkHasEditorType, - EditorType, + type EditorType, } from '@vybestack/llxprt-code-core'; import { useAppDispatch } from '../contexts/AppDispatchContext.js'; -import { AppState } from '../reducers/appReducer.js'; +import type { AppState } from '../reducers/appReducer.js'; import { SettingPaths } from '../../config/settingPaths.js'; diff --git a/packages/cli/src/ui/hooks/useExtensionUpdates.ts b/packages/cli/src/ui/hooks/useExtensionUpdates.ts index f366662c39..8c3a84df81 100644 --- a/packages/cli/src/ui/hooks/useExtensionUpdates.ts +++ b/packages/cli/src/ui/hooks/useExtensionUpdates.ts @@ -87,7 +87,7 @@ export const useExtensionUpdates = ( const currentStatus = extensionsUpdateState.extensionStatuses.get( extension.name, ); - if (!currentStatus) return true; + if (currentStatus == null) return true; const currentState = currentStatus.status; return !currentState || currentState === ExtensionUpdateState.UNKNOWN; }); @@ -109,21 +109,20 @@ export const useExtensionUpdates = ( return; } const scheduledUpdate = extensionsUpdateState.scheduledUpdate; - if (scheduledUpdate) { + if (scheduledUpdate != null) { dispatchExtensionStateUpdate({ type: 'CLEAR_SCHEDULED_UPDATE', }); } function shouldDoUpdate(extension: GeminiCLIExtension): boolean { - if (scheduledUpdate) { + if (scheduledUpdate != null) { if (scheduledUpdate.all) { return true; } return scheduledUpdate.names?.includes(extension.name) === true; - } else { - return extension.installMetadata?.autoUpdate === true; } + return extension.installMetadata?.autoUpdate === true; } const pendingUpdates: string[] = []; @@ -133,7 +132,7 @@ export const useExtensionUpdates = ( extension.name, ); if ( - !currentState || + currentState == null || currentState.status !== ExtensionUpdateState.UPDATE_AVAILABLE ) { continue; @@ -163,7 +162,7 @@ export const useExtensionUpdates = ( updatePromises.push(updatePromise); updatePromise .then((result) => { - if (!result) return; + if (result == null) return; addItem( { type: MessageType.INFO, @@ -193,7 +192,7 @@ export const useExtensionUpdates = ( Date.now(), ); } - if (scheduledUpdate) { + if (scheduledUpdate != null) { void Promise.allSettled(updatePromises).then((results) => { const nonNullResults = results .filter( diff --git a/packages/cli/src/ui/hooks/useFlickerDetector.ts b/packages/cli/src/ui/hooks/useFlickerDetector.ts index 9dbf9b2557..614f934a1d 100644 --- a/packages/cli/src/ui/hooks/useFlickerDetector.ts +++ b/packages/cli/src/ui/hooks/useFlickerDetector.ts @@ -29,13 +29,13 @@ export function useFlickerDetector( constrainHeight: boolean, ): void { useEffect(() => { - if (rootUiRef.current) { + if (rootUiRef.current != null) { const measurement = measureElement(rootUiRef.current); if (measurement.height > terminalHeight && constrainHeight) { appEvents.emit(AppEvent.Flicker, { contentHeight: measurement.height, - terminalHeight, overflow: measurement.height - terminalHeight, + terminalHeight, }); } } diff --git a/packages/cli/src/ui/hooks/useFolderTrust.test.ts b/packages/cli/src/ui/hooks/useFolderTrust.test.ts index 93012d6ead..658460f73a 100644 --- a/packages/cli/src/ui/hooks/useFolderTrust.test.ts +++ b/packages/cli/src/ui/hooks/useFolderTrust.test.ts @@ -8,11 +8,11 @@ import { vi } from 'vitest'; import { renderHook } from '../../test-utils/render.js'; import { act } from 'react'; import { useFolderTrust } from './useFolderTrust.js'; -import { LoadedSettings } from '../../config/settings.js'; +import type { LoadedSettings } from '../../config/settings.js'; import { FolderTrustChoice } from '../components/FolderTrustDialog.js'; import type { LoadedTrustedFolders } from '../../config/trustedFolders.js'; import { TrustLevel } from '../../config/trustedFolders.js'; -import { Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; import * as trustedFolders from '../../config/trustedFolders.js'; const mockedCwd = vi.hoisted(() => vi.fn()); diff --git a/packages/cli/src/ui/hooks/useFolderTrust.ts b/packages/cli/src/ui/hooks/useFolderTrust.ts index 9355ad2cf9..2510399675 100644 --- a/packages/cli/src/ui/hooks/useFolderTrust.ts +++ b/packages/cli/src/ui/hooks/useFolderTrust.ts @@ -10,7 +10,7 @@ import { DebugLogger, ExitCodes, } from '@vybestack/llxprt-code-core'; -import { LoadedSettings, Settings } from '../../config/settings.js'; +import type { LoadedSettings, Settings } from '../../config/settings.js'; import { FolderTrustChoice } from '../components/FolderTrustDialog.js'; import { loadTrustedFolders, @@ -54,7 +54,7 @@ export const useFolderTrust = ( debug.log( 'Folder is untrusted - displaying permissions command hint on startup', ); - if (addItem) { + if (addItem != null) { addItem( { type: MessageType.INFO, @@ -92,7 +92,7 @@ export const useFolderTrust = ( try { trustedFolders.setValue(cwd, trustLevel); } catch (_e) { - if (addItem) { + if (addItem != null) { addItem( { type: MessageType.ERROR, diff --git a/packages/cli/src/ui/hooks/useGeminiStream.dedup.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.dedup.test.tsx index 101d33d04e..0f765432a4 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.dedup.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.dedup.test.tsx @@ -17,15 +17,15 @@ import { renderHook } from '../../test-utils/render.js'; import { act } from 'react'; import { useGeminiStream } from './geminiStream/index.js'; import { - Config, - GeminiClient, + type Config, + type GeminiClient, GeminiEventType, - ToolCallRequestInfo, - ToolRegistry, + type ToolCallRequestInfo, + type ToolRegistry, ApprovalMode, DebugLogger, } from '@vybestack/llxprt-code-core'; -import { LoadedSettings } from '../../config/settings.js'; +import type { LoadedSettings } from '../../config/settings.js'; import type { UseHistoryManagerReturn } from './useHistoryManager.js'; import { ToolCallStatus, diff --git a/packages/cli/src/ui/hooks/useGeminiStream.subagent.spec.tsx b/packages/cli/src/ui/hooks/useGeminiStream.subagent.spec.tsx index 0f6d97257c..39f6479689 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.subagent.spec.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.subagent.spec.tsx @@ -5,26 +5,26 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { describe, it, expect, vi, Mock, beforeEach } from 'vitest'; +import { describe, it, expect, vi, type Mock, beforeEach } from 'vitest'; import React, { act } from 'react'; import { renderHook, waitFor } from '../../test-utils/render.js'; import * as ReactDOM from 'react-dom'; import { useGeminiStream } from './geminiStream/index.js'; import { useReactToolScheduler, - TrackedCompletedToolCall, - TrackedToolCall, + type TrackedCompletedToolCall, + type TrackedToolCall, } from './useReactToolScheduler.js'; -import { +import type { Config, GeminiClient, EditorType, AnyToolInvocation, } from '@vybestack/llxprt-code-core'; -import { Part, PartListUnion } from '@google/genai'; -import { LoadedSettings } from '../../config/settings.js'; -import { UseHistoryManagerReturn } from './useHistoryManager.js'; -import { SlashCommandProcessorResult } from '../types.js'; +import type { Part, PartListUnion } from '@google/genai'; +import type { LoadedSettings } from '../../config/settings.js'; +import type { UseHistoryManagerReturn } from './useHistoryManager.js'; +import type { SlashCommandProcessorResult } from '../types.js'; const inkMock = vi.hoisted(() => { const noop = vi.fn(() => null); @@ -50,8 +50,8 @@ const inkMock = vi.hoisted(() => { removeListener: vi.fn(), off: vi.fn(), }, - setRawMode, isRawModeSupported: true, + setRawMode, })), useIsScreenReaderEnabled: vi.fn(() => false), measureElement: vi.fn(() => ({ width: 0, height: 0 })), @@ -168,10 +168,9 @@ describe('useGeminiStream subagent isolation', () => { vertexai: false, }; - const mockGetGeminiClient = vi.fn().mockImplementation(() => { - const clientInstance = new MockedGeminiClientClass(mockConfig); - return clientInstance; - }); + const mockGetGeminiClient = vi + .fn() + .mockImplementation(() => new MockedGeminiClientClass(mockConfig)); mockConfig = { apiKey: 'test-api-key', @@ -312,7 +311,7 @@ describe('useGeminiStream subagent isolation', () => { }; await act(async () => { - if (capturedOnComplete) { + if (capturedOnComplete != null) { await capturedOnComplete(Symbol('scheduler'), [subagentToolCall], { isPrimary: true, }); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx index 2134054a1c..8b5d6f6cc9 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.test.tsx @@ -154,12 +154,12 @@ describe('useGeminiStream', () => { mockAddItem = vi.fn(); // Define the mock for getGeminiClient - const _mockGetGeminiClient = vi.fn().mockImplementation(() => { - // MockedGeminiClientClass is defined in the module scope by the previous change. - // It will use the mockStartChat and mockSendMessageStream that are managed within beforeEach. - const clientInstance = new MockedGeminiClientClass(mockConfig); - return clientInstance; - }); + const _mockGetGeminiClient = vi.fn().mockImplementation( + () => + // MockedGeminiClientClass is defined in the module scope by the previous change. + // It will use the mockStartChat and mockSendMessageStream that are managed within beforeEach. + new MockedGeminiClientClass(mockConfig), + ); const contentGeneratorConfig = { model: 'test-model', @@ -580,7 +580,7 @@ describe('useGeminiStream', () => { // Trigger the onComplete callback with completed tools await act(async () => { - if (capturedOnComplete) { + if (capturedOnComplete != null) { await capturedOnComplete(completedToolCalls); } }); @@ -675,7 +675,7 @@ describe('useGeminiStream', () => { ); await act(async () => { - if (capturedOnComplete) { + if (capturedOnComplete != null) { await capturedOnComplete(completedToolCalls); } }); @@ -764,7 +764,7 @@ describe('useGeminiStream', () => { // Trigger the onComplete callback with cancelled tools await act(async () => { - if (capturedOnComplete) { + if (capturedOnComplete != null) { await capturedOnComplete(cancelledToolCalls); } }); @@ -875,7 +875,7 @@ describe('useGeminiStream', () => { // Trigger the onComplete callback with multiple cancelled tools await act(async () => { - if (capturedOnComplete) { + if (capturedOnComplete != null) { await capturedOnComplete(allCancelledTools); } }); @@ -1011,7 +1011,7 @@ describe('useGeminiStream', () => { // 4. Trigger the onComplete callback to simulate tool completion await act(async () => { - if (capturedOnComplete) { + if (capturedOnComplete != null) { await capturedOnComplete(completedToolCalls); } }); @@ -1570,7 +1570,7 @@ describe('useGeminiStream', () => { // Trigger the onComplete callback with the completed save_memory tool await act(async () => { - if (capturedOnComplete) { + if (capturedOnComplete != null) { await capturedOnComplete([completedToolCall]); } }); @@ -2130,7 +2130,7 @@ describe('useGeminiStream', () => { const expectedMessage = shouldAddMessage ? { type: 'info', text: message } : undefined; - expect(infoMessages[0]?.[0]).toEqual(expectedMessage); + expect(infoMessages[0]?.[0]).toStrictEqual(expectedMessage); }, ); }); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.thinking.test.tsx b/packages/cli/src/ui/hooks/useGeminiStream.thinking.test.tsx index 22e2e1e506..13f75786f4 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.thinking.test.tsx +++ b/packages/cli/src/ui/hooks/useGeminiStream.thinking.test.tsx @@ -11,24 +11,28 @@ */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import React, { act } from 'react'; import { renderHook, waitFor } from '../../test-utils/render.js'; import * as ReactDOM from 'react-dom'; import { useGeminiStream } from './geminiStream/index.js'; import { useReactToolScheduler, - TrackedToolCall, + type TrackedToolCall, } from './useReactToolScheduler.js'; import { - Config, - EditorType, + type Config, + type EditorType, GeminiEventType as ServerGeminiEventType, } from '@vybestack/llxprt-code-core'; import { FinishReason } from '@google/genai'; -import { UseHistoryManagerReturn } from './useHistoryManager.js'; -import { MessageType, HistoryItemGemini, StreamingState } from '../types.js'; -import { LoadedSettings } from '../../config/settings.js'; +import type { UseHistoryManagerReturn } from './useHistoryManager.js'; +import { + MessageType, + type HistoryItemGemini, + StreamingState, +} from '../types.js'; +import type { LoadedSettings } from '../../config/settings.js'; const inkMock = vi.hoisted(() => { const write = vi.fn(); @@ -58,8 +62,8 @@ const inkMock = vi.hoisted(() => { removeListener: vi.fn(), off: vi.fn(), }, - setRawMode, isRawModeSupported: true, + setRawMode, })), useIsScreenReaderEnabled: vi.fn(() => false), measureElement: vi.fn(() => ({ width: 0, height: 0 })), @@ -222,10 +226,9 @@ describe('useGeminiStream - ThinkingBlock Integration', () => { mockAddItem = vi.fn(); - const mockGetGeminiClient = vi.fn().mockImplementation(() => { - const clientInstance = new MockedGeminiClientClass(mockConfig); - return clientInstance; - }); + const mockGetGeminiClient = vi + .fn() + .mockImplementation(() => new MockedGeminiClientClass(mockConfig)); const contentGeneratorConfig = { model: 'test-model', @@ -344,7 +347,7 @@ describe('useGeminiStream - ThinkingBlock Integration', () => { loadedSettings: LoadedSettings; toolCalls?: TrackedToolCall[]; }) => { - if (props.toolCalls) { + if (props.toolCalls != null) { setToolCalls(props.toolCalls); } return useGeminiStream( @@ -445,7 +448,7 @@ describe('useGeminiStream - ThinkingBlock Integration', () => { }); const secondThoughtValue = result.current.thought; - expect(secondThoughtValue).not.toEqual(firstThoughtValue); + expect(secondThoughtValue).not.toStrictEqual(firstThoughtValue); }); it('should replace thinking content on subsequent Thought events (not append)', async () => { diff --git a/packages/cli/src/ui/hooks/useGitBranchName.test.ts b/packages/cli/src/ui/hooks/useGitBranchName.test.ts index e8f9c5479b..64e9a3b9ec 100644 --- a/packages/cli/src/ui/hooks/useGitBranchName.test.ts +++ b/packages/cli/src/ui/hooks/useGitBranchName.test.ts @@ -11,7 +11,7 @@ import { expect, it, vi, - MockedFunction, + type MockedFunction, } from 'vitest'; import { act } from 'react'; import { renderHook, waitFor } from '../../test-utils/render.js'; @@ -125,7 +125,7 @@ describe('useGitBranchName', () => { let watchCallback: ((eventType: string) => void) | null = null; const watchSpy = vi .mocked(fs.watch) - .mockImplementation((path, callback) => { + .mockImplementation((_path, callback) => { watchCallback = callback as (eventType: string) => void; return mockWatcher as unknown as fs.FSWatcher; }); diff --git a/packages/cli/src/ui/hooks/useGitBranchName.ts b/packages/cli/src/ui/hooks/useGitBranchName.ts index 6c9affb226..b559da1e3b 100644 --- a/packages/cli/src/ui/hooks/useGitBranchName.ts +++ b/packages/cli/src/ui/hooks/useGitBranchName.ts @@ -19,7 +19,7 @@ export function useGitBranchName(cwd: string): string | undefined { 'git rev-parse --abbrev-ref HEAD', { cwd }, (error, stdout, _stderr) => { - if (error) { + if (error != null) { setBranchName(undefined); return; } @@ -31,7 +31,7 @@ export function useGitBranchName(cwd: string): string | undefined { 'git rev-parse --short HEAD', { cwd }, (error, stdout, _stderr) => { - if (error) { + if (error != null) { setBranchName(undefined); return; } diff --git a/packages/cli/src/ui/hooks/useHistoryManager.test.ts b/packages/cli/src/ui/hooks/useHistoryManager.test.ts index 59cf57f1ba..9716004a19 100644 --- a/packages/cli/src/ui/hooks/useHistoryManager.test.ts +++ b/packages/cli/src/ui/hooks/useHistoryManager.test.ts @@ -8,12 +8,12 @@ import { describe, it, expect } from 'vitest'; import { renderHook } from '../../test-utils/render.js'; import { act } from 'react'; import { useHistory } from './useHistoryManager.js'; -import { HistoryItem } from '../types.js'; +import type { HistoryItem } from '../types.js'; describe('useHistoryManager', () => { it('should initialize with an empty history', () => { const { result } = renderHook(() => useHistory()); - expect(result.current.history).toEqual([]); + expect(result.current.history).toStrictEqual([]); }); it('should add an item to history with a unique ID', () => { @@ -29,7 +29,7 @@ describe('useHistoryManager', () => { }); expect(result.current.history).toHaveLength(1); - expect(result.current.history[0]).toEqual( + expect(result.current.history[0]).toStrictEqual( expect.objectContaining({ ...itemData, id: expect.any(Number), @@ -60,9 +60,9 @@ describe('useHistoryManager', () => { }); expect(result.current.history).toHaveLength(2); - expect(id1).not.toEqual(id2); - expect(result.current.history[0].id).toEqual(id1); - expect(result.current.history[1].id).toEqual(id2); + expect(id1).not.toStrictEqual(id2); + expect(result.current.history[0].id).toStrictEqual(id1); + expect(result.current.history[1].id).toStrictEqual(id2); // IDs should be sequential based on the counter expect(id2).toBeGreaterThan(id1); }); @@ -86,7 +86,7 @@ describe('useHistoryManager', () => { }); expect(result.current.history).toHaveLength(1); - expect(result.current.history[0]).toEqual({ + expect(result.current.history[0]).toStrictEqual({ ...initialItem, id: itemId, text: updatedText, @@ -111,7 +111,7 @@ describe('useHistoryManager', () => { result.current.updateItem(99999, { text: 'Should not apply' }); // Nonexistent ID }); - expect(result.current.history).toEqual(originalHistory); + expect(result.current.history).toStrictEqual(originalHistory); }); it('should clear the history', () => { @@ -137,7 +137,7 @@ describe('useHistoryManager', () => { result.current.clearItems(); }); - expect(result.current.history).toEqual([]); + expect(result.current.history).toStrictEqual([]); }); it('should not add consecutive duplicate user messages', () => { diff --git a/packages/cli/src/ui/hooks/useHookDisplayState.test.tsx b/packages/cli/src/ui/hooks/useHookDisplayState.test.tsx index c87c7dd391..61967d0fe9 100644 --- a/packages/cli/src/ui/hooks/useHookDisplayState.test.tsx +++ b/packages/cli/src/ui/hooks/useHookDisplayState.test.tsx @@ -39,7 +39,7 @@ describe('useHookDisplayState', () => { it('returns an empty array when no hooks are active', () => { const bus = createTestMessageBus(); const { result } = renderHook(() => useHookDisplayState(bus)); - expect(result.current).toEqual([]); + expect(result.current).toStrictEqual([]); }); it('adds an active hook on request event', () => { @@ -106,7 +106,7 @@ describe('useHookDisplayState', () => { }); it('returns empty when no messageBus is provided', () => { - const { result } = renderHook(() => useHookDisplayState(undefined)); - expect(result.current).toEqual([]); + const { result } = renderHook(() => useHookDisplayState()); + expect(result.current).toStrictEqual([]); }); }); diff --git a/packages/cli/src/ui/hooks/useHookDisplayState.ts b/packages/cli/src/ui/hooks/useHookDisplayState.ts index 2d6beb1c27..bba40cbae3 100644 --- a/packages/cli/src/ui/hooks/useHookDisplayState.ts +++ b/packages/cli/src/ui/hooks/useHookDisplayState.ts @@ -23,7 +23,7 @@ export function useHookDisplayState(messageBus?: MessageBus): ActiveHook[] { const [activeHooks, setActiveHooks] = useState([]); useEffect(() => { - if (!messageBus) { + if (messageBus == null) { return; } diff --git a/packages/cli/src/ui/hooks/useIdeTrustListener.ts b/packages/cli/src/ui/hooks/useIdeTrustListener.ts index f3954fb4fa..757faf3245 100644 --- a/packages/cli/src/ui/hooks/useIdeTrustListener.ts +++ b/packages/cli/src/ui/hooks/useIdeTrustListener.ts @@ -17,7 +17,7 @@ export function useIdeTrustListener(config: Config) { const subscribe = useCallback( (onStoreChange: () => void) => { const ideClient = config.getIdeClient(); - if (!ideClient) { + if (ideClient == null) { return () => {}; // Return empty cleanup function if no IDE client } ideClient.addTrustChangeListener(onStoreChange); diff --git a/packages/cli/src/ui/hooks/useInputHistory.test.ts b/packages/cli/src/ui/hooks/useInputHistory.test.ts index bcd9d64b65..7d5e8f261d 100644 --- a/packages/cli/src/ui/hooks/useInputHistory.test.ts +++ b/packages/cli/src/ui/hooks/useInputHistory.test.ts @@ -119,11 +119,11 @@ describe('useInputHistory', () => { const currentQuery = 'current query'; const { result } = renderHook(() => useInputHistory({ - userMessages, onSubmit: mockOnSubmit, isActive: true, - currentQuery, onChange: mockOnChange, + userMessages, + currentQuery, }), ); @@ -138,11 +138,11 @@ describe('useInputHistory', () => { const currentQuery = 'original user input'; const { result } = renderHook(() => useInputHistory({ - userMessages, onSubmit: mockOnSubmit, isActive: true, - currentQuery, onChange: mockOnChange, + userMessages, + currentQuery, }), ); diff --git a/packages/cli/src/ui/hooks/useInputHistoryStore.test.ts b/packages/cli/src/ui/hooks/useInputHistoryStore.test.ts index a333462698..af4e6e5fbd 100644 --- a/packages/cli/src/ui/hooks/useInputHistoryStore.test.ts +++ b/packages/cli/src/ui/hooks/useInputHistoryStore.test.ts @@ -18,7 +18,7 @@ describe('useInputHistoryStore', () => { it('should initialize with empty input history', () => { const { result } = renderHook(() => useInputHistoryStore()); - expect(result.current.inputHistory).toEqual([]); + expect(result.current.inputHistory).toStrictEqual([]); }); it('should add input to history', () => { @@ -28,13 +28,13 @@ describe('useInputHistoryStore', () => { result.current.addInput('test message 1'); }); - expect(result.current.inputHistory).toEqual(['test message 1']); + expect(result.current.inputHistory).toStrictEqual(['test message 1']); act(() => { result.current.addInput('test message 2'); }); - expect(result.current.inputHistory).toEqual([ + expect(result.current.inputHistory).toStrictEqual([ 'test message 1', 'test message 2', ]); @@ -47,13 +47,13 @@ describe('useInputHistoryStore', () => { result.current.addInput(''); }); - expect(result.current.inputHistory).toEqual([]); + expect(result.current.inputHistory).toStrictEqual([]); act(() => { result.current.addInput(' '); }); - expect(result.current.inputHistory).toEqual([]); + expect(result.current.inputHistory).toStrictEqual([]); }); it('should deduplicate consecutive identical messages', () => { @@ -67,7 +67,7 @@ describe('useInputHistoryStore', () => { result.current.addInput('test message'); // Same as previous }); - expect(result.current.inputHistory).toEqual(['test message']); + expect(result.current.inputHistory).toStrictEqual(['test message']); act(() => { result.current.addInput('different message'); @@ -77,7 +77,7 @@ describe('useInputHistoryStore', () => { result.current.addInput('test message'); // Same as first, but not consecutive }); - expect(result.current.inputHistory).toEqual([ + expect(result.current.inputHistory).toStrictEqual([ 'test message', 'different message', 'test message', @@ -98,7 +98,11 @@ describe('useInputHistoryStore', () => { }); // Should reverse the order to oldest first - expect(result.current.inputHistory).toEqual(['oldest', 'middle', 'newest']); + expect(result.current.inputHistory).toStrictEqual([ + 'oldest', + 'middle', + 'newest', + ]); expect(mockLogger.getPreviousUserMessages).toHaveBeenCalledTimes(1); }); @@ -119,7 +123,7 @@ describe('useInputHistoryStore', () => { await result.current.initializeFromLogger(mockLogger); }); - expect(result.current.inputHistory).toEqual([]); + expect(result.current.inputHistory).toStrictEqual([]); expect(debugWarnSpy).toHaveBeenCalledWith( 'Failed to initialize input history from logger:', expect.any(Error), @@ -148,7 +152,7 @@ describe('useInputHistoryStore', () => { // Should be called only once expect(mockLogger.getPreviousUserMessages).toHaveBeenCalledTimes(1); - expect(result.current.inputHistory).toEqual(['message2', 'message1']); + expect(result.current.inputHistory).toStrictEqual(['message2', 'message1']); }); it('should handle null logger gracefully', async () => { @@ -158,7 +162,7 @@ describe('useInputHistoryStore', () => { await result.current.initializeFromLogger(null); }); - expect(result.current.inputHistory).toEqual([]); + expect(result.current.inputHistory).toStrictEqual([]); }); it('should trim input before adding to history', () => { @@ -168,7 +172,7 @@ describe('useInputHistoryStore', () => { result.current.addInput(' test message '); }); - expect(result.current.inputHistory).toEqual(['test message']); + expect(result.current.inputHistory).toStrictEqual(['test message']); }); describe('deduplication logic from previous implementation', () => { @@ -192,7 +196,7 @@ describe('useInputHistoryStore', () => { }); // Should deduplicate consecutive messages and reverse to oldest first - expect(result.current.inputHistory).toEqual([ + expect(result.current.inputHistory).toStrictEqual([ 'message3', 'message2', 'message1', @@ -217,13 +221,17 @@ describe('useInputHistoryStore', () => { }); // Should deduplicate across session boundary - expect(result.current.inputHistory).toEqual(['old1', 'old2']); + expect(result.current.inputHistory).toStrictEqual(['old1', 'old2']); act(() => { result.current.addInput('new1'); }); - expect(result.current.inputHistory).toEqual(['old1', 'old2', 'new1']); + expect(result.current.inputHistory).toStrictEqual([ + 'old1', + 'old2', + 'new1', + ]); }); it('should preserve non-consecutive duplicates', async () => { @@ -240,7 +248,7 @@ describe('useInputHistoryStore', () => { }); // Non-consecutive duplicates should be preserved - expect(result.current.inputHistory).toEqual([ + expect(result.current.inputHistory).toStrictEqual([ 'message2', 'message1', 'message2', @@ -268,7 +276,11 @@ describe('useInputHistoryStore', () => { }); // Should have deduplicated consecutive ones - expect(result.current.inputHistory).toEqual(['hello', 'world', 'hello']); + expect(result.current.inputHistory).toStrictEqual([ + 'hello', + 'world', + 'hello', + ]); }); it('should maintain oldest-first order in final output', async () => { @@ -293,7 +305,7 @@ describe('useInputHistoryStore', () => { }); // Should maintain oldest-first order - expect(result.current.inputHistory).toEqual([ + expect(result.current.inputHistory).toStrictEqual([ 'oldest', 'middle', 'newest', diff --git a/packages/cli/src/ui/hooks/useInputHistoryStore.ts b/packages/cli/src/ui/hooks/useInputHistoryStore.ts index ab7429a4bb..4be5949df5 100644 --- a/packages/cli/src/ui/hooks/useInputHistoryStore.ts +++ b/packages/cli/src/ui/hooks/useInputHistoryStore.ts @@ -61,7 +61,7 @@ export function useInputHistoryStore(): UseInputHistoryStoreReturn { */ const initializeFromLogger = useCallback( async (logger: Logger | null) => { - if (isInitialized || !logger) return; + if (isInitialized || logger == null) return; try { const pastMessages = (await logger.getPreviousUserMessages()) || []; diff --git a/packages/cli/src/ui/hooks/useKeypress.test.tsx b/packages/cli/src/ui/hooks/useKeypress.test.tsx index 2243688a81..e2b8285cd2 100644 --- a/packages/cli/src/ui/hooks/useKeypress.test.tsx +++ b/packages/cli/src/ui/hooks/useKeypress.test.tsx @@ -201,7 +201,7 @@ describe.each([true, false])(`useKeypress with useKitty=%s`, (useKitty) => { const sequences = onKeypress.mock.calls.map(([arg]) => arg.sequence); const expectedSequences = useKitty ? ['\x1B[200do'] : ['\x1B[200d', 'o']; - expect(sequences).toEqual(expectedSequences); + expect(sequences).toStrictEqual(expectedSequences); }); it('should handle back to back pastes', () => { diff --git a/packages/cli/src/ui/hooks/useKeypress.ts b/packages/cli/src/ui/hooks/useKeypress.ts index 12770c1638..a78be410fc 100644 --- a/packages/cli/src/ui/hooks/useKeypress.ts +++ b/packages/cli/src/ui/hooks/useKeypress.ts @@ -7,7 +7,7 @@ import { useEffect } from 'react'; import { useKeypressContext, - KeypressHandler, + type KeypressHandler, Key, } from '../contexts/KeypressContext.js'; diff --git a/packages/cli/src/ui/hooks/useLoadProfileDialog.ts b/packages/cli/src/ui/hooks/useLoadProfileDialog.ts index cb6278a078..478bd3ca2f 100644 --- a/packages/cli/src/ui/hooks/useLoadProfileDialog.ts +++ b/packages/cli/src/ui/hooks/useLoadProfileDialog.ts @@ -7,9 +7,9 @@ import { useCallback, useState } from 'react'; import { MessageType } from '../types.js'; import { useAppDispatch } from '../contexts/AppDispatchContext.js'; -import { AppState } from '../reducers/appReducer.js'; -import { Config } from '@vybestack/llxprt-code-core'; -import { LoadedSettings } from '../../config/settings.js'; +import type { AppState } from '../reducers/appReducer.js'; +import type { Config } from '@vybestack/llxprt-code-core'; +import type { LoadedSettings } from '../../config/settings.js'; import { useRuntimeApi } from '../contexts/RuntimeContext.js'; interface UseLoadProfileDialogParams { diff --git a/packages/cli/src/ui/hooks/useLoadingIndicator.ts b/packages/cli/src/ui/hooks/useLoadingIndicator.ts index 391a6a5fbe..db514770f1 100644 --- a/packages/cli/src/ui/hooks/useLoadingIndicator.ts +++ b/packages/cli/src/ui/hooks/useLoadingIndicator.ts @@ -7,7 +7,7 @@ import { StreamingState } from '../types.js'; import { useTimer } from './useTimer.js'; import { usePhraseCycler } from './usePhraseCycler.js'; -import { type WittyPhraseStyle } from '../constants/phrasesCollections.js'; +import type { WittyPhraseStyle } from '../constants/phrasesCollections.js'; import { useState, useEffect, useRef } from 'react'; // Added useRef export const useLoadingIndicator = ( diff --git a/packages/cli/src/ui/hooks/useLogger.ts b/packages/cli/src/ui/hooks/useLogger.ts index a792026edd..93550cdd07 100644 --- a/packages/cli/src/ui/hooks/useLogger.ts +++ b/packages/cli/src/ui/hooks/useLogger.ts @@ -5,7 +5,7 @@ */ import { useState, useEffect } from 'react'; -import { sessionId, Logger, Storage } from '@vybestack/llxprt-code-core'; +import { sessionId, Logger, type Storage } from '@vybestack/llxprt-code-core'; /** * Hook to manage the logger instance. diff --git a/packages/cli/src/ui/hooks/useMouseClick.ts b/packages/cli/src/ui/hooks/useMouseClick.ts index 0e221f1505..970a238dff 100644 --- a/packages/cli/src/ui/hooks/useMouseClick.ts +++ b/packages/cli/src/ui/hooks/useMouseClick.ts @@ -18,7 +18,7 @@ export const useMouseClick = ( useMouse( (event: MouseEvent) => { const eventName = button === 'left' ? 'left-press' : 'right-release'; - if (event.name === eventName && containerRef.current) { + if (event.name === eventName && containerRef.current != null) { const { x, y, width, height } = getBoundingBox(containerRef.current); // Terminal mouse events are 1-based, Ink layout is 0-based. const mouseX = event.col - 1; diff --git a/packages/cli/src/ui/hooks/useMouseSelection.ts b/packages/cli/src/ui/hooks/useMouseSelection.ts index 8004757140..f8d1f96ef4 100644 --- a/packages/cli/src/ui/hooks/useMouseSelection.ts +++ b/packages/cli/src/ui/hooks/useMouseSelection.ts @@ -144,7 +144,7 @@ export function useMouseSelection({ event: MouseEvent, ): { node: DOMElement; point: { x: number; y: number } } | null => { const root = rootRef.current; - if (!root) return null; + if (root == null) return null; const x = event.col - 1; const y = event.row - 1; @@ -158,27 +158,27 @@ export function useMouseSelection({ }> = []; for (const entry of scrollables) { - if (!entry.ref.current || !entry.hasFocus()) continue; + if (entry.ref.current == null || !entry.hasFocus()) continue; const box = getBoundingBox(entry.ref.current); if (isInsideBox(point, box)) { scrollableCandidates.push({ node: entry.ref.current, - box, area: box.width * box.height, + box, }); } } scrollableCandidates.sort((a, b) => a.area - b.area); - const scrollableRoot = scrollableCandidates[0]?.node; - - if (!scrollableRoot) { + if (scrollableCandidates.length === 0) { return { node: root, point }; } + const scrollableRoot = scrollableCandidates[0].node; + // Ignore clicks on the scrollbar column so scrollbar dragging still works. - const scrollableBox = scrollableCandidates[0]?.box; - if (scrollableBox && x === scrollableBox.x + scrollableBox.width - 1) { + const scrollableBox = scrollableCandidates[0].box; + if (x === scrollableBox.x + scrollableBox.width - 1) { return null; } @@ -187,7 +187,7 @@ export function useMouseSelection({ point, ); - if (innermostScrollable) { + if (innermostScrollable != null) { const { scrollTop, scrollLeft } = getScrollOffsets( innermostScrollable.node, ); @@ -203,8 +203,8 @@ export function useMouseSelection({ return { node: scrollableRoot, point: { - x: x - (scrollableBox?.x ?? 0), - y: y - (scrollableBox?.y ?? 0), + x: x - scrollableBox.x, + y: y - scrollableBox.y, }, }; }, @@ -214,13 +214,13 @@ export function useMouseSelection({ const resolveSelectionPoint = useCallback( (event: MouseEvent): SelectionPoint | null => { if (!enabled) return null; - if (!selection) return null; + if (selection == null) return null; const target = findHitTestTarget(event); - if (!target) return null; + if (target == null) return null; const hit = hitTest(target.node, target.point.x, target.point.y); - if (!hit) return null; + if (hit == null) return null; return { node: hit.node, offset: hit.offset }; }, @@ -229,7 +229,7 @@ export function useMouseSelection({ const updateSelectionRange = useCallback( (anchor: SelectionPoint, focus: SelectionPoint) => { - if (!selection) return; + if (selection == null) return; const ordering = comparePoints( anchor.node, @@ -244,7 +244,7 @@ export function useMouseSelection({ range.setStart(start.node, start.offset); range.setEnd(end.node, end.offset); - if (!selectionRangeRef.current) { + if (selectionRangeRef.current == null) { selection.removeAllRanges(); selection.addRange(range); selectionRangeRef.current = range; @@ -256,7 +256,7 @@ export function useMouseSelection({ ); const copySelectionToClipboard = useCallback(async () => { - if (!selection || selection.rangeCount === 0) return; + if (selection == null || selection.rangeCount === 0) return; // Issue #885: Snapshot the text immediately before any async operations // to prevent race conditions where selection changes during copy const text = selection.toString(); @@ -273,11 +273,11 @@ export function useMouseSelection({ const mouseHandler = useMemo( () => (event: MouseEvent) => { if (!enabled) return; - if (!selection) return; + if (selection == null) return; if (event.name === 'left-press') { const point = resolveSelectionPoint(event); - if (!point) return; + if (point == null) return; isDraggingRef.current = true; anchorPointRef.current = point; updateSelectionRange(point, point); @@ -287,9 +287,9 @@ export function useMouseSelection({ if (event.name === 'move') { if (!isDraggingRef.current) return; const anchor = anchorPointRef.current; - if (!anchor) return; + if (anchor == null) return; const point = resolveSelectionPoint(event); - if (!point) return; + if (point == null) return; updateSelectionRange(anchor, point); return; } @@ -297,9 +297,9 @@ export function useMouseSelection({ if (event.name === 'left-release') { if (!isDraggingRef.current) return; const anchor = anchorPointRef.current; - if (anchor) { + if (anchor != null) { const releasePoint = resolveSelectionPoint(event); - if (releasePoint) { + if (releasePoint != null) { updateSelectionRange(anchor, releasePoint); } } diff --git a/packages/cli/src/ui/hooks/useOpenAIProviderInfo.ts b/packages/cli/src/ui/hooks/useOpenAIProviderInfo.ts index 8e09d44818..aa163bc7ba 100644 --- a/packages/cli/src/ui/hooks/useOpenAIProviderInfo.ts +++ b/packages/cli/src/ui/hooks/useOpenAIProviderInfo.ts @@ -6,8 +6,8 @@ import { useState, useEffect, useCallback } from 'react'; import { - Config, - ProviderMessage as Message, + type Config, + type ProviderMessage as Message, getOpenAIProviderInfo, } from '@vybestack/llxprt-code-core'; import { useRuntimeApi } from '../contexts/RuntimeContext.js'; @@ -64,7 +64,7 @@ export function useOpenAIProviderInfo( const getCachedConversation = useCallback( (conversationId: string, parentId: string) => { - if (!providerInfo.conversationCache) { + if (providerInfo.conversationCache == null) { return null; } return providerInfo.conversationCache.get(conversationId, parentId); diff --git a/packages/cli/src/ui/hooks/usePermissionsModifyTrust.test.tsx b/packages/cli/src/ui/hooks/usePermissionsModifyTrust.test.tsx index 3a1877cfb2..55cdbc3f6d 100644 --- a/packages/cli/src/ui/hooks/usePermissionsModifyTrust.test.tsx +++ b/packages/cli/src/ui/hooks/usePermissionsModifyTrust.test.tsx @@ -59,7 +59,7 @@ describe('usePermissionsModifyTrust', () => { const folders = loadTrustedFolders(); expect(folders).toBeDefined(); - expect(folders.rules).toEqual([]); + expect(folders.rules).toStrictEqual([]); expect(folders.setValue).toBeDefined(); }); diff --git a/packages/cli/src/ui/hooks/usePermissionsModifyTrust.ts b/packages/cli/src/ui/hooks/usePermissionsModifyTrust.ts index 4969f77a03..cebdcbc3ce 100644 --- a/packages/cli/src/ui/hooks/usePermissionsModifyTrust.ts +++ b/packages/cli/src/ui/hooks/usePermissionsModifyTrust.ts @@ -9,7 +9,7 @@ import * as path from 'node:path'; import { loadTrustedFolders, TrustLevel, - LoadedTrustedFolders, + type LoadedTrustedFolders, } from '../../config/trustedFolders.js'; import { useSettings } from '../contexts/SettingsContext.js'; import { getIdeTrust } from '@vybestack/llxprt-code-core'; diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.test.ts b/packages/cli/src/ui/hooks/usePhraseCycler.test.ts index 3bc252e807..829ac080ca 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.test.ts +++ b/packages/cli/src/ui/hooks/usePhraseCycler.test.ts @@ -254,7 +254,7 @@ describe('usePhraseCycler', () => { it('should use LLxprt phrases for "default" style when custom is undefined', () => { const { result } = renderHook(() => - usePhraseCycler(true, false, 'default', false, 0, undefined), + usePhraseCycler(true, false, 'default', false, 0), ); expect(LLXPRT_PHRASES).toContain(result.current); }); diff --git a/packages/cli/src/ui/hooks/usePhraseCycler.ts b/packages/cli/src/ui/hooks/usePhraseCycler.ts index 2ffd94dff6..79ebefde85 100644 --- a/packages/cli/src/ui/hooks/usePhraseCycler.ts +++ b/packages/cli/src/ui/hooks/usePhraseCycler.ts @@ -54,7 +54,7 @@ export const usePhraseCycler = ( useEffect(() => { if (showShellFocusHint) { setCurrentLoadingPhrase(INTERACTIVE_SHELL_WAITING_PHRASE); - if (phraseIntervalRef.current) { + if (phraseIntervalRef.current != null) { clearInterval(phraseIntervalRef.current); phraseIntervalRef.current = null; } @@ -63,12 +63,12 @@ export const usePhraseCycler = ( if (isWaiting) { setCurrentLoadingPhrase('Waiting for user confirmation...'); - if (phraseIntervalRef.current) { + if (phraseIntervalRef.current != null) { clearInterval(phraseIntervalRef.current); phraseIntervalRef.current = null; } } else if (isActive) { - if (phraseIntervalRef.current) { + if (phraseIntervalRef.current != null) { clearInterval(phraseIntervalRef.current); } // Select an initial random phrase @@ -85,7 +85,7 @@ export const usePhraseCycler = ( } else { // Idle or other states, clear the phrase interval // and reset to the first phrase for next active state. - if (phraseIntervalRef.current) { + if (phraseIntervalRef.current != null) { clearInterval(phraseIntervalRef.current); phraseIntervalRef.current = null; } @@ -93,7 +93,7 @@ export const usePhraseCycler = ( } return () => { - if (phraseIntervalRef.current) { + if (phraseIntervalRef.current != null) { clearInterval(phraseIntervalRef.current); phraseIntervalRef.current = null; } diff --git a/packages/cli/src/ui/hooks/useProfileManagement.ts b/packages/cli/src/ui/hooks/useProfileManagement.ts index 737ee448cd..072d3e9eae 100644 --- a/packages/cli/src/ui/hooks/useProfileManagement.ts +++ b/packages/cli/src/ui/hooks/useProfileManagement.ts @@ -7,7 +7,7 @@ import { useCallback, useEffect, useState } from 'react'; import { MessageType } from '../types.js'; import { useAppDispatch } from '../contexts/AppDispatchContext.js'; -import { AppState } from '../reducers/appReducer.js'; +import type { AppState } from '../reducers/appReducer.js'; import { useRuntimeApi } from '../contexts/RuntimeContext.js'; import type { Profile } from '@vybestack/llxprt-code-core'; import type { ProfileListItem } from '../components/ProfileListDialog.js'; diff --git a/packages/cli/src/ui/hooks/usePromptCompletion.ts b/packages/cli/src/ui/hooks/usePromptCompletion.ts index 54a49048b7..9aa2b24293 100644 --- a/packages/cli/src/ui/hooks/usePromptCompletion.ts +++ b/packages/cli/src/ui/hooks/usePromptCompletion.ts @@ -6,7 +6,7 @@ import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { - Config, + type Config, DEFAULT_GEMINI_FLASH_LITE_MODEL, getResponseText, debugLogger, @@ -76,13 +76,13 @@ export function usePromptCompletion({ return; } - if (abortControllerRef.current) { + if (abortControllerRef.current != null) { abortControllerRef.current.abort(); } if ( trimmedText.length < PROMPT_COMPLETION_MIN_LENGTH || - !geminiClient || + geminiClient == null || isSlashCommand(trimmedText) || trimmedText.includes('@') || !isPromptCompletionEnabled @@ -129,20 +129,18 @@ export function usePromptCompletion({ return; } - if (response) { - const responseText = getResponseText(response); + const responseText = getResponseText(response); - if (responseText) { - const suggestionText = responseText.trim(); + if (responseText) { + const suggestionText = responseText.trim(); - if ( - suggestionText.length > 0 && - suggestionText.startsWith(trimmedText) - ) { - setGhostText(suggestionText); - } else { - clearGhostText(); - } + if ( + suggestionText.length > 0 && + suggestionText.startsWith(trimmedText) + ) { + setGhostText(suggestionText); + } else { + clearGhostText(); } } } catch (error) { @@ -245,9 +243,9 @@ export function usePromptCompletion({ return { text: ghostText, isLoading: isLoadingGhostText, - isActive, accept: acceptGhostText, clear: clearGhostText, markSelected: markSuggestionSelected, + isActive, }; } diff --git a/packages/cli/src/ui/hooks/useProviderDialog.ts b/packages/cli/src/ui/hooks/useProviderDialog.ts index e3f50115a6..cf28d46b36 100644 --- a/packages/cli/src/ui/hooks/useProviderDialog.ts +++ b/packages/cli/src/ui/hooks/useProviderDialog.ts @@ -7,8 +7,8 @@ import { useCallback, useState } from 'react'; import { MessageType } from '../types.js'; import { useAppDispatch } from '../contexts/AppDispatchContext.js'; -import { AppState } from '../reducers/appReducer.js'; -import { Config, type RecordingIntegration } from '@vybestack/llxprt-code-core'; +import type { AppState } from '../reducers/appReducer.js'; +import type { Config, RecordingIntegration } from '@vybestack/llxprt-code-core'; import { useRuntimeApi } from '../contexts/RuntimeContext.js'; interface UseProviderDialogParams { @@ -69,7 +69,7 @@ export const useProviderDialog = ({ const result = await runtime.switchActiveProvider(providerName); // Clear UI history to prevent tool call ID mismatches - if (onClear) { + if (onClear != null) { onClear(); } diff --git a/packages/cli/src/ui/hooks/useQuitCleanup.test.ts b/packages/cli/src/ui/hooks/useQuitCleanup.test.ts index a6c9a53803..52f0f443ab 100644 --- a/packages/cli/src/ui/hooks/useQuitCleanup.test.ts +++ b/packages/cli/src/ui/hooks/useQuitCleanup.test.ts @@ -56,6 +56,6 @@ describe('quit cleanup integration', () => { const { runExitCleanup } = await import('../../utils/cleanup.js'); await runExitCleanup(); - expect(callOrder).toEqual(['first', 'second', 'third']); + expect(callOrder).toStrictEqual(['first', 'second', 'third']); }); }); diff --git a/packages/cli/src/ui/hooks/useResponsive.ts b/packages/cli/src/ui/hooks/useResponsive.ts index 83169aa436..9d23212b34 100644 --- a/packages/cli/src/ui/hooks/useResponsive.ts +++ b/packages/cli/src/ui/hooks/useResponsive.ts @@ -25,9 +25,9 @@ export function useResponsive(): UseResponsiveReturn { return { width: columns, - breakpoint, isNarrow: isNarrowWidth(columns), isStandard: breakpoint === 'STANDARD', isWide: breakpoint === 'WIDE', + breakpoint, }; } diff --git a/packages/cli/src/ui/hooks/useReverseSearchCompletion.test.tsx b/packages/cli/src/ui/hooks/useReverseSearchCompletion.test.tsx index 5b8edc9570..f3af3e89db 100644 --- a/packages/cli/src/ui/hooks/useReverseSearchCompletion.test.tsx +++ b/packages/cli/src/ui/hooks/useReverseSearchCompletion.test.tsx @@ -36,7 +36,7 @@ describe('useReverseSearchCompletion', () => { ), ); - expect(result.current.suggestions).toEqual([]); + expect(result.current.suggestions).toStrictEqual([]); expect(result.current.activeSuggestionIndex).toBe(-1); expect(result.current.visibleStartIndex).toBe(0); expect(result.current.showSuggestions).toBe(false); @@ -60,7 +60,7 @@ describe('useReverseSearchCompletion', () => { // Simulate reverseSearchActive becoming false rerender({ text: 'echo', active: false }); - expect(result.current.suggestions).toEqual([]); + expect(result.current.suggestions).toStrictEqual([]); expect(result.current.activeSuggestionIndex).toBe(-1); expect(result.current.visibleStartIndex).toBe(0); expect(result.current.showSuggestions).toBe(false); @@ -241,7 +241,7 @@ describe('useReverseSearchCompletion', () => { ); // should only return the two entries containing "foo" - expect(result.current.suggestions.map((s) => s.value)).toEqual([ + expect(result.current.suggestions.map((s) => s.value)).toStrictEqual([ 'foo', 'barfoo', ]); @@ -254,7 +254,7 @@ describe('useReverseSearchCompletion', () => { useReverseSearchCompletion(useTextBufferForTest('γ'), history, true), ); - expect(result.current.suggestions).toEqual([]); + expect(result.current.suggestions).toStrictEqual([]); expect(result.current.showSuggestions).toBe(false); }); }); diff --git a/packages/cli/src/ui/hooks/useReverseSearchCompletion.tsx b/packages/cli/src/ui/hooks/useReverseSearchCompletion.tsx index b558bd0495..570c46d94e 100644 --- a/packages/cli/src/ui/hooks/useReverseSearchCompletion.tsx +++ b/packages/cli/src/ui/hooks/useReverseSearchCompletion.tsx @@ -6,8 +6,8 @@ import { useEffect, useCallback } from 'react'; import { useCompletion } from './useCompletion.js'; -import { TextBuffer } from '../components/shared/text-buffer.js'; -import { Suggestion } from '../components/SuggestionsDisplay.js'; +import type { TextBuffer } from '../components/shared/text-buffer.js'; +import type { Suggestion } from '../components/SuggestionsDisplay.js'; export interface UseReverseSearchCompletionReturn { suggestions: Suggestion[]; diff --git a/packages/cli/src/ui/hooks/useRewind.test.ts b/packages/cli/src/ui/hooks/useRewind.test.ts index 1468f06883..55e49bf317 100644 --- a/packages/cli/src/ui/hooks/useRewind.test.ts +++ b/packages/cli/src/ui/hooks/useRewind.test.ts @@ -71,7 +71,7 @@ describe('useRewindLogic', () => { }); expect(result.current.selectedMessageId).toBe('msg-1'); - expect(result.current.confirmationStats).toEqual(mockStats); + expect(result.current.confirmationStats).toStrictEqual(mockStats); expect(rewindFileOps.calculateRewindImpact).toHaveBeenCalledWith( mockConversation, mockUserMessage, @@ -126,7 +126,7 @@ describe('useRewindLogic', () => { const stats = result.current.getStats(mockUserMessage); - expect(stats).toEqual(mockStats); + expect(stats).toStrictEqual(mockStats); expect(rewindFileOps.calculateTurnStats).toHaveBeenCalledWith( mockConversation, mockUserMessage, diff --git a/packages/cli/src/ui/hooks/useRewind.ts b/packages/cli/src/ui/hooks/useRewind.ts index b34b71262a..1dcf061636 100644 --- a/packages/cli/src/ui/hooks/useRewind.ts +++ b/packages/cli/src/ui/hooks/useRewind.ts @@ -31,7 +31,7 @@ export function useRewind(conversation: ConversationRecord) { const selectMessage = useCallback( (messageId: string) => { const msg = conversation.messages.find((m) => m.id === messageId); - if (msg) { + if (msg != null) { setSelectedMessageId(messageId); setConfirmationStats(calculateRewindImpact(conversation, msg)); } diff --git a/packages/cli/src/ui/hooks/useSelectionList.test.ts b/packages/cli/src/ui/hooks/useSelectionList.test.ts index 540982bcfe..f00437a658 100644 --- a/packages/cli/src/ui/hooks/useSelectionList.test.ts +++ b/packages/cli/src/ui/hooks/useSelectionList.test.ts @@ -54,7 +54,7 @@ describe('useSelectionList', () => { options: { shift?: boolean; ctrl?: boolean } = {}, ) => { act(() => { - if (activeKeypressHandler) { + if (activeKeypressHandler != null) { const key: Key = { name, sequence, @@ -312,7 +312,7 @@ describe('useSelectionList', () => { ); // Simulate rapid inputs with separate act blocks to allow effects to run - if (!activeKeypressHandler) throw new Error('Handler not active'); + if (activeKeypressHandler == null) throw new Error('Handler not active'); const handler = activeKeypressHandler; @@ -362,7 +362,8 @@ describe('useSelectionList', () => { // Simulate ultra-rapid inputs where all keypresses happen faster than React can re-render act(() => { - if (!activeKeypressHandler) throw new Error('Handler not active'); + if (activeKeypressHandler == null) + throw new Error('Handler not active'); const handler = activeKeypressHandler; diff --git a/packages/cli/src/ui/hooks/useSelectionList.ts b/packages/cli/src/ui/hooks/useSelectionList.ts index 7d2d2e6dbe..568f175f04 100644 --- a/packages/cli/src/ui/hooks/useSelectionList.ts +++ b/packages/cli/src/ui/hooks/useSelectionList.ts @@ -89,7 +89,7 @@ const findNextValidIndex = ( // We add `len` before the modulo to ensure a positive result in JS for negative steps. nextIndex = (nextIndex + step + len) % len; - if (!items[nextIndex]?.disabled) { + if (items[nextIndex].disabled !== true) { return nextIndex; } } @@ -109,7 +109,7 @@ const computeInitialIndex = ( if (initialKey !== undefined) { for (let i = 0; i < items.length; i++) { - if (items[i].key === initialKey && !items[i].disabled) { + if (items[i].key === initialKey && items[i].disabled !== true) { return i; } } @@ -121,7 +121,7 @@ const computeInitialIndex = ( targetIndex = 0; } - if (items[targetIndex]?.disabled) { + if (items[targetIndex].disabled === true) { const nextValid = findNextValidIndex(targetIndex, 'down', items); targetIndex = nextValid; } @@ -184,10 +184,10 @@ function selectionListReducer( return { ...state, - items, - initialIndex, activeIndex: targetIndex, pendingHighlight: false, + items, + initialIndex, }; } @@ -252,10 +252,10 @@ export function useSelectionList({ const [state, dispatch] = useReducer(selectionListReducer, { activeIndex: computeInitialIndex(initialIndex, baseItems), - initialIndex, pendingHighlight: false, pendingSelect: false, items: baseItems, + initialIndex, }); const numberInputRef = useRef(''); const numberInputTimer = useRef(null); @@ -285,14 +285,14 @@ export function useSelectionList({ useEffect(() => { let needsClear = false; - if (state.pendingHighlight && items[state.activeIndex]) { + if (state.pendingHighlight) { onHighlight?.(items[state.activeIndex].value); needsClear = true; } - if (state.pendingSelect && items[state.activeIndex]) { + if (state.pendingSelect) { const currentItem = items[state.activeIndex]; - if (currentItem && !currentItem.disabled) { + if (currentItem.disabled !== true) { if (selectionLogger.enabled) { selectionLogger.debug( () => @@ -320,7 +320,7 @@ export function useSelectionList({ useEffect( () => () => { - if (numberInputTimer.current) { + if (numberInputTimer.current != null) { clearTimeout(numberInputTimer.current); } }, @@ -343,7 +343,7 @@ export function useSelectionList({ } // Clear number input buffer on non-numeric key press - if (!isNumeric && numberInputTimer.current) { + if (!isNumeric && numberInputTimer.current != null) { clearTimeout(numberInputTimer.current); numberInputRef.current = ''; } @@ -371,7 +371,7 @@ export function useSelectionList({ // Handle numeric input for quick selection if (isNumeric) { - if (numberInputTimer.current) { + if (numberInputTimer.current != null) { clearTimeout(numberInputTimer.current); } diff --git a/packages/cli/src/ui/hooks/useSession.ts b/packages/cli/src/ui/hooks/useSession.ts index 3b152af671..0a6b02f2c3 100644 --- a/packages/cli/src/ui/hooks/useSession.ts +++ b/packages/cli/src/ui/hooks/useSession.ts @@ -16,7 +16,7 @@ import { */ export const useSession = (): SessionContextType => { const context = useContext(SessionContext); - if (!context) { + if (context == null) { throw new Error('useSession must be used within SessionController'); } return context; diff --git a/packages/cli/src/ui/hooks/useSessionBrowser.ts b/packages/cli/src/ui/hooks/useSessionBrowser.ts index 6bacb27d15..1bf05cd5ed 100644 --- a/packages/cli/src/ui/hooks/useSessionBrowser.ts +++ b/packages/cli/src/ui/hooks/useSessionBrowser.ts @@ -204,11 +204,7 @@ export function useSessionBrowser( // Always include sessions with unloaded previews (REQ-SR-003) if (s.previewState === 'loading') return true; // Match against loaded preview text, provider, model - const fields = [ - s.firstUserMessage ?? '', - s.provider ?? '', - s.model ?? '', - ]; + const fields = [s.firstUserMessage ?? '', s.provider, s.model]; return fields.some((f) => f.toLowerCase().includes(lowerTerm)); }); }, [sessions]); @@ -228,7 +224,7 @@ export function useSessionBrowser( (a, b) => a.lastModified.getTime() - b.lastModified.getTime(), ); case 'size': - return sorted.sort((a, b) => (b.fileSize ?? 0) - (a.fileSize ?? 0)); + return sorted.sort((a, b) => b.fileSize - a.fileSize); default: return sorted; } @@ -365,7 +361,7 @@ export function useSessionBrowser( const enriched: EnrichedSessionSummary = { ...session, isLocked: locked, - previewState: cached ? cached.state : 'loading', + previewState: cached != null ? cached.state : 'loading', firstUserMessage: cached?.text ?? undefined, }; filtered.push(enriched); @@ -517,9 +513,7 @@ export function useSessionBrowser( const lowerInput = input.toLowerCase(); if (lowerInput === 'y') { const session = currentPageItems[currentDeleteConfirmIndex]; - if (session) { - void executeDelete(session); - } + void executeDelete(session); } else if (lowerInput === 'n' || key.name === 'escape') { setDeleteConfirmIndex(null); } @@ -532,9 +526,7 @@ export function useSessionBrowser( const lowerInput = input.toLowerCase(); if (lowerInput === 'y') { setConversationConfirmActive(false); - if (currentSelectedSession) { - void executeResume(currentSelectedSession); - } + void executeResume(currentSelectedSession); } else if (lowerInput === 'n' || key.name === 'escape') { setConversationConfirmActive(false); } @@ -562,8 +554,6 @@ export function useSessionBrowser( // Enter: initiate resume if (key.name === 'return') { - if (!currentSelectedSession) return; - if (hasActiveConversation) { setConversationConfirmActive(true); } else { @@ -630,7 +620,7 @@ export function useSessionBrowser( if (!currentIsSearching) { // Delete key shows confirmation if (key.name === 'delete') { - if (currentPageItems.length > 0 && currentSelectedSession) { + if (currentPageItems.length > 0) { setDeleteConfirmIndex(currentSelectedIndex); } return; @@ -645,7 +635,6 @@ export function useSessionBrowser( } // Characters and backspace are no-op in nav mode - return; } }, [ @@ -683,7 +672,6 @@ export function useSessionBrowser( // Return object with getters for synchronous observation return { - sessions, get filteredSessions() { return getSortedSessions(); }, @@ -702,7 +690,6 @@ export function useSessionBrowser( get isSearching() { return isSearchingRef.current; }, - isLoading, get isResuming() { return isResumingRef.current; }, @@ -715,7 +702,6 @@ export function useSessionBrowser( get error() { return errorRef.current; }, - skippedCount, get totalPages() { return getPaginationValues().totalPages; }, @@ -725,6 +711,9 @@ export function useSessionBrowser( get selectedSession() { return getPaginationValues().selectedSession; }, + sessions, + isLoading, + skippedCount, handleKeypress, }; } diff --git a/packages/cli/src/ui/hooks/useSessionRestore.test.ts b/packages/cli/src/ui/hooks/useSessionRestore.test.ts index 3accbd32ef..1546840e07 100644 --- a/packages/cli/src/ui/hooks/useSessionRestore.test.ts +++ b/packages/cli/src/ui/hooks/useSessionRestore.test.ts @@ -50,7 +50,7 @@ describe('Session Restore Chat Initialization', () => { expect(mockGeminiClient.resetChat).not.toHaveBeenCalled(); const geminiClient = mockConfig.getGeminiClient(); - if (geminiClient) { + if (geminiClient != null) { await geminiClient.resetChat(); } @@ -66,7 +66,7 @@ describe('Session Restore Chat Initialization', () => { const geminiClient = mockConfig.getGeminiClient(); expect(async () => { - if (geminiClient) { + if (geminiClient != null) { await geminiClient.resetChat().catch(() => {}); } }).not.toThrow(); @@ -82,7 +82,7 @@ describe('Session Restore Chat Initialization', () => { const geminiClient = mockConfig.getGeminiClient(); expect(() => { - if (geminiClient) { + if (geminiClient != null) { geminiClient.resetChat(); } }).not.toThrow(); @@ -102,10 +102,10 @@ describe('Session Restore Chat Initialization', () => { const geminiClient = mockConfig.getGeminiClient(); - if (geminiClient) { + if (geminiClient != null) { await geminiClient.resetChat(); const historyService = geminiClient.getHistoryService(); - if (historyService) { + if (historyService != null) { historyService.addAll(restoredSessionHistory); } } @@ -133,10 +133,10 @@ describe('Session Restore Chat Initialization', () => { const geminiClient = mockConfig.getGeminiClient(); - if (geminiClient) { + if (geminiClient != null) { await geminiClient.resetChat().catch(() => {}); const historyService = geminiClient.getHistoryService(); - if (historyService) { + if (historyService != null) { historyService.addAll(restoredSessionHistory); } } diff --git a/packages/cli/src/ui/hooks/useShellHistory.test.ts b/packages/cli/src/ui/hooks/useShellHistory.test.ts index bc2fd79af6..77bf58cc54 100644 --- a/packages/cli/src/ui/hooks/useShellHistory.test.ts +++ b/packages/cli/src/ui/hooks/useShellHistory.test.ts @@ -257,6 +257,6 @@ describe('useShellHistory', () => { const writtenContent = mockedFs.writeFile.mock.calls[0][1] as string; const writtenLines = writtenContent.split('\n'); - expect(writtenLines).toEqual(['cmd2', 'cmd3', 'cmd1']); + expect(writtenLines).toStrictEqual(['cmd2', 'cmd3', 'cmd1']); }); }); diff --git a/packages/cli/src/ui/hooks/useShellHistory.ts b/packages/cli/src/ui/hooks/useShellHistory.ts index 463337f970..06a31817da 100644 --- a/packages/cli/src/ui/hooks/useShellHistory.ts +++ b/packages/cli/src/ui/hooks/useShellHistory.ts @@ -38,8 +38,8 @@ async function readHistoryFile(filePath: string): Promise { if (!raw.trim()) continue; const line = raw; - const m = cur.match(/(\\+)$/); - if (m && m[1].length % 2) { + const m = RegExp(/(\\+)$/).exec(cur); + if (m != null && m[1].length % 2) { // odd number of trailing '\' cur = cur.slice(0, -1) + ' ' + line; } else { diff --git a/packages/cli/src/ui/hooks/useShellPathCompletion.test.ts b/packages/cli/src/ui/hooks/useShellPathCompletion.test.ts index 0218a15f16..0c456b64b3 100644 --- a/packages/cli/src/ui/hooks/useShellPathCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useShellPathCompletion.test.ts @@ -14,7 +14,7 @@ import { useTextBuffer } from '../components/shared/text-buffer.js'; import { createTmpDir, cleanupTmpDir, - FileSystemStructure, + type FileSystemStructure, } from '@vybestack/llxprt-code-test-utils'; describe('useShellPathCompletion', () => { @@ -54,7 +54,7 @@ describe('useShellPathCompletion', () => { return useShellPathCompletion(buffer, testRootDir, true, false); }); - expect(result.current.suggestions).toEqual([]); + expect(result.current.suggestions).toStrictEqual([]); expect(result.current.showSuggestions).toBe(false); }); diff --git a/packages/cli/src/ui/hooks/useShellPathCompletion.ts b/packages/cli/src/ui/hooks/useShellPathCompletion.ts index 5723d8e514..9a96192c86 100644 --- a/packages/cli/src/ui/hooks/useShellPathCompletion.ts +++ b/packages/cli/src/ui/hooks/useShellPathCompletion.ts @@ -6,9 +6,9 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useCompletion } from './useCompletion.js'; -import { Suggestion } from '../components/SuggestionsDisplay.js'; +import type { Suggestion } from '../components/SuggestionsDisplay.js'; import { - TextBuffer, + type TextBuffer, logicalPosToOffset, } from '../components/shared/text-buffer.js'; import { @@ -81,17 +81,16 @@ export function useShellPathCompletion( useEffect(() => { if (!shellModeActive || reverseSearchActive) { resetCompletionState(); - return; } }, [shellModeActive, reverseSearchActive, resetCompletionState]); useEffect(() => { - if (debounceTimerRef.current) { + if (debounceTimerRef.current != null) { clearTimeout(debounceTimerRef.current); debounceTimerRef.current = null; } - if (!pathToken) { + if (pathToken == null) { resetCompletionState(); return; } @@ -122,7 +121,7 @@ export function useShellPathCompletion( }, DEBOUNCE_MS); return () => { - if (debounceTimerRef.current) { + if (debounceTimerRef.current != null) { clearTimeout(debounceTimerRef.current); debounceTimerRef.current = null; } @@ -143,7 +142,7 @@ export function useShellPathCompletion( if (index < 0 || index >= suggestions.length) return; const suggestion = suggestions[index]; - if (!pathToken) return; + if (pathToken == null) return; const { tokenStart, tokenEnd } = pathToken; const isDir = suggestion.value.endsWith('/'); diff --git a/packages/cli/src/ui/hooks/useShowMemoryCommand.ts b/packages/cli/src/ui/hooks/useShowMemoryCommand.ts index d77115b4ae..4ad8b35dde 100644 --- a/packages/cli/src/ui/hooks/useShowMemoryCommand.ts +++ b/packages/cli/src/ui/hooks/useShowMemoryCommand.ts @@ -4,9 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Message, MessageType } from '../types.js'; -import { Config } from '@vybestack/llxprt-code-core'; -import { LoadedSettings } from '../../config/settings.js'; +import { type Message, MessageType } from '../types.js'; +import type { Config } from '@vybestack/llxprt-code-core'; +import type { LoadedSettings } from '../../config/settings.js'; export function createShowMemoryAction( config: Config | null, @@ -14,7 +14,7 @@ export function createShowMemoryAction( addMessage: (message: Message) => void, ) { return async () => { - if (!config) { + if (config == null) { addMessage({ type: MessageType.ERROR, content: 'Configuration not available. Cannot show memory.', diff --git a/packages/cli/src/ui/hooks/useSlashCompletion.extensions.test.tsx b/packages/cli/src/ui/hooks/useSlashCompletion.extensions.test.tsx index b4e9cf3821..3f10dc6ed1 100644 --- a/packages/cli/src/ui/hooks/useSlashCompletion.extensions.test.tsx +++ b/packages/cli/src/ui/hooks/useSlashCompletion.extensions.test.tsx @@ -18,11 +18,11 @@ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { renderHook, waitFor } from '../../test-utils/render.js'; import { useSlashCompletion } from './useSlashCompletion.js'; import { - CommandContext, - SlashCommand, + type CommandContext, + type SlashCommand, CommandKind, } from '../commands/types.js'; -import { Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; import { useTextBuffer } from '../components/shared/text-buffer.js'; import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; diff --git a/packages/cli/src/ui/hooks/useSlashCompletion.test.ts b/packages/cli/src/ui/hooks/useSlashCompletion.test.ts index 79d7e33bf7..b92c073de4 100644 --- a/packages/cli/src/ui/hooks/useSlashCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useSlashCompletion.test.ts @@ -14,11 +14,11 @@ import * as fs from 'fs/promises'; import * as path from 'path'; import * as os from 'os'; import { - CommandContext, - SlashCommand, + type CommandContext, + type SlashCommand, CommandKind, } from '../commands/types.js'; -import { Config, FileDiscoveryService } from '@vybestack/llxprt-code-core'; +import { type Config, FileDiscoveryService } from '@vybestack/llxprt-code-core'; import { useTextBuffer } from '../components/shared/text-buffer.js'; describe('useSlashCompletion', () => { @@ -1182,7 +1182,6 @@ describe('useSlashCompletion', () => { testRootDir, [], mockCommandContext, - undefined, ), ); diff --git a/packages/cli/src/ui/hooks/useSlashCompletion.tsx b/packages/cli/src/ui/hooks/useSlashCompletion.tsx index 462508e41b..08b1fc434d 100644 --- a/packages/cli/src/ui/hooks/useSlashCompletion.tsx +++ b/packages/cli/src/ui/hooks/useSlashCompletion.tsx @@ -13,32 +13,23 @@ import { escapePath, unescapePath, getErrorMessage, - Config, - FileDiscoveryService, + type Config, + type FileDiscoveryService, DEFAULT_FILE_FILTERING_OPTIONS, SHELL_SPECIAL_CHARS, DebugLogger, } from '@vybestack/llxprt-code-core'; -import { Suggestion } from '../components/SuggestionsDisplay.js'; -import { CommandContext, SlashCommand } from '../commands/types.js'; +import type { Suggestion } from '../components/SuggestionsDisplay.js'; +import type { CommandContext, SlashCommand } from '../commands/types.js'; import { logicalPosToOffset, - TextBuffer, + type TextBuffer, } from '../components/shared/text-buffer.js'; import { isSlashCommand } from '../utils/commandUtils.js'; import { toCodePoints } from '../utils/textUtils.js'; import { useCompletion } from './useCompletion.js'; import { createCompletionHandler } from '../commands/schema/index.js'; -/** - * @plan:PLAN-20251013-AUTOCOMPLETE.P08 - * @requirement:REQ-002 - * @requirement:REQ-003 - * @requirement:REQ-004 - * Feature flag enabled for hint display integration - */ -const SHOW_ARGUMENT_HINTS = true; - /** * @plan:PLAN-20251013-AUTOCOMPLETE.P05 * @requirement:REQ-001 @@ -213,7 +204,7 @@ export function useSlashCompletion( `useEffect triggered - commandIndex: ${commandIndex}, reverseSearchActive: ${reverseSearchActive}`, ); - if (!memoizedInput) return; + if (memoizedInput == null) return; const currentLine = memoizedInput.line; @@ -248,13 +239,13 @@ export function useSlashCompletion( const completeParts = hasTrailingSpace ? rawParts : rawParts.slice(0, -1); for (const part of completeParts) { - if (!currentLevel) { + if (currentLevel == null) { break; } const found = currentLevel.find( - (cmd) => cmd.name === part || cmd.altNames?.includes(part), + (cmd) => cmd.name === part || cmd.altNames?.includes(part) === true, ); - if (!found) { + if (found == null) { break; } pathParts.push(part); @@ -285,16 +276,17 @@ export function useSlashCompletion( } let exactMatchAsParent: SlashCommand | undefined; - if (!hasTrailingSpace && currentLevel) { + if (!hasTrailingSpace && currentLevel != null) { const candidate = commandPartial || argumentPartial; if (candidate) { exactMatchAsParent = currentLevel.find( (cmd) => - (cmd.name === candidate || cmd.altNames?.includes(candidate)) && - cmd.subCommands, + (cmd.name === candidate || + cmd.altNames?.includes(candidate) === true) && + cmd.subCommands !== undefined, ); - if (exactMatchAsParent) { + if (exactMatchAsParent != null) { // Only descend if there are NO other matches for the partial at this level. // This ensures that typing "/memory" still shows "/memory-leak" if it exists. const otherMatches = currentLevel.filter( @@ -303,7 +295,7 @@ export function useSlashCompletion( (cmd.name.toLowerCase().startsWith(candidate.toLowerCase()) || cmd.altNames?.some((alt) => alt.toLowerCase().startsWith(candidate.toLowerCase()), - )), + ) === true), ); if (otherMatches.length === 0) { @@ -323,22 +315,22 @@ export function useSlashCompletion( // Check for perfect, executable match if (!hasTrailingSpace) { if ( - leafCommand && + leafCommand != null && commandPartial === '' && argumentPartial === '' && - leafCommand.action && + leafCommand.action != null && !leafSupportsArguments ) { setIsPerfectMatch(true); setActiveHint(''); - } else if (currentLevel && commandPartial) { + } else if (currentLevel != null && commandPartial) { const perfectMatch = currentLevel.find( (cmd) => (cmd.name === commandPartial || - cmd.altNames?.includes(commandPartial)) && - cmd.action, + cmd.altNames?.includes(commandPartial) === true) && + cmd.action !== undefined, ); - if (perfectMatch) { + if (perfectMatch != null) { setIsPerfectMatch(true); setActiveHint(''); } @@ -353,7 +345,7 @@ export function useSlashCompletion( ? argumentPartial : commandPartial; - if (hasTrailingSpace || exactMatchAsParent) { + if (hasTrailingSpace || exactMatchAsParent != null) { completionStart.current = currentLine.length; completionEnd.current = currentLine.length; } else if (activePartial) { @@ -379,7 +371,7 @@ export function useSlashCompletion( const argString = argsForHandler.join(' '); // Check if command has schema-based completion - if (leafCommand!.schema) { + if (leafCommand!.schema != null) { /** * @plan:PLAN-20251013-AUTOCOMPLETE.P08 * @requirement:REQ-002 @@ -426,11 +418,7 @@ export function useSlashCompletion( setShowSuggestions(finalSuggestions.length > 0); setActiveSuggestionIndex(finalSuggestions.length > 0 ? 0 : -1); - if (SHOW_ARGUMENT_HINTS) { - setActiveHint(completionResult.hint || ''); - } else { - setActiveHint(''); - } + setActiveHint(completionResult.hint); setIsLoadingSuggestions(false); }) @@ -452,13 +440,14 @@ export function useSlashCompletion( } // Command/Sub-command Completion - const commandsToSearch = currentLevel || []; + const commandsToSearch = currentLevel ?? []; debugLogger.debug( () => `Commands to search: ${commandsToSearch.length}, Partial: "${commandPartial}"`, ); debugLogger.debug( - () => `currentLevel: ${currentLevel ? 'exists' : 'null/undefined'}`, + () => + `currentLevel: ${currentLevel != null ? 'exists' : 'null/undefined'}`, ); debugLogger.debug(() => `slashCommands at root: ${slashCommands.length}`); if (commandsToSearch.length > 0) { @@ -466,11 +455,14 @@ export function useSlashCompletion( // Filter extension commands: must have extensionName AND be enabled if (cmd.kind === 'extension') { // Extension commands without extensionName are treated as invalid/disabled - if (!cmd.extensionName) { + if (cmd.extensionName === undefined || cmd.extensionName === '') { return false; } - const config = commandContext.services?.config; - if (config && typeof config.isExtensionEnabled === 'function') { + const config = commandContext.services.config; + if ( + config != null && + typeof config.isExtensionEnabled === 'function' + ) { if (!config.isExtensionEnabled(cmd.extensionName)) { return false; } @@ -478,9 +470,10 @@ export function useSlashCompletion( } // Match by name or altNames return ( - cmd.description && + cmd.description.length > 0 && (cmd.name.startsWith(commandPartial) || - cmd.altNames?.some((alt) => alt.startsWith(commandPartial))) + cmd.altNames?.some((alt) => alt.startsWith(commandPartial)) === + true) ); }); debugLogger.debug( @@ -490,9 +483,11 @@ export function useSlashCompletion( // Sort potentialSuggestions so that exact match (by name or altName) comes first const sortedSuggestions = [...potentialSuggestions].sort((a, b) => { const aIsExact = - a.name === commandPartial || a.altNames?.includes(commandPartial); + a.name === commandPartial || + a.altNames?.includes(commandPartial) === true; const bIsExact = - b.name === commandPartial || b.altNames?.includes(commandPartial); + b.name === commandPartial || + b.altNames?.includes(commandPartial) === true; if (aIsExact && !bIsExact) return -1; if (!aIsExact && bIsExact) return 1; return 0; @@ -597,8 +592,10 @@ export function useSlashCompletion( // Check if this entry should be ignored by filtering options if ( - fileDiscovery && - fileDiscovery.shouldIgnoreFile(entryPathFromRoot, filterOptions) + fileDiscovery?.shouldIgnoreFile( + entryPathFromRoot, + filterOptions, + ) === true ) { continue; } @@ -656,12 +653,9 @@ export function useSlashCompletion( }); const suggestions: Suggestion[] = files - .filter((file) => { - if (fileDiscoveryService) { - return !fileDiscoveryService.shouldIgnoreFile(file, filterOptions); - } - return true; - }) + .filter( + (file) => !fileDiscoveryService.shouldIgnoreFile(file, filterOptions), + ) .map((file: string) => { const absolutePath = path.resolve(searchDir, file); const label = path.relative(cwd, absolutePath); @@ -686,7 +680,8 @@ export function useSlashCompletion( setIsLoadingSuggestions(true); }, 200); // Only show loading if operation takes more than 200ms - const fileDiscoveryService = config ? config.getFileService() : null; + const fileDiscoveryService = + config != null ? config.getFileService() : null; const enableRecursiveSearch = config?.getEnableRecursiveFileSearch() ?? true; const filterOptions = @@ -701,7 +696,7 @@ export function useSlashCompletion( prefix && enableRecursiveSearch ) { - if (fileDiscoveryService) { + if (fileDiscoveryService != null) { fetchedSuggestionsPerDir = await findFilesWithGlob( prefix, fileDiscoveryService, @@ -738,11 +733,10 @@ export function useSlashCompletion( path.join(baseDirAbsolute, entry.name), ); if ( - fileDiscoveryService && - fileDiscoveryService.shouldIgnoreFile( + fileDiscoveryService?.shouldIgnoreFile( relativePath, filterOptions, - ) + ) === true ) { continue; } @@ -782,8 +776,8 @@ export function useSlashCompletion( // Sort by depth, then directories first, then alphabetically fetchedSuggestions.sort((a, b) => { - const depthA = (a.label.match(/\//g) || []).length; - const depthB = (b.label.match(/\//g) || []).length; + const depthA = (a.label.match(/\//g) ?? []).length; + const depthB = (b.label.match(/\//g) ?? []).length; if (depthA !== depthB) { return depthA - depthB; @@ -804,9 +798,10 @@ export function useSlashCompletion( b.label.length - path.extname(b.label).length, ); - return ( - filenameA.localeCompare(filenameB) || a.label.localeCompare(b.label) - ); + const nameCompare = filenameA.localeCompare(filenameB); + return nameCompare !== 0 + ? nameCompare + : a.label.localeCompare(b.label); }); if (isMounted) { @@ -835,9 +830,7 @@ export function useSlashCompletion( } // Clear the loading timer if it hasn't fired yet - if (loadingTimer) { - clearTimeout(loadingTimer); - } + clearTimeout(loadingTimer); if (isMounted) { setIsLoadingSuggestions(false); @@ -849,12 +842,14 @@ export function useSlashCompletion( let debounceTimeout: NodeJS.Timeout | undefined; if (codePoints[commandIndex] === '@') { // File operations are expensive and benefit from debouncing - debounceTimeout = setTimeout(fetchSuggestions, 100); + debounceTimeout = setTimeout(() => { + void fetchSuggestions(); + }, 100); } return () => { isMounted = false; - if (debounceTimeout) { + if (debounceTimeout != null) { clearTimeout(debounceTimeout); } }; @@ -869,7 +864,7 @@ export function useSlashCompletion( cwd, slashCommands, commandContext, - commandContext.services?.config, // Add explicit dependency on config to catch changes + commandContext.services.config, // Add explicit dependency on config to catch changes config, // These are the setters - they're stable references resetCompletionState, @@ -887,7 +882,7 @@ export function useSlashCompletion( return null; } const suggestion = suggestions[suggestionIndex]; - return suggestionCommandMapRef.current.get(suggestion.value) || null; + return suggestionCommandMapRef.current.get(suggestion.value) ?? null; }, [suggestions], ); diff --git a/packages/cli/src/ui/hooks/useStableCallback.ts b/packages/cli/src/ui/hooks/useStableCallback.ts index cfed89bd34..9e65000494 100644 --- a/packages/cli/src/ui/hooks/useStableCallback.ts +++ b/packages/cli/src/ui/hooks/useStableCallback.ts @@ -38,12 +38,10 @@ export function useStableCallback unknown>( }); // Return a stable callback that always calls the latest version - const stableCallback = useCallback( + return useCallback( (...args: Parameters) => callbackRef.current(...args), [], // Empty deps = stable reference ) as T; - - return stableCallback; } /** diff --git a/packages/cli/src/ui/hooks/useTerminalSize.ts b/packages/cli/src/ui/hooks/useTerminalSize.ts index d58a87dc15..3e9ae7d7b9 100644 --- a/packages/cli/src/ui/hooks/useTerminalSize.ts +++ b/packages/cli/src/ui/hooks/useTerminalSize.ts @@ -22,7 +22,7 @@ export function useTerminalSize(): { columns: number; rows: number } { function updateSize() { // Clear previous timeout - if (resizeTimeout) { + if (resizeTimeout != null) { clearTimeout(resizeTimeout); } @@ -45,7 +45,7 @@ export function useTerminalSize(): { columns: number; rows: number } { return () => { process.stdout.off('resize', updateSize); - if (resizeTimeout) { + if (resizeTimeout != null) { clearTimeout(resizeTimeout); } }; diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts index 478955caab..d6e77aa564 100644 --- a/packages/cli/src/ui/hooks/useThemeCommand.ts +++ b/packages/cli/src/ui/hooks/useThemeCommand.ts @@ -6,11 +6,11 @@ import { useCallback, useEffect } from 'react'; import { themeManager } from '../themes/theme-manager.js'; -import { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting +import type { LoadedSettings, SettingScope } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting import { type HistoryItem, MessageType } from '../types.js'; import process from 'node:process'; import { useAppDispatch } from '../contexts/AppDispatchContext.js'; -import { AppState } from '../reducers/appReducer.js'; +import type { AppState } from '../reducers/appReducer.js'; interface UseThemeCommandReturn { isThemeDialogOpen: boolean; diff --git a/packages/cli/src/ui/hooks/useTimer.ts b/packages/cli/src/ui/hooks/useTimer.ts index f07da383d6..a9be6bf090 100644 --- a/packages/cli/src/ui/hooks/useTimer.ts +++ b/packages/cli/src/ui/hooks/useTimer.ts @@ -40,21 +40,19 @@ export const useTimer = (isActive: boolean, resetKey: unknown) => { if (isActive) { // Clear previous interval unconditionally before starting a new one // This handles resetKey changes while active, ensuring a fresh interval start. - if (timerRef.current) { + if (timerRef.current != null) { clearInterval(timerRef.current); } timerRef.current = setInterval(() => { setElapsedTime((prev) => prev + 1); }, 1000); - } else { - if (timerRef.current) { - clearInterval(timerRef.current); - timerRef.current = null; - } + } else if (timerRef.current != null) { + clearInterval(timerRef.current); + timerRef.current = null; } return () => { - if (timerRef.current) { + if (timerRef.current != null) { clearInterval(timerRef.current); timerRef.current = null; } diff --git a/packages/cli/src/ui/hooks/useTodoContinuation.ts b/packages/cli/src/ui/hooks/useTodoContinuation.ts index bdd76438d6..d3deb6e1d2 100644 --- a/packages/cli/src/ui/hooks/useTodoContinuation.ts +++ b/packages/cli/src/ui/hooks/useTodoContinuation.ts @@ -6,9 +6,9 @@ import { useState, useRef, useCallback, useEffect } from 'react'; import { - Config, - GeminiClient, - Todo, + type Config, + type GeminiClient, + type Todo, ApprovalMode, } from '@vybestack/llxprt-code-core'; import { useTodoContext } from '../contexts/TodoContext.js'; @@ -84,9 +84,9 @@ export const useTodoContinuation = ( return { streamCompleted: true, noToolCallsMade: !hadBlockingToolCalls, - hasActiveTodos, continuationEnabled: isEnabled, notAlreadyContinuing: !continuationState.isActive, + hasActiveTodos, todoPaused, }; }, @@ -221,11 +221,7 @@ export const useTodoContinuation = ( // Find the most relevant active todo const activeTodo = _findMostRelevantActiveTodo(todoContext.todos); - if ( - !activeTodo || - !activeTodo.content || - activeTodo.content.trim() === '' - ) { + if (!activeTodo?.content || activeTodo.content.trim() === '') { return; } @@ -254,8 +250,8 @@ export const useTodoContinuation = ( todoContext.setPaused(true); return { type: 'pause' as const, - reason, message: `Task paused: ${reason}`, + reason, }; }, [todoContext], diff --git a/packages/cli/src/ui/hooks/useTodoPausePreserver.test.ts b/packages/cli/src/ui/hooks/useTodoPausePreserver.test.ts index 9d59fcd8a1..ebd4621329 100644 --- a/packages/cli/src/ui/hooks/useTodoPausePreserver.test.ts +++ b/packages/cli/src/ui/hooks/useTodoPausePreserver.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect } from 'vitest'; import fc from 'fast-check'; -import { type Todo } from '@vybestack/llxprt-code-core'; +import type { Todo } from '@vybestack/llxprt-code-core'; import { shouldClearTodos } from './useTodoPausePreserver.js'; /** diff --git a/packages/cli/src/ui/hooks/useTodoPausePreserver.ts b/packages/cli/src/ui/hooks/useTodoPausePreserver.ts index 4eeda2a975..defe7dfb23 100644 --- a/packages/cli/src/ui/hooks/useTodoPausePreserver.ts +++ b/packages/cli/src/ui/hooks/useTodoPausePreserver.ts @@ -5,7 +5,7 @@ */ import { useCallback } from 'react'; -import { type Todo } from '@vybestack/llxprt-code-core'; +import type { Todo } from '@vybestack/llxprt-code-core'; /** * @plan PLAN-20260129-TODOPERSIST.P03 diff --git a/packages/cli/src/ui/hooks/useToolScheduler.test.ts b/packages/cli/src/ui/hooks/useToolScheduler.test.ts index 534d4e4bc9..5b1cd509d5 100644 --- a/packages/cli/src/ui/hooks/useToolScheduler.test.ts +++ b/packages/cli/src/ui/hooks/useToolScheduler.test.ts @@ -4,16 +4,17 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; +import type { Mock } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { renderHook, cleanup } from '../../test-utils/render.js'; import { act } from 'react'; import { useReactToolScheduler } from './useReactToolScheduler.js'; import { mapToDisplay } from './toolMapping.js'; -import { - ApprovalMode, +import type { AnyDeclarativeTool, AnyToolInvocation, CompletedToolCall, + ApprovalMode, type Config, type CoreToolScheduler, type MessageBus, @@ -416,11 +417,10 @@ const buildMockScheduler = ( config: nextConfig, }; }), - toolCalls: [], + toolRegistry: mockToolRegistry as unknown as ToolRegistry, callbacks, config, - toolRegistry: mockToolRegistry as unknown as ToolRegistry, }; return scheduler; }; @@ -1370,8 +1370,8 @@ describe('mapToDisplay', () => { it(`should map ToolCall with status '${status}' (${testName}) correctly`, () => { const toolCall: ToolCall = { request: baseRequest, - status, ...(extraProps || {}), + status, } as ToolCall; const display = mapToDisplay(toolCall); diff --git a/packages/cli/src/ui/hooks/useToolsDialog.ts b/packages/cli/src/ui/hooks/useToolsDialog.ts index bd1185803e..10ba560b42 100644 --- a/packages/cli/src/ui/hooks/useToolsDialog.ts +++ b/packages/cli/src/ui/hooks/useToolsDialog.ts @@ -5,10 +5,10 @@ */ import { useCallback, useState } from 'react'; -import { AnyDeclarativeTool, Config } from '@vybestack/llxprt-code-core'; +import type { AnyDeclarativeTool, Config } from '@vybestack/llxprt-code-core'; import { MessageType } from '../types.js'; import { useAppDispatch } from '../contexts/AppDispatchContext.js'; -import { AppState } from '../reducers/appReducer.js'; +import type { AppState } from '../reducers/appReducer.js'; interface UseToolsDialogParams { addMessage: (msg: { @@ -107,7 +107,7 @@ export const useToolsDialog = ({ const handleSelect = useCallback( (toolName: string) => { const selectedTool = availableTools.find((t) => t.name === toolName); - if (!selectedTool) return; + if (selectedTool == null) return; // Update disabled tools list let updatedDisabledTools: string[]; diff --git a/packages/cli/src/ui/hooks/useWelcomeOnboarding.ts b/packages/cli/src/ui/hooks/useWelcomeOnboarding.ts index 4c92f04955..2074fd3bec 100644 --- a/packages/cli/src/ui/hooks/useWelcomeOnboarding.ts +++ b/packages/cli/src/ui/hooks/useWelcomeOnboarding.ts @@ -6,7 +6,7 @@ import { useState, useCallback, useEffect } from 'react'; import { DebugLogger } from '@vybestack/llxprt-code-core'; -import { type LoadedSettings } from '../../config/settings.js'; +import type { LoadedSettings } from '../../config/settings.js'; import { isWelcomeCompleted, markWelcomeCompleted, @@ -39,9 +39,9 @@ export interface WelcomeState { export interface WelcomeActions { startSetup: () => void; selectProvider: (providerId: string) => void; - selectModel: (modelId: string) => void; + selectModel: (modelId: string) => Promise; selectAuthMethod: (method: 'oauth' | 'api_key') => void; - onAuthComplete: () => void; + onAuthComplete: () => Promise; onAuthError: (error: string) => void; skipSetup: () => void; goBack: () => void; @@ -98,13 +98,9 @@ export const useWelcomeOnboarding = ( // Load available providers on mount useEffect(() => { const providerManager = runtime.getCliProviderManager(); - if (providerManager) { - const providers = providerManager.listProviders(); - setAvailableProviders(providers); - debug.log( - `Loaded ${providers.length} providers: ${providers.join(', ')}`, - ); - } + const providers = providerManager.listProviders(); + setAvailableProviders(providers); + debug.log(`Loaded ${providers.length} providers: ${providers.join(', ')}`); }, [runtime]); // Load available models when provider is selected @@ -229,8 +225,8 @@ export const useWelcomeOnboarding = ( setState((prev) => ({ ...prev, authInProgress: false, - error, step: 'auth_method', + error, })); }, []); @@ -256,6 +252,9 @@ export const useWelcomeOnboarding = ( return { ...prev, step: 'auth_method', selectedModel: undefined }; case 'provider': return { ...prev, step: 'welcome' }; + case 'welcome': + case 'completion': + case 'skipped': default: return prev; } @@ -267,7 +266,7 @@ export const useWelcomeOnboarding = ( try { const providerManager = runtime.getCliProviderManager(); debug.log( - `[saveProfile] START name=${name}, active provider: ${providerManager?.getActiveProviderName()}`, + `[saveProfile] START name=${name}, active provider: ${providerManager.getActiveProviderName()}`, ); // Check if profile already exists @@ -298,7 +297,7 @@ export const useWelcomeOnboarding = ( `[saveProfile] Load result: ${JSON.stringify(loadResult, null, 2)}`, ); debug.log( - `[saveProfile] After load - active provider: ${providerManager?.getActiveProviderName()}`, + `[saveProfile] After load - active provider: ${providerManager.getActiveProviderName()}`, ); } catch (error) { debug.log(`[saveProfile] Failed: ${error}`); @@ -340,12 +339,12 @@ export const useWelcomeOnboarding = ( const providerManager = runtime.getCliProviderManager(); debug.log( - `[triggerAuth] Before auth - current active: ${providerManager?.getActiveProviderName()}`, + `[triggerAuth] Before auth - current active: ${providerManager.getActiveProviderName()}`, ); // Authenticate FIRST before switching provider (prevents double OAuth) if (method === 'oauth') { - if (!oauthManager) { + if (oauthManager == null) { throw new Error('OAuth manager not available'); } debug.log(`[triggerAuth] Starting OAuth for ${provider}`); @@ -360,7 +359,7 @@ export const useWelcomeOnboarding = ( autoOAuth: false, }); debug.log( - `[triggerAuth] After switchActiveProvider - changed: ${switchResult.changed}, now active: ${providerManager?.getActiveProviderName()}`, + `[triggerAuth] After switchActiveProvider - changed: ${switchResult.changed}, now active: ${providerManager.getActiveProviderName()}`, ); // For API key method, set the key after switching provider @@ -378,7 +377,7 @@ export const useWelcomeOnboarding = ( } debug.log( - `[triggerAuth] END - active provider: ${providerManager?.getActiveProviderName()}`, + `[triggerAuth] END - active provider: ${providerManager.getActiveProviderName()}`, ); }, [runtime], diff --git a/packages/cli/src/ui/hooks/vim.test.ts b/packages/cli/src/ui/hooks/vim.test.ts index f1e0ad9a30..6bd259d5cf 100644 --- a/packages/cli/src/ui/hooks/vim.test.ts +++ b/packages/cli/src/ui/hooks/vim.test.ts @@ -5,7 +5,8 @@ */ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import React, { act } from 'react'; +import type React from 'react'; +import { act } from 'react'; import { renderHook } from '../../test-utils/render.js'; import { useVim } from './vim.js'; import type { TextBuffer } from '../components/shared/buffer-types.js'; @@ -86,14 +87,12 @@ describe('useVim hook', () => { const lines = text.split('\n'); return { - lines, get cursor() { return cursorState.pos; }, set cursor(newPos: [number, number]) { cursorState.pos = newPos; }, - text, move: vi.fn().mockImplementation((direction: string) => { let [row, col] = cursorState.pos; const _line = lines[row] || ''; @@ -115,7 +114,6 @@ describe('useVim hook', () => { replaceRangeByOffset: vi.fn(), handleInput: vi.fn(), setText: vi.fn(), - // Vim-specific methods vimDeleteWordForward: vi.fn(), vimDeleteWordBackward: vi.fn(), vimDeleteWordEnd: vi.fn(), @@ -161,6 +159,8 @@ describe('useVim hook', () => { cursorState.pos = [row, col - 1]; } }), + lines, + text, }; }; @@ -715,11 +715,11 @@ describe('useVim hook', () => { result.current.handleInput({ sequence: 'a' }); }); expect(result.current.mode).toBe('INSERT'); - expect(testBuffer.cursor).toEqual([0, 11]); + expect(testBuffer.cursor).toStrictEqual([0, 11]); exitInsertMode(result); expect(result.current.mode).toBe('NORMAL'); - expect(testBuffer.cursor).toEqual([0, 10]); + expect(testBuffer.cursor).toStrictEqual([0, 10]); }); }); @@ -805,7 +805,7 @@ describe('useVim hook', () => { }); // Should delete "hello " (word + space), leaving "world test" - expect(result.lines).toEqual(['world test']); + expect(result.lines).toStrictEqual(['world test']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(0); }); @@ -819,7 +819,7 @@ describe('useVim hook', () => { }); // Should delete "llo " (rest of word + space), leaving "he world test" - expect(result.lines).toEqual(['heworld test']); + expect(result.lines).toStrictEqual(['heworld test']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(2); }); @@ -833,7 +833,7 @@ describe('useVim hook', () => { }); // Should delete "world" (no trailing space at end), leaving "hello " - expect(result.lines).toEqual(['hello ']); + expect(result.lines).toStrictEqual(['hello ']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(6); }); @@ -1298,7 +1298,7 @@ describe('useVim hook', () => { }); // Should delete "ello" (from cursor to end of word), leaving "h world test" - expect(result.lines).toEqual(['h world test']); + expect(result.lines).toStrictEqual(['h world test']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(1); }); @@ -1316,7 +1316,7 @@ describe('useVim hook', () => { }); // Should delete "ello world" (to end of second word), leaving "h test more" - expect(result.lines).toEqual(['h test more']); + expect(result.lines).toStrictEqual(['h test more']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(1); }); @@ -1332,7 +1332,7 @@ describe('useVim hook', () => { }); // Should delete "world" (previous word only), leaving "hello test" - expect(result.lines).toEqual(['hello test']); + expect(result.lines).toStrictEqual(['hello test']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(6); }); @@ -1350,7 +1350,7 @@ describe('useVim hook', () => { }); // Should delete "world test " (two words backward), leaving "hello more" - expect(result.lines).toEqual(['hello more']); + expect(result.lines).toStrictEqual(['hello more']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(6); }); @@ -1366,7 +1366,7 @@ describe('useVim hook', () => { }); // Should delete "hello " (word + space), leaving "world test" - expect(result.lines).toEqual(['world test']); + expect(result.lines).toStrictEqual(['world test']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(0); }); @@ -1384,7 +1384,7 @@ describe('useVim hook', () => { }); // Should delete "hello world " (two words), leaving "test more" - expect(result.lines).toEqual(['test more']); + expect(result.lines).toStrictEqual(['test more']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(0); }); @@ -1400,7 +1400,7 @@ describe('useVim hook', () => { }); // Should delete "ello" (from cursor to end of word), leaving "h world test" - expect(result.lines).toEqual(['h world test']); + expect(result.lines).toStrictEqual(['h world test']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(1); }); @@ -1414,7 +1414,7 @@ describe('useVim hook', () => { }); // Should delete "ello world" (to end of second word), leaving "h test" - expect(result.lines).toEqual(['h test']); + expect(result.lines).toStrictEqual(['h test']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(1); }); @@ -1430,7 +1430,7 @@ describe('useVim hook', () => { }); // Should delete "world" (previous word only), leaving "hello test" - expect(result.lines).toEqual(['hello test']); + expect(result.lines).toStrictEqual(['hello test']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(6); }); @@ -1445,7 +1445,7 @@ describe('useVim hook', () => { payload: { count: 1 }, }); - expect(result.lines).toEqual(['']); + expect(result.lines).toStrictEqual(['']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(0); }); @@ -1464,7 +1464,7 @@ describe('useVim hook', () => { payload: { count: 1 }, }); - expect(result.lines).toEqual(['line1', 'line3']); + expect(result.lines).toStrictEqual(['line1', 'line3']); expect(result.cursorRow).toBe(1); expect(result.cursorCol).toBe(0); }); @@ -1482,7 +1482,7 @@ describe('useVim hook', () => { }); // Should delete lines 1 and 2 - expect(result.lines).toEqual(['line1', 'line4']); + expect(result.lines).toStrictEqual(['line1', 'line4']); expect(result.cursorRow).toBe(1); expect(result.cursorCol).toBe(0); }); @@ -1496,7 +1496,7 @@ describe('useVim hook', () => { }); // Should leave an empty line when deleting the only line - expect(result.lines).toEqual(['']); + expect(result.lines).toStrictEqual(['']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(0); }); @@ -1511,7 +1511,7 @@ describe('useVim hook', () => { }); // Should delete "world test", leaving "hello " - expect(result.lines).toEqual(['hello ']); + expect(result.lines).toStrictEqual(['hello ']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(6); }); @@ -1524,7 +1524,7 @@ describe('useVim hook', () => { }); // Should not change anything when at end of line - expect(result.lines).toEqual(['hello world']); + expect(result.lines).toStrictEqual(['hello world']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(11); }); @@ -1539,7 +1539,7 @@ describe('useVim hook', () => { }); // Should delete "world test", leaving "hello " - expect(result.lines).toEqual(['hello ']); + expect(result.lines).toStrictEqual(['hello ']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(6); }); @@ -1552,7 +1552,7 @@ describe('useVim hook', () => { }); // Should delete entire line content - expect(result.lines).toEqual(['']); + expect(result.lines).toStrictEqual(['']); expect(result.cursorRow).toBe(0); expect(result.cursorCol).toBe(0); }); diff --git a/packages/cli/src/ui/hooks/vim.ts b/packages/cli/src/ui/hooks/vim.ts index b5a96bc1a2..5ee2b1a934 100644 --- a/packages/cli/src/ui/hooks/vim.ts +++ b/packages/cli/src/ui/hooks/vim.ts @@ -283,7 +283,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) { !normalizedKey.ctrl && !normalizedKey.meta ) { - if (buffer.text.trim() && onSubmit) { + if (buffer.text.trim() && onSubmit != null) { // Handle command submission directly const submittedValue = buffer.text; buffer.setText(''); @@ -689,7 +689,7 @@ export function useVim(buffer: TextBuffer, onSubmit?: (value: string) => void) { case '.': { // Repeat last command - if (state.lastCommand) { + if (state.lastCommand != null) { const cmdData = state.lastCommand; // All repeatable commands are now handled by executeCommand diff --git a/packages/cli/src/ui/inkRenderOptions.test.ts b/packages/cli/src/ui/inkRenderOptions.test.ts index 3c9d4bd28b..fe27f3d611 100644 --- a/packages/cli/src/ui/inkRenderOptions.test.ts +++ b/packages/cli/src/ui/inkRenderOptions.test.ts @@ -18,7 +18,7 @@ describe('inkRenderOptions', () => { }, ); - expect(options).toEqual( + expect(options).toStrictEqual( expect.objectContaining({ exitOnCtrlC: false, patchConsole: false, @@ -35,7 +35,7 @@ describe('inkRenderOptions', () => { { merged: { ui: { useAlternateBuffer: true } } }, ); - expect(options).toEqual( + expect(options).toStrictEqual( expect.objectContaining({ exitOnCtrlC: false, patchConsole: false, @@ -56,7 +56,7 @@ describe('inkRenderOptions', () => { }, ); - expect(options).toEqual( + expect(options).toStrictEqual( expect.objectContaining({ exitOnCtrlC: false, patchConsole: false, @@ -73,7 +73,7 @@ describe('inkRenderOptions', () => { { merged: { ui: { useAlternateBuffer: false } } }, ); - expect(options).toEqual( + expect(options).toStrictEqual( expect.objectContaining({ exitOnCtrlC: false, patchConsole: false, diff --git a/packages/cli/src/ui/inkRenderOptions.ts b/packages/cli/src/ui/inkRenderOptions.ts index e20082fccf..c4cb9c6918 100644 --- a/packages/cli/src/ui/inkRenderOptions.ts +++ b/packages/cli/src/ui/inkRenderOptions.ts @@ -42,8 +42,8 @@ export const inkRenderOptions = ( stderr: sharedStdio.stderr, exitOnCtrlC: false, patchConsole: false, - isScreenReaderEnabled, alternateBuffer: useAlternateBuffer, + isScreenReaderEnabled, incrementalRendering, }; }; diff --git a/packages/cli/src/ui/keyMatchers.test.ts b/packages/cli/src/ui/keyMatchers.test.ts index 31ba18a608..e9af6f4a96 100644 --- a/packages/cli/src/ui/keyMatchers.test.ts +++ b/packages/cli/src/ui/keyMatchers.test.ts @@ -6,7 +6,10 @@ import { describe, it, expect } from 'vitest'; import { keyMatchers, Command, createKeyMatchers } from './keyMatchers.js'; -import { KeyBindingConfig, defaultKeyBindings } from '../config/keyBindings.js'; +import { + type KeyBindingConfig, + defaultKeyBindings, +} from '../config/keyBindings.js'; import type { Key } from './hooks/useKeypress.js'; describe('keyMatchers', () => { diff --git a/packages/cli/src/ui/keyMatchers.ts b/packages/cli/src/ui/keyMatchers.ts index d9e2c68f05..bc1366a29c 100644 --- a/packages/cli/src/ui/keyMatchers.ts +++ b/packages/cli/src/ui/keyMatchers.ts @@ -7,8 +7,8 @@ import type { Key } from './hooks/useKeypress.js'; import { Command, - KeyBinding, - KeyBindingConfig, + type KeyBinding, + type KeyBindingConfig, defaultKeyBindings, } from '../config/keyBindings.js'; diff --git a/packages/cli/src/ui/layouts/DefaultAppLayout.tsx b/packages/cli/src/ui/layouts/DefaultAppLayout.tsx index b17304207c..43f3e73a58 100644 --- a/packages/cli/src/ui/layouts/DefaultAppLayout.tsx +++ b/packages/cli/src/ui/layouts/DefaultAppLayout.tsx @@ -9,8 +9,8 @@ import { Box, type DOMElement, Static, Text } from 'ink'; import type { Config } from '@vybestack/llxprt-code-core'; import { ApprovalMode } from '@vybestack/llxprt-code-core'; import { StreamingState } from '../types.js'; -import { LoadedSettings } from '../../config/settings.js'; -import { UpdateObject } from '../utils/updateCheck.js'; +import type { LoadedSettings } from '../../config/settings.js'; +import type { UpdateObject } from '../utils/updateCheck.js'; import { useUIState } from '../contexts/UIStateContext.js'; import type { UIState } from '../contexts/UIStateContext.js'; import { useUIActions } from '../contexts/UIActionsContext.js'; @@ -353,7 +353,7 @@ export const DefaultAppLayout = ({ ], ); - if (quittingMessages) { + if (quittingMessages != null) { return ( {quittingMessages.map((item) => ( diff --git a/packages/cli/src/ui/oauth-submission.ts b/packages/cli/src/ui/oauth-submission.ts index 31435dcce4..2d571f42ce 100644 --- a/packages/cli/src/ui/oauth-submission.ts +++ b/packages/cli/src/ui/oauth-submission.ts @@ -41,7 +41,7 @@ export function submitOAuthCode( } const oauthManager = deps.getOAuthManager(); - if (!oauthManager) { + if (oauthManager == null) { return false; } diff --git a/packages/cli/src/ui/privacy/CloudFreePrivacyNotice.tsx b/packages/cli/src/ui/privacy/CloudFreePrivacyNotice.tsx index e6704b48b2..46fa9ecc7b 100644 --- a/packages/cli/src/ui/privacy/CloudFreePrivacyNotice.tsx +++ b/packages/cli/src/ui/privacy/CloudFreePrivacyNotice.tsx @@ -9,7 +9,7 @@ import { useCallback } from 'react'; import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js'; import { usePrivacySettings } from '../hooks/usePrivacySettings.js'; import { CloudPaidPrivacyNotice } from './CloudPaidPrivacyNotice.js'; -import { Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; import { Colors } from '../colors.js'; import { useKeypress } from '../hooks/useKeypress.js'; diff --git a/packages/cli/src/ui/privacy/MultiProviderPrivacyNotice.tsx b/packages/cli/src/ui/privacy/MultiProviderPrivacyNotice.tsx index 62013df9ab..e54fb8e35c 100644 --- a/packages/cli/src/ui/privacy/MultiProviderPrivacyNotice.tsx +++ b/packages/cli/src/ui/privacy/MultiProviderPrivacyNotice.tsx @@ -83,7 +83,7 @@ function getProviderInfo(providerName: string): ProviderInfo | null { function formatProviderName(providerName: string): string { const info = getProviderInfo(providerName); - if (info) { + if (info != null) { return info.displayName; } // Capitalize first letter for unknown providers @@ -151,7 +151,7 @@ export const MultiProviderPrivacyNotice = ({ ))} - ) : providerInfo ? ( + ) : providerInfo != null ? ( [Terms] {providerInfo.tosUrl} diff --git a/packages/cli/src/ui/privacy/PrivacyNotice.tsx b/packages/cli/src/ui/privacy/PrivacyNotice.tsx index dadd59049d..e1913ae7b2 100644 --- a/packages/cli/src/ui/privacy/PrivacyNotice.tsx +++ b/packages/cli/src/ui/privacy/PrivacyNotice.tsx @@ -5,7 +5,7 @@ */ import { Box } from 'ink'; -import { type Config } from '@vybestack/llxprt-code-core'; +import type { Config } from '@vybestack/llxprt-code-core'; import { GeminiPrivacyNotice } from './GeminiPrivacyNotice.js'; import { MultiProviderPrivacyNotice } from './MultiProviderPrivacyNotice.js'; @@ -29,7 +29,7 @@ const PrivacyNoticeText = ({ const activeProvider = providerManager?.getActiveProvider?.(); // If we have a non-Gemini provider active, show its specific notice - if (activeProvider && activeProvider.name !== 'gemini') { + if (activeProvider != null && activeProvider.name !== 'gemini') { return ( { describe('initial state', () => { it('should have correct initial state', () => { - expect(initialAppState).toEqual({ + expect(initialAppState).toStrictEqual({ openDialogs: { theme: false, auth: false, @@ -61,7 +61,7 @@ describe('appReducer', () => { const result = appReducer(initialAppState, action); - expect(result.lastAddItemAction).toEqual({ + expect(result.lastAddItemAction).toStrictEqual({ itemData, baseTimestamp: 1234567890, }); @@ -91,7 +91,7 @@ describe('appReducer', () => { payload: { itemData: secondItem, baseTimestamp: 2000 }, }); - expect(state2.lastAddItemAction).toEqual({ + expect(state2.lastAddItemAction).toStrictEqual({ itemData: secondItem, baseTimestamp: 2000, }); @@ -536,7 +536,7 @@ describe('appReducer', () => { }); // Verify complete state - expect(state.lastAddItemAction).toEqual({ + expect(state.lastAddItemAction).toStrictEqual({ itemData: { type: 'user', text: 'test' }, baseTimestamp: 1000, }); @@ -622,7 +622,7 @@ describe('appReducer', () => { lastAddItemAction: originalState.lastAddItemAction, }; - expect(stateAfter).toEqual(stateCopy); + expect(stateAfter).toStrictEqual(stateCopy); }); }); }); diff --git a/packages/cli/src/ui/reducers/appReducer.ts b/packages/cli/src/ui/reducers/appReducer.ts index 2c14c81796..03ebe110dd 100644 --- a/packages/cli/src/ui/reducers/appReducer.ts +++ b/packages/cli/src/ui/reducers/appReducer.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { HistoryItem } from '../types.js'; +import type { HistoryItem } from '../types.js'; export type AppAction = | { diff --git a/packages/cli/src/ui/reducers/sessionReducer.ts b/packages/cli/src/ui/reducers/sessionReducer.ts index 2d32c4aaa0..17b2cde41c 100644 --- a/packages/cli/src/ui/reducers/sessionReducer.ts +++ b/packages/cli/src/ui/reducers/sessionReducer.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { UserTierId } from '@vybestack/llxprt-code-core'; +import type { UserTierId } from '@vybestack/llxprt-code-core'; // Session state interface export interface SessionState { diff --git a/packages/cli/src/ui/state/extensions.ts b/packages/cli/src/ui/state/extensions.ts index 7e7cc83c74..24560b4c15 100644 --- a/packages/cli/src/ui/state/extensions.ts +++ b/packages/cli/src/ui/state/extensions.ts @@ -85,7 +85,7 @@ export function extensionUpdatesReducer( } case 'SET_NOTIFIED': { const existing = state.extensionStatuses.get(action.payload.name); - if (!existing || existing.notified === action.payload.notified) { + if (existing == null || existing.notified === action.payload.notified) { return state; } const newStatuses = new Map(state.extensionStatuses); diff --git a/packages/cli/src/ui/themes/color-utils.ts b/packages/cli/src/ui/themes/color-utils.ts index cce234ccb1..0d26d32f39 100644 --- a/packages/cli/src/ui/themes/color-utils.ts +++ b/packages/cli/src/ui/themes/color-utils.ts @@ -213,9 +213,8 @@ export function resolveColor(colorValue: string): string | undefined { if (lowerColor.startsWith('#')) { if (/^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) { return lowerColor; - } else { - return undefined; } + return undefined; } // 2. Check if it's an Ink supported name (lowercase) else if (INK_SUPPORTED_NAMES.has(lowerColor)) { @@ -322,15 +321,15 @@ export function detectTerminalBackgroundColor(): Promise { // ESC ] 11 ; rgb:RRRR/GGGG/BBBB ESC \ (ST terminator - standard) // ESC ] 11 ; rgb:RRRR/GGGG/BBBB BEL (BEL terminator - legacy terminals) // Match either terminator (case-insensitive hex) - const matchST = response.match( + const matchST = RegExp( /\x1b\]11;rgb:([0-9a-f]{4})\/([0-9a-f]{4})\/([0-9a-f]{4})\x1b\\/i, - ); - const matchBEL = response.match( + ).exec(response); + const matchBEL = RegExp( /\x1b\]11;rgb:([0-9a-f]{4})\/([0-9a-f]{4})\/([0-9a-f]{4})\x07/i, - ); + ).exec(response); const match = matchST || matchBEL; - if (match) { + if (match != null) { // Convert 16-bit RGB components to 8-bit hex // Take first 2 hex digits of each 4-digit component (high byte) const r = parseInt(match[1].substring(0, 2), 16); diff --git a/packages/cli/src/ui/themes/no-color.ts b/packages/cli/src/ui/themes/no-color.ts index 575dae3763..e28fc07263 100644 --- a/packages/cli/src/ui/themes/no-color.ts +++ b/packages/cli/src/ui/themes/no-color.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Theme, ColorsTheme, SemanticColors } from './theme.js'; +import { Theme, type ColorsTheme, type SemanticColors } from './theme.js'; const noColorColorsTheme: ColorsTheme = { type: 'ansi', diff --git a/packages/cli/src/ui/themes/semantic-resolver.test.ts b/packages/cli/src/ui/themes/semantic-resolver.test.ts index 1afe0ac711..97adfb21e0 100644 --- a/packages/cli/src/ui/themes/semantic-resolver.test.ts +++ b/packages/cli/src/ui/themes/semantic-resolver.test.ts @@ -6,8 +6,8 @@ import { describe, it, expect } from 'vitest'; import { resolveSemanticColors } from './semantic-resolver.js'; -import { SemanticColors } from './semantic-tokens.js'; -import { ColorsTheme } from './theme.js'; +import type { SemanticColors } from './semantic-tokens.js'; +import type { ColorsTheme } from './theme.js'; describe('semantic-resolver', () => { describe('resolveSemanticColors', () => { @@ -33,7 +33,7 @@ describe('semantic-resolver', () => { const semanticColors = resolveSemanticColors(darkTheme); - expect(semanticColors).toEqual({ + expect(semanticColors).toStrictEqual({ text: { primary: '#CDD6F4', secondary: '#6C7086', @@ -88,7 +88,7 @@ describe('semantic-resolver', () => { const semanticColors = resolveSemanticColors(lightTheme); - expect(semanticColors).toEqual({ + expect(semanticColors).toStrictEqual({ text: { primary: '#3C3C43', secondary: '#97a0b0', @@ -143,7 +143,7 @@ describe('semantic-resolver', () => { const semanticColors = resolveSemanticColors(ansiTheme); - expect(semanticColors).toEqual({ + expect(semanticColors).toStrictEqual({ text: { primary: 'white', secondary: 'gray', @@ -198,7 +198,7 @@ describe('semantic-resolver', () => { const semanticColors = resolveSemanticColors(customTheme); - expect(semanticColors).toEqual({ + expect(semanticColors).toStrictEqual({ text: { primary: '#EDF2F4', secondary: '#6C757D', diff --git a/packages/cli/src/ui/themes/semantic-resolver.ts b/packages/cli/src/ui/themes/semantic-resolver.ts index e8b0bf56f9..410458c78e 100644 --- a/packages/cli/src/ui/themes/semantic-resolver.ts +++ b/packages/cli/src/ui/themes/semantic-resolver.ts @@ -4,8 +4,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { ColorsTheme } from './theme.js'; -import { SemanticColors } from './semantic-tokens.js'; +import type { ColorsTheme } from './theme.js'; +import type { SemanticColors } from './semantic-tokens.js'; /** * Resolves a theme's colors to semantic color tokens. diff --git a/packages/cli/src/ui/themes/semantic-tokens.test.ts b/packages/cli/src/ui/themes/semantic-tokens.test.ts index 59b85ac418..72b80bcdb8 100644 --- a/packages/cli/src/ui/themes/semantic-tokens.test.ts +++ b/packages/cli/src/ui/themes/semantic-tokens.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { themeManager } from './theme-manager.js'; -import { SemanticColors } from './semantic-tokens.js'; +import type { SemanticColors } from './semantic-tokens.js'; import { getColorMigrationMapping } from './theme-compat.js'; import { Colors } from '../colors.js'; @@ -253,7 +253,7 @@ describe('semantic tokens system', () => { const semanticColors = themeManager.getSemanticColors(); // Verify structure matches interface - expect(semanticColors).toEqual({ + expect(semanticColors).toStrictEqual({ text: { primary: expect.any(String), secondary: expect.any(String), diff --git a/packages/cli/src/ui/themes/theme-manager.test.ts b/packages/cli/src/ui/themes/theme-manager.test.ts index 022df94621..1bf664de7c 100644 --- a/packages/cli/src/ui/themes/theme-manager.test.ts +++ b/packages/cli/src/ui/themes/theme-manager.test.ts @@ -11,8 +11,8 @@ if (process.env.NO_COLOR !== undefined) { import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { themeManager, DEFAULT_THEME } from './theme-manager.js'; -import { CustomTheme } from './theme.js'; -import { SemanticColors } from './semantic-tokens.js'; +import type { CustomTheme } from './theme.js'; +import type { SemanticColors } from './semantic-tokens.js'; import * as fs from 'node:fs'; import * as os from 'node:os'; import type * as osActual from 'node:os'; @@ -132,7 +132,7 @@ describe('ThemeManager', () => { themeManager.setActiveTheme('Ayu'); const semanticColors = themeManager.getSemanticColors(); - expect(semanticColors).toEqual({ + expect(semanticColors).toStrictEqual({ text: { primary: expect.any(String), secondary: expect.any(String), diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts index 4d3aca2107..b7b56b012f 100644 --- a/packages/cli/src/ui/themes/theme-manager.ts +++ b/packages/cli/src/ui/themes/theme-manager.ts @@ -20,16 +20,16 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { - Theme, - ThemeType, - CustomTheme, + type Theme, + type ThemeType, + type CustomTheme, createCustomTheme, validateCustomTheme, } from './theme.js'; import { ANSI } from './ansi.js'; import { ANSILight } from './ansi-light.js'; import { NoColorTheme } from './no-color.js'; -import { SemanticColors } from './semantic-tokens.js'; +import type { SemanticColors } from './semantic-tokens.js'; import { resolveSemanticColors } from './semantic-resolver.js'; import process from 'node:process'; import { debugLogger } from '@vybestack/llxprt-code-core'; @@ -77,7 +77,7 @@ class ThemeManager { // Invalidate semantic colors cache when custom themes change this.semanticColorsCache = null; - if (!customThemesSettings) { + if (customThemesSettings == null) { return; } @@ -123,7 +123,7 @@ class ThemeManager { */ setActiveTheme(themeName: string | undefined): boolean { const theme = this.findThemeByName(themeName); - if (!theme) { + if (theme == null) { return false; } this.activeTheme = theme; @@ -209,7 +209,7 @@ class ThemeManager { const allThemes = [...builtInThemes, ...customThemes]; - const sortedThemes = allThemes.sort((a, b) => { + return allThemes.sort((a, b) => { const typeOrder = (type: ThemeType): number => { switch (type) { case 'dark': @@ -231,8 +231,6 @@ class ThemeManager { } return a.name.localeCompare(b.name); }); - - return sortedThemes; } /** @@ -331,7 +329,7 @@ class ThemeManager { const builtInTheme = this.availableThemes.find( (theme) => theme.name === themeName, ); - if (builtInTheme) { + if (builtInTheme != null) { return builtInTheme; } diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts index 893a5620a5..7eae0be0b6 100644 --- a/packages/cli/src/ui/themes/theme.ts +++ b/packages/cli/src/ui/themes/theme.ts @@ -679,7 +679,7 @@ export function pickDefaultThemeName( const preferredFallback = availableThemes.find( (t) => t.name === fallbackForType && t.type === terminalThemeType, ); - if (preferredFallback) { + if (preferredFallback != null) { return preferredFallback.name; } @@ -687,7 +687,7 @@ export function pickDefaultThemeName( const matchingTheme = availableThemes.find( (t) => t.type === terminalThemeType, ); - if (matchingTheme) { + if (matchingTheme != null) { return matchingTheme.name; } diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index ad3e532f27..27000ba863 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -4,14 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { +import type { CompressionStatus, GeminiCLIExtension, ToolCallConfirmationDetails, ToolResultDisplay, - type ThinkingBlock, - type SkillDefinition, - type SkillSource, + ThinkingBlock, + SkillDefinition, + SkillSource, } from '@vybestack/llxprt-code-core'; export type { SkillDefinition, SkillSource }; diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx index 124c7a0d2d..f7287e2d65 100644 --- a/packages/cli/src/ui/utils/CodeColorizer.tsx +++ b/packages/cli/src/ui/utils/CodeColorizer.tsx @@ -15,12 +15,12 @@ import type { RootContent, } from 'hast'; import { themeManager } from '../themes/theme-manager.js'; -import { Theme } from '../themes/theme.js'; +import type { Theme } from '../themes/theme.js'; import { MaxSizedBox, MINIMUM_MAX_HEIGHT, } from '../components/shared/MaxSizedBox.js'; -import { LoadedSettings } from '../../config/settings.js'; +import type { LoadedSettings } from '../../config/settings.js'; import { debugLogger } from '@vybestack/llxprt-code-core'; // Configure theming and parsing utilities. diff --git a/packages/cli/src/ui/utils/ConsolePatcher.ts b/packages/cli/src/ui/utils/ConsolePatcher.ts index c3b274238c..9037b4678d 100644 --- a/packages/cli/src/ui/utils/ConsolePatcher.ts +++ b/packages/cli/src/ui/utils/ConsolePatcher.ts @@ -6,7 +6,7 @@ */ import util from 'util'; -import { ConsoleMessageItem } from '../types.js'; +import type { ConsoleMessageItem } from '../types.js'; interface ConsolePatcherParams { onNewMessage?: (message: Omit) => void; @@ -52,14 +52,12 @@ export class ConsolePatcher { if (type !== 'debug' || this.params.debugMode) { this.originalConsoleError(this.formatArgs(args)); } - } else { - if (type !== 'debug' || this.params.debugMode) { - this.params.onNewMessage?.({ - type, - content: this.formatArgs(args), - count: 1, - }); - } + } else if (type !== 'debug' || this.params.debugMode) { + this.params.onNewMessage?.({ + type, + content: this.formatArgs(args), + count: 1, + }); } }; } diff --git a/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx b/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx index 39a81705c9..56e70548cc 100644 --- a/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx +++ b/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx @@ -99,8 +99,8 @@ const RenderInlineInternal: React.FC = ({ fullMatch.endsWith('`') && fullMatch.length > INLINE_CODE_MARKER_LENGTH ) { - const codeMatch = fullMatch.match(/^(`+)(.+?)\1$/s); - if (codeMatch && codeMatch[2]) { + const codeMatch = RegExp(/^(`+)(.+?)\1$/s).exec(fullMatch); + if (codeMatch?.[2]) { renderedNode = ( {codeMatch[2]} @@ -112,8 +112,8 @@ const RenderInlineInternal: React.FC = ({ fullMatch.includes('](') && fullMatch.endsWith(')') ) { - const linkMatch = fullMatch.match(/\[(.*?)\]\((.*?)\)/); - if (linkMatch) { + const linkMatch = RegExp(/\[(.*?)\]\((.*?)\)/).exec(fullMatch); + if (linkMatch != null) { const linkText = linkMatch[1]; const url = linkMatch[2]; renderedNode = ( @@ -137,7 +137,7 @@ const RenderInlineInternal: React.FC = ({ )} ); - } else if (fullMatch.match(/^https?:\/\//)) { + } else if (RegExp(/^https?:\/\//).exec(fullMatch) != null) { renderedNode = ( {fullMatch} diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx index 2a5645b956..2e0e0ce35e 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx @@ -88,9 +88,9 @@ const MarkdownDisplayInternal: React.FC = ({ const key = `line-${index}`; if (inCodeBlock) { - const fenceMatch = line.match(codeFenceRegex); + const fenceMatch = RegExp(codeFenceRegex).exec(line); if ( - fenceMatch && + fenceMatch != null && fenceMatch[1].startsWith(codeBlockFence[0]) && fenceMatch[1].length >= codeBlockFence.length ) { @@ -114,23 +114,23 @@ const MarkdownDisplayInternal: React.FC = ({ return; } - const codeFenceMatch = line.match(codeFenceRegex); - const headerMatch = line.match(headerRegex); - const ulMatch = line.match(ulItemRegex); - const olMatch = line.match(olItemRegex); - const hrMatch = line.match(hrRegex); - const tableRowMatch = line.match(tableRowRegex); - const tableSeparatorMatch = line.match(tableSeparatorRegex); + const codeFenceMatch = RegExp(codeFenceRegex).exec(line); + const headerMatch = RegExp(headerRegex).exec(line); + const ulMatch = RegExp(ulItemRegex).exec(line); + const olMatch = RegExp(olItemRegex).exec(line); + const hrMatch = RegExp(hrRegex).exec(line); + const tableRowMatch = RegExp(tableRowRegex).exec(line); + const tableSeparatorMatch = RegExp(tableSeparatorRegex).exec(line); - if (codeFenceMatch) { + if (codeFenceMatch != null) { inCodeBlock = true; codeBlockFence = codeFenceMatch[1]; codeBlockLang = codeFenceMatch[2] || null; - } else if (tableRowMatch && !inTable) { + } else if (tableRowMatch != null && !inTable) { // Potential table start - check if next line is separator if ( index + 1 < lines.length && - lines[index + 1].match(tableSeparatorRegex) + RegExp(tableSeparatorRegex).exec(lines[index + 1]) != null ) { inTable = true; tableHeaders = tableRowMatch[1].split('|').map((cell) => cell.trim()); @@ -145,9 +145,9 @@ const MarkdownDisplayInternal: React.FC = ({ , ); } - } else if (inTable && tableSeparatorMatch) { + } else if (inTable && tableSeparatorMatch != null) { // Skip separator line - already handled - } else if (inTable && tableRowMatch) { + } else if (inTable && tableRowMatch != null) { // Add table row const cells = tableRowMatch[1].split('|').map((cell) => cell.trim()); // Ensure row has same column count as headers @@ -158,7 +158,7 @@ const MarkdownDisplayInternal: React.FC = ({ cells.length = tableHeaders.length; } tableRows.push(cells); - } else if (inTable && !tableRowMatch) { + } else if (inTable && tableRowMatch == null) { // End of table if (tableHeaders.length > 0 && tableRows.length > 0) { addContentBlock( @@ -184,13 +184,13 @@ const MarkdownDisplayInternal: React.FC = ({ , ); } - } else if (hrMatch) { + } else if (hrMatch != null) { addContentBlock( --- , ); - } else if (headerMatch) { + } else if (headerMatch != null) { const level = headerMatch[1].length; const headerText = headerMatch[2]; let headerNode: React.ReactNode = null; @@ -235,7 +235,7 @@ const MarkdownDisplayInternal: React.FC = ({ break; } if (headerNode) addContentBlock({headerNode}); - } else if (ulMatch) { + } else if (ulMatch != null) { const leadingWhitespace = ulMatch[1]; const marker = ulMatch[2]; const itemText = ulMatch[3]; @@ -248,7 +248,7 @@ const MarkdownDisplayInternal: React.FC = ({ leadingWhitespace={leadingWhitespace} />, ); - } else if (olMatch) { + } else if (olMatch != null) { const leadingWhitespace = olMatch[1]; const marker = olMatch[2]; const itemText = olMatch[3]; @@ -261,23 +261,21 @@ const MarkdownDisplayInternal: React.FC = ({ leadingWhitespace={leadingWhitespace} />, ); - } else { - if (line.trim().length === 0 && !inCodeBlock) { - if (!lastLineEmpty) { - contentBlocks.push( - , - ); - lastLineEmpty = true; - } - } else { - addContentBlock( - - - - - , + } else if (line.trim().length === 0 && !inCodeBlock) { + if (!lastLineEmpty) { + contentBlocks.push( + , ); + lastLineEmpty = true; } + } else { + addContentBlock( + + + + + , + ); } }); diff --git a/packages/cli/src/ui/utils/TableRenderer.tsx b/packages/cli/src/ui/utils/TableRenderer.tsx index 7ac592178f..4c9c0bbab4 100644 --- a/packages/cli/src/ui/utils/TableRenderer.tsx +++ b/packages/cli/src/ui/utils/TableRenderer.tsx @@ -165,7 +165,7 @@ const adjustBreakIndex = (content: string, index: number): number => { const wrapCellContent = (content: string, maxWidth: number): string[] => { const cacheKey = `${maxWidth}::${content}`; const cached = wrapCache.get(cacheKey); - if (cached) { + if (cached != null) { return cached; } diff --git a/packages/cli/src/ui/utils/autoPromptGenerator.ts b/packages/cli/src/ui/utils/autoPromptGenerator.ts index a6412478f4..c813725e98 100644 --- a/packages/cli/src/ui/utils/autoPromptGenerator.ts +++ b/packages/cli/src/ui/utils/autoPromptGenerator.ts @@ -5,14 +5,14 @@ */ import { - Config, + type Config, GeminiClient, createRuntimeStateFromConfig, DebugLogger, } from '@vybestack/llxprt-code-core'; import { FunctionCallingConfigMode, - SendMessageParameters, + type SendMessageParameters, } from '@google/genai'; import { getRuntimeBridge } from '../contexts/RuntimeContext.js'; diff --git a/packages/cli/src/ui/utils/clipboard.ts b/packages/cli/src/ui/utils/clipboard.ts index dd9ca1ec12..74f23092a0 100644 --- a/packages/cli/src/ui/utils/clipboard.ts +++ b/packages/cli/src/ui/utils/clipboard.ts @@ -67,6 +67,6 @@ export async function copyTextToClipboard( // System clipboard failed, but OSC52 was still attempted const errorMessage = err instanceof Error ? err.message : 'Unknown clipboard error'; - return { success: false, text, error: errorMessage }; + return { success: false, error: errorMessage, text }; } } diff --git a/packages/cli/src/ui/utils/clipboardUtils.test.ts b/packages/cli/src/ui/utils/clipboardUtils.test.ts index dd9ff6a89b..88956a39a7 100644 --- a/packages/cli/src/ui/utils/clipboardUtils.test.ts +++ b/packages/cli/src/ui/utils/clipboardUtils.test.ts @@ -78,20 +78,20 @@ describe('clipboardUtils', () => { describe('splitEscapedPaths', () => { it('should return single path when no spaces', () => { - expect(splitEscapedPaths('/path/to/image.png')).toEqual([ + expect(splitEscapedPaths('/path/to/image.png')).toStrictEqual([ '/path/to/image.png', ]); }); it('should split simple space-separated paths', () => { - expect(splitEscapedPaths('/img1.png /img2.png')).toEqual([ + expect(splitEscapedPaths('/img1.png /img2.png')).toStrictEqual([ '/img1.png', '/img2.png', ]); }); it('should split three paths', () => { - expect(splitEscapedPaths('/a.png /b.jpg /c.heic')).toEqual([ + expect(splitEscapedPaths('/a.png /b.jpg /c.heic')).toStrictEqual([ '/a.png', '/b.jpg', '/c.heic', @@ -99,44 +99,43 @@ describe('clipboardUtils', () => { }); it('should preserve escaped spaces within filenames', () => { - expect(splitEscapedPaths('/my\\\\ image.png')).toEqual([ + expect(splitEscapedPaths('/my\\\\ image.png')).toStrictEqual([ '/my\\\\ image.png', ]); }); it('should handle multiple paths with escaped spaces', () => { - expect(splitEscapedPaths('/my\\\\ img1.png /my\\\\ img2.png')).toEqual([ - '/my\\\\ img1.png', - '/my\\\\ img2.png', - ]); + expect( + splitEscapedPaths('/my\\\\ img1.png /my\\\\ img2.png'), + ).toStrictEqual(['/my\\\\ img1.png', '/my\\\\ img2.png']); }); it('should handle path with multiple escaped spaces', () => { - expect(splitEscapedPaths('/path/to/my\\\\ cool\\\\ image.png')).toEqual([ - '/path/to/my\\\\ cool\\\\ image.png', - ]); + expect( + splitEscapedPaths('/path/to/my\\\\ cool\\\\ image.png'), + ).toStrictEqual(['/path/to/my\\\\ cool\\\\ image.png']); }); it('should handle multiple consecutive spaces between paths', () => { - expect(splitEscapedPaths('/img1.png /img2.png')).toEqual([ + expect(splitEscapedPaths('/img1.png /img2.png')).toStrictEqual([ '/img1.png', '/img2.png', ]); }); it('should handle trailing and leading whitespace', () => { - expect(splitEscapedPaths(' /img1.png /img2.png ')).toEqual([ + expect(splitEscapedPaths(' /img1.png /img2.png ')).toStrictEqual([ '/img1.png', '/img2.png', ]); }); it('should return empty array for empty string', () => { - expect(splitEscapedPaths('')).toEqual([]); + expect(splitEscapedPaths('')).toStrictEqual([]); }); it('should return empty array for whitespace only', () => { - expect(splitEscapedPaths(' ')).toEqual([]); + expect(splitEscapedPaths(' ')).toStrictEqual([]); }); }); @@ -197,7 +196,7 @@ describe('clipboardUtils', () => { validatedPaths.push(p); return validPaths.has(p); }); - expect(validatedPaths).toEqual([ + expect(validatedPaths).toStrictEqual([ '/my\\ file.txt /other.txt', '/my file.txt', '/other.txt', diff --git a/packages/cli/src/ui/utils/commandUtils.test.ts b/packages/cli/src/ui/utils/commandUtils.test.ts index a65a467981..c66abd9a21 100644 --- a/packages/cli/src/ui/utils/commandUtils.test.ts +++ b/packages/cli/src/ui/utils/commandUtils.test.ts @@ -57,22 +57,21 @@ vi.stubGlobal( const makeWritable = (opts?: { isTTY?: boolean; writeReturn?: boolean }) => { const { isTTY = false, writeReturn = true } = opts ?? {}; - const stream = Object.assign(new EventEmitter(), { + return Object.assign(new EventEmitter(), { write: vi.fn().mockReturnValue(writeReturn), end: vi.fn(), destroy: vi.fn(), - isTTY, once: EventEmitter.prototype.once, on: EventEmitter.prototype.on, off: EventEmitter.prototype.off, removeAllListeners: EventEmitter.prototype.removeAllListeners, + isTTY, }) as unknown as EventEmitter & { write: Mock; end: Mock; isTTY?: boolean; removeAllListeners: Mock; }; - return stream; }; const resetEnv = () => { diff --git a/packages/cli/src/ui/utils/commandUtils.ts b/packages/cli/src/ui/utils/commandUtils.ts index 46c3b7eebe..351edc58fe 100644 --- a/packages/cli/src/ui/utils/commandUtils.ts +++ b/packages/cli/src/ui/utils/commandUtils.ts @@ -297,7 +297,7 @@ export const getUrlOpenCommand = (): string => { export function isAutoExecutableCommand( command: SlashCommand | undefined | null, ): boolean { - if (!command) { + if (command == null) { return false; } diff --git a/packages/cli/src/ui/utils/computeStats.test.ts b/packages/cli/src/ui/utils/computeStats.test.ts index 9e0ebf70a8..3d3b8f9419 100644 --- a/packages/cli/src/ui/utils/computeStats.test.ts +++ b/packages/cli/src/ui/utils/computeStats.test.ts @@ -11,7 +11,10 @@ import { calculateErrorRate, computeSessionStats, } from './computeStats'; -import { ModelMetrics, SessionMetrics } from '../contexts/SessionContext.js'; +import type { + ModelMetrics, + SessionMetrics, +} from '../contexts/SessionContext.js'; describe('calculateErrorRate', () => { it('should return 0 if totalRequests is 0', () => { @@ -135,7 +138,7 @@ describe('computeSessionStats', () => { const result = computeSessionStats(metrics); - expect(result).toEqual({ + expect(result).toStrictEqual({ totalApiTime: 0, totalToolTime: 0, agentActiveTime: 0, diff --git a/packages/cli/src/ui/utils/computeStats.ts b/packages/cli/src/ui/utils/computeStats.ts index 95638b8501..32a32a44b1 100644 --- a/packages/cli/src/ui/utils/computeStats.ts +++ b/packages/cli/src/ui/utils/computeStats.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { +import type { SessionMetrics, ComputedSessionStats, ModelMetrics, diff --git a/packages/cli/src/ui/utils/fuzzyFilter.test.ts b/packages/cli/src/ui/utils/fuzzyFilter.test.ts index 189e05334d..718c729bdf 100644 --- a/packages/cli/src/ui/utils/fuzzyFilter.test.ts +++ b/packages/cli/src/ui/utils/fuzzyFilter.test.ts @@ -31,7 +31,7 @@ describe('fuzzyFilter', () => { it('returns all items when query is empty', () => { const result = filterCompletions(mockOptions, '', { enableFuzzy: true }); expect(result).toHaveLength(5); - expect(result).toEqual(mockOptions); + expect(result).toStrictEqual(mockOptions); }); it('performs fuzzy matching when enabled', () => { @@ -61,7 +61,7 @@ describe('fuzzyFilter', () => { const result = filterCompletions(mockOptions, 'xyz', { enableFuzzy: true, }); - expect(result).toEqual([]); + expect(result).toStrictEqual([]); }); it('is case insensitive', () => { @@ -104,7 +104,7 @@ describe('fuzzyFilter', () => { it('returns all items when query is empty', () => { const result = filterStrings(testStrings, '', { enableFuzzy: true }); expect(result).toHaveLength(5); - expect(result).toEqual(testStrings); + expect(result).toStrictEqual(testStrings); }); it('performs fuzzy matching when enabled', () => { @@ -116,12 +116,12 @@ describe('fuzzyFilter', () => { it('performs exact prefix matching when fuzzy disabled', () => { const result = filterStrings(testStrings, 'top', { enableFuzzy: false }); - expect(result).toEqual(['top_p']); + expect(result).toStrictEqual(['top_p']); }); it('returns empty array when no matches found', () => { const result = filterStrings(testStrings, 'xyz', { enableFuzzy: true }); - expect(result).toEqual([]); + expect(result).toStrictEqual([]); }); it('is case insensitive', () => { @@ -288,7 +288,7 @@ describe('fuzzyFilter', () => { const wrappedCompleter = withFuzzyFilter(baseCompleter); const result = await wrappedCompleter(mockContext, 'test', mockTokens); - expect(result).toEqual([]); + expect(result).toStrictEqual([]); }); it('preserves description fields from original options', async () => { diff --git a/packages/cli/src/ui/utils/fuzzyFilter.ts b/packages/cli/src/ui/utils/fuzzyFilter.ts index 818c50fdaa..e2dd7cf150 100644 --- a/packages/cli/src/ui/utils/fuzzyFilter.ts +++ b/packages/cli/src/ui/utils/fuzzyFilter.ts @@ -45,13 +45,12 @@ export function filterCompletions( const results = fzf.find(query); return results.map((result: FzfResultItem