diff options
Diffstat (limited to 'packages/cli/src/config/config.ts')
| -rw-r--r-- | packages/cli/src/config/config.ts | 319 |
1 files changed, 310 insertions, 9 deletions
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 2f605ec3..7e564ee2 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -4,6 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as fs from 'fs/promises'; +import * as fsSync from 'fs'; // For synchronous checks like existsSync +import * as path from 'path'; +import { homedir } from 'os'; import yargs from 'yargs/yargs'; import { hideBin } from 'yargs/helpers'; import process from 'node:process'; @@ -15,9 +19,32 @@ import { import { Settings } from './settings.js'; import { readPackageUp } from 'read-package-up'; +// Simple console logger for now - replace with actual logger if available +const logger = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + debug: (...args: any[]) => console.debug('[DEBUG]', ...args), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + warn: (...args: any[]) => console.warn('[WARN]', ...args), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error: (...args: any[]) => console.error('[ERROR]', ...args), +}; + const DEFAULT_GEMINI_MODEL = 'gemini-2.5-pro-preview-05-06'; +const GEMINI_MD_FILENAME = 'GEMINI.md'; +const GEMINI_CONFIG_DIR = '.gemini'; +// TODO(adh): Refactor to use a shared ignore list with other tools like glob and read-many-files. +const DEFAULT_IGNORE_DIRECTORIES = [ + 'node_modules', + '.git', + 'dist', + 'build', + 'out', + 'coverage', + '.vscode', + '.idea', + '.DS_Store', +]; -// Keep CLI-specific argument parsing interface CliArgs { model: string | undefined; sandbox: boolean | string | undefined; @@ -61,25 +88,290 @@ async function parseArguments(): Promise<CliArgs> { .help() .alias('h', 'help') .strict().argv; - return argv; + + const finalArgv: CliArgs = { + ...argv, + sandbox: argv.sandbox, + }; + + return finalArgv; +} + +async function findProjectRoot(startDir: string): Promise<string | null> { + let currentDir = path.resolve(startDir); + while (true) { + const gitPath = path.join(currentDir, '.git'); + try { + const stats = await fs.stat(gitPath); + if (stats.isDirectory()) { + return currentDir; + } + } catch (error: unknown) { + if (typeof error === 'object' && error !== null && 'code' in error) { + const fsError = error as { code: string; message: string }; + if (fsError.code !== 'ENOENT') { + logger.warn( + `Error checking for .git directory at ${gitPath}: ${fsError.message}`, + ); + } + } else { + logger.warn( + `Non-standard error checking for .git directory at ${gitPath}: ${String(error)}`, + ); + } + } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + return null; + } + currentDir = parentDir; + } +} + +async function collectDownwardGeminiFiles( + directory: string, + debugMode: boolean, + ignoreDirs: string[], +): Promise<string[]> { + if (debugMode) logger.debug(`Recursively scanning downward in: ${directory}`); + const collectedPaths: string[] = []; + try { + const entries = await fs.readdir(directory, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(directory, entry.name); + if (entry.isDirectory()) { + if (ignoreDirs.includes(entry.name)) { + if (debugMode) + logger.debug(`Skipping ignored directory: ${fullPath}`); + continue; + } + const subDirPaths = await collectDownwardGeminiFiles( + fullPath, + debugMode, + ignoreDirs, + ); + collectedPaths.push(...subDirPaths); + } else if (entry.isFile() && entry.name === GEMINI_MD_FILENAME) { + try { + await fs.access(fullPath, fsSync.constants.R_OK); + collectedPaths.push(fullPath); + if (debugMode) + logger.debug(`Found readable downward GEMINI.md: ${fullPath}`); + } catch { + if (debugMode) + logger.debug( + `Downward GEMINI.md not readable, skipping: ${fullPath}`, + ); + } + } + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + logger.warn(`Error scanning directory ${directory}: ${message}`); + if (debugMode) logger.debug(`Failed to scan directory: ${directory}`); + } + return collectedPaths; +} + +export async function getGeminiMdFilePaths( + currentWorkingDirectory: string, + userHomePath: string, + debugMode: boolean, +): Promise<string[]> { + const resolvedCwd = path.resolve(currentWorkingDirectory); + const resolvedHome = path.resolve(userHomePath); + const globalMemoryPath = path.join( + resolvedHome, + GEMINI_CONFIG_DIR, + GEMINI_MD_FILENAME, + ); + const paths: string[] = []; + + if (debugMode) + logger.debug(`Searching for GEMINI.md starting from CWD: ${resolvedCwd}`); + if (debugMode) logger.debug(`User home directory: ${resolvedHome}`); + + try { + await fs.access(globalMemoryPath, fsSync.constants.R_OK); + paths.push(globalMemoryPath); + if (debugMode) + logger.debug(`Found readable global GEMINI.md: ${globalMemoryPath}`); + } catch { + if (debugMode) + logger.debug( + `Global GEMINI.md not found or not readable: ${globalMemoryPath}`, + ); + } + + const projectRoot = await findProjectRoot(resolvedCwd); + if (debugMode) + logger.debug(`Determined project root: ${projectRoot ?? 'None'}`); + + const upwardPaths: string[] = []; + let currentDir = resolvedCwd; + const stopDir = projectRoot ? path.dirname(projectRoot) : resolvedHome; + + while ( + currentDir && + currentDir !== stopDir && + currentDir !== path.dirname(currentDir) + ) { + if (debugMode) + logger.debug(`Checking for GEMINI.md in (upward scan): ${currentDir}`); + if (currentDir === path.join(resolvedHome, GEMINI_CONFIG_DIR)) { + if (debugMode) + logger.debug(`Skipping check inside global config dir: ${currentDir}`); + break; + } + const potentialPath = path.join(currentDir, GEMINI_MD_FILENAME); + try { + await fs.access(potentialPath, fsSync.constants.R_OK); + upwardPaths.unshift(potentialPath); + if (debugMode) + logger.debug(`Found readable upward GEMINI.md: ${potentialPath}`); + } catch { + if (debugMode) + logger.debug( + `Upward GEMINI.md not found or not readable in: ${currentDir}`, + ); + } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + if (debugMode) + logger.debug(`Reached filesystem root, stopping upward search.`); + break; + } + currentDir = parentDir; + } + paths.push(...upwardPaths); + + if (debugMode) + logger.debug(`Starting downward scan from CWD: ${resolvedCwd}`); + const downwardPaths = await collectDownwardGeminiFiles( + resolvedCwd, + debugMode, + DEFAULT_IGNORE_DIRECTORIES, + ); + downwardPaths.sort(); + if (debugMode && downwardPaths.length > 0) + logger.debug( + `Found downward GEMINI.md files (sorted): ${JSON.stringify(downwardPaths)}`, + ); + for (const dPath of downwardPaths) { + if (!paths.includes(dPath)) { + paths.push(dPath); + } + } + + if (debugMode) + logger.debug( + `Final ordered GEMINI.md paths to read: ${JSON.stringify(paths)}`, + ); + return paths; +} + +interface GeminiFileContent { + filePath: string; + content: string | null; +} + +async function readGeminiMdFiles( + filePaths: string[], + debugMode: boolean, +): Promise<GeminiFileContent[]> { + const results: GeminiFileContent[] = []; + for (const filePath of filePaths) { + try { + const content = await fs.readFile(filePath, 'utf-8'); + results.push({ filePath, content }); + if (debugMode) + logger.debug( + `Successfully read: ${filePath} (Length: ${content.length})`, + ); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + logger.warn( + `Warning: Could not read GEMINI.md file at ${filePath}. Error: ${message}`, + ); + results.push({ filePath, content: null }); + if (debugMode) logger.debug(`Failed to read: ${filePath}`); + } + } + return results; +} + +function concatenateInstructions( + instructionContents: GeminiFileContent[], +): string { + return instructionContents + .filter((item) => typeof item.content === 'string') + .map((item) => { + const trimmedContent = (item.content as string).trim(); + if (trimmedContent.length === 0) { + return null; // Filter out empty content after trimming + } + // Use a relative path for the marker if possible, otherwise full path. + // This assumes process.cwd() is the project root or a relevant base. + const displayPath = path.isAbsolute(item.filePath) + ? path.relative(process.cwd(), item.filePath) + : item.filePath; + return `--- Context from: ${displayPath} ---\n${trimmedContent}\n--- End of Context from: ${displayPath} ---`; + }) + .filter((block): block is string => block !== null) + .join('\n\n'); +} + +export async function loadHierarchicalGeminiMemory( + currentWorkingDirectory: string, + debugMode: boolean, +): Promise<string> { + if (debugMode) + logger.debug( + `Loading hierarchical memory for CWD: ${currentWorkingDirectory}`, + ); + const userHomePath = homedir(); + const filePaths = await getGeminiMdFilePaths( + currentWorkingDirectory, + userHomePath, + debugMode, + ); + if (filePaths.length === 0) { + if (debugMode) logger.debug('No GEMINI.md files found in hierarchy.'); + return ''; + } + const contentsWithPaths = await readGeminiMdFiles(filePaths, debugMode); + const combinedInstructions = concatenateInstructions(contentsWithPaths); + if (debugMode) + logger.debug( + `Combined instructions length: ${combinedInstructions.length}`, + ); + if (debugMode && combinedInstructions.length > 0) + logger.debug( + `Combined instructions (snippet): ${combinedInstructions.substring(0, 500)}...`, + ); + return combinedInstructions; } -// Renamed function for clarity export async function loadCliConfig(settings: Settings): Promise<Config> { // Load .env file using logic from server package loadEnvironment(); // Check API key (CLI responsibility) if (!process.env.GEMINI_API_KEY) { - console.log( + logger.error( 'GEMINI_API_KEY is not set. See https://ai.google.dev/gemini-api/docs/api-key to obtain one. ' + 'Please set it in your .env file or as an environment variable.', ); process.exit(1); } - // Parse CLI arguments const argv = await parseArguments(); + const debugMode = argv.debug_mode || false; + + const userMemory = await loadHierarchicalGeminiMemory( + process.cwd(), + debugMode, + ); const userAgent = await createUserAgent(); @@ -89,18 +381,27 @@ export async function loadCliConfig(settings: Settings): Promise<Config> { argv.model || DEFAULT_GEMINI_MODEL, argv.sandbox ?? settings.sandbox ?? false, process.cwd(), - argv.debug_mode || false, + debugMode, argv.question || '', argv.full_context || false, settings.toolDiscoveryCommand, settings.toolCallCommand, settings.mcpServerCommand, userAgent, + userMemory, ); } async function createUserAgent(): Promise<string> { - const packageJsonInfo = await readPackageUp({ cwd: import.meta.url }); - const cliVersion = packageJsonInfo?.packageJson.version || 'unknown'; - return `GeminiCLI/${cliVersion} Node.js/${process.version} (${process.platform}; ${process.arch})`; + try { + const packageJsonInfo = await readPackageUp({ cwd: import.meta.url }); + const cliVersion = packageJsonInfo?.packageJson.version || 'unknown'; + return `GeminiCLI/${cliVersion} Node.js/${process.version} (${process.platform}; ${process.arch})`; + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error); + logger.warn( + `Could not determine package version for User-Agent: ${message}`, + ); + return `GeminiCLI/unknown Node.js/${process.version} (${process.platform}; ${process.arch})`; + } } |
