UNPKG

git-contextor

Version:

A code context tool with vector search and real-time monitoring, with optional Git integration.

228 lines (207 loc) 6.74 kB
const fs = require('fs').promises; const path = require('path'); const crypto = require('crypto'); const { merge } = require('lodash'); require('dotenv').config(); class ConfigManager { constructor(repoPath) { this.repoPath = repoPath; this.configDir = path.join(repoPath, '.gitcontextor'); this.configFile = path.join(this.configDir, 'config.json'); this.defaultConfig = { version: '1.0.0', repository: { path: repoPath, name: path.basename(repoPath), branch: 'main' }, features: { fileBrowser: { enabled: true } }, embedding: { provider: 'local', // Safe default - no API key needed model: 'Xenova/all-MiniLM-L6-v2', apiKey: process.env.OPENAI_API_KEY || process.env.GOOGLE_API_KEY || null, dimensions: 384 }, chat: { provider: 'openai', model: 'gpt-4o-mini', apiKey: null }, vision: { enabled: false, provider: null, model: null, apiKey: null, prompt: 'Describe this image for a software developer. Focus on text, code, diagrams, UI elements, and technical content. Be concise but comprehensive.' }, chunking: { strategy: 'function', maxChunkSize: 1024, overlap: 0.25, includeComments: true }, indexing: { excludePatterns: [ 'node_modules/**', '.git/**', '.gitcontextor/**', '*.test.js', '*.spec.js', 'dist/**', 'build/**', 'coverage/**' ], includeExtensions: [ '.js', '.jsx', '.ts', '.tsx', '.py', '.java', '.c', '.cpp', '.cs', '.php', '.rb', '.go', '.rs', '.kt', '.scala', '.swift', '.dart', '.r', '.html', '.css', '.scss', '.sass', '.json', '.yaml', '.yml', '.toml', '.xml', '.md', '.txt', '.sql', '.pdf' ], followSymlinks: false }, vectorStore: { provider: 'auto', // 'auto', 'memory', 'qdrant' memory: { persistence: true }, qdrant: { host: null, port: 6333 } }, services: { port: 3333, apiKey: this.generateApiKey(), keepCollectionOnExit: true }, monitoring: { watchEnabled: true, debounceMs: 1000, maxQueueSize: 100 }, performance: { batchSize: 10, concurrency: 3, cacheEnabled: true, cacheTtl: 300000 }, tunneling: { provider: 'corrently', // 'corrently', 'managed', 'localtunnel', 'ngrok' corrently: { serverUrl: 'https://tunnel.corrently.cloud', apiKey: process.env.CORRENTLY_TUNNEL_API_KEY || null, description: 'Git Contextor Share' }, managed: { apiUrl: 'https://tunnel.corrently.cloud', apiKey: null, // User-provided API key subdomain: null, // Optional custom subdomain gitContextorShare: true }, localtunnel: { subdomain: null } } }; this.config = { ...this.defaultConfig }; } async init(initialOverrides = {}, force = false) { try { await fs.access(this.configDir); if (!force) { // This case is handled in the init command now, but we keep it for safety. throw new Error('Git Contextor already initialized. Use --force to reinitialize.'); } } catch (error) { if (error.code !== 'ENOENT') throw error; } // Create .gitcontextor directory await fs.mkdir(this.configDir, { recursive: true }); await fs.mkdir(path.join(this.configDir, 'qdrant'), { recursive: true }); await fs.mkdir(path.join(this.configDir, 'logs'), { recursive: true }); // Apply overrides to default config before saving this.config = merge(this.config, initialOverrides); // Write the merged config await this.save(); // Create .gitignore entry await this.updateGitignore(); } async load() { try { const configData = await fs.readFile(this.configFile, 'utf8'); const loadedConfig = JSON.parse(configData); this._migrateConfig(loadedConfig); // Ensure old configs are compatible this.config = merge({}, this.defaultConfig, loadedConfig); } catch (error) { if (error.code === 'ENOENT') { throw new Error('Git Contextor not initialized. Run "git-contextor init" first.'); } throw error; } } async save() { const formattedConfig = JSON.stringify(this.config, null, 2); await fs.writeFile(this.configFile, formattedConfig); } _migrateConfig(config) { if (config.services?.qdrantHost || config.services?.qdrantPort) { config.vectorStore = config.vectorStore || {}; config.vectorStore.provider = 'qdrant'; config.vectorStore.qdrant = { host: config.services.qdrantHost || 'localhost', port: config.services.qdrantPort || 6333 }; delete config.services.qdrantHost; delete config.services.qdrantPort; } return config; } async updateConfig(updates) { // Validate the updates before applying this._validateConfig(updates); this.config = merge(this.config, updates); await this.save(); } getConfig() { return this.config; } _validateConfig(updates) { // Validate port numbers if (updates.services?.port && (typeof updates.services.port !== 'number' || updates.services.port < 1 || updates.services.port > 65535)) { throw new Error('Port must be a number between 1 and 65535'); } // Validate required fields only if they are being updated if (updates.repository && (updates.repository.name === null || updates.repository.name === undefined)) { throw new Error('Repository name is required'); } // Validate array fields if (updates.indexing?.includeExtensions && !Array.isArray(updates.indexing.includeExtensions)) { throw new Error('includeExtensions must be an array'); } } generateApiKey() { return 'gctx_' + crypto.randomBytes(32).toString('hex'); } async updateGitignore() { const gitignorePath = path.join(this.repoPath, '.gitignore'); const entry = '\n# Git Contextor\n.gitcontextor/\n'; try { const content = await fs.readFile(gitignorePath, 'utf8'); if (!content.includes('.gitcontextor/')) { await fs.writeFile(gitignorePath, content + entry); } } catch (error) { if (error.code === 'ENOENT') { await fs.writeFile(gitignorePath, entry + '\n'); } } } } module.exports = ConfigManager;