UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

281 lines (280 loc) 10.7 kB
import { promises as fs } from "node:fs"; import path from "node:path"; import os from "node:os"; // import { buildIndex } from "../retriever/build.js"; import { retrieve } from "../retriever/retrieve.js"; import { log } from "../util/logging.js"; const multiRepoDir = path.join(os.homedir(), ".termcode", "multi-repo"); const registryPath = path.join(multiRepoDir, "registry.json"); // Initialize multi-repo directory async function ensureMultiRepoDir() { await fs.mkdir(multiRepoDir, { recursive: true }); } // Load repository registry export async function loadRepoRegistry() { try { await ensureMultiRepoDir(); const content = await fs.readFile(registryPath, "utf8"); return JSON.parse(content); } catch (error) { // Return default registry if file doesn't exist return { repos: [], workspace: { name: "default", created: new Date().toISOString(), lastUsed: new Date().toISOString() } }; } } // Save repository registry export async function saveRepoRegistry(registry) { try { await ensureMultiRepoDir(); registry.workspace.lastUsed = new Date().toISOString(); await fs.writeFile(registryPath, JSON.stringify(registry, null, 2), "utf8"); } catch (error) { log.error("Failed to save repository registry:", error); throw error; } } // Add repository to registry export async function addRepository(repoPath, options = {}) { const registry = await loadRepoRegistry(); const resolvedPath = path.resolve(repoPath); // Check if repo already exists const existingIndex = registry.repos.findIndex(repo => repo.path === resolvedPath); const repoInfo = { id: existingIndex >= 0 ? registry.repos[existingIndex].id : generateRepoId(resolvedPath), name: options.name || path.basename(resolvedPath), path: resolvedPath, lastIndexed: new Date().toISOString(), tags: options.tags || [], type: options.type || "primary", remote: options.remote, branch: options.branch, active: true }; if (existingIndex >= 0) { registry.repos[existingIndex] = repoInfo; log.success(`Updated repository: ${repoInfo.name}`); } else { registry.repos.push(repoInfo); log.success(`Added repository: ${repoInfo.name} (${repoInfo.type})`); } await saveRepoRegistry(registry); // Index the repository (would need to import buildIndex properly) try { log.step("Indexing repository", "building search index..."); // await buildIndex(resolvedPath); log.success(`Repository indexed: ${repoInfo.name}`); } catch (error) { log.warn(`Failed to index repository ${repoInfo.name}:`, error); } } // Remove repository from registry export async function removeRepository(repoIdOrPath) { const registry = await loadRepoRegistry(); const index = registry.repos.findIndex(repo => repo.id === repoIdOrPath || repo.path === path.resolve(repoIdOrPath) || repo.name === repoIdOrPath); if (index === -1) { return false; } const removed = registry.repos.splice(index, 1)[0]; await saveRepoRegistry(registry); log.success(`Removed repository: ${removed.name}`); return true; } // List all repositories export async function listRepositories(activeOnly = false) { const registry = await loadRepoRegistry(); return activeOnly ? registry.repos.filter(repo => repo.active) : registry.repos; } // Activate/deactivate repository export async function setRepositoryActive(repoIdOrName, active) { const registry = await loadRepoRegistry(); const repo = registry.repos.find(r => r.id === repoIdOrName || r.name === repoIdOrName || r.path === path.resolve(repoIdOrName)); if (!repo) { return false; } repo.active = active; await saveRepoRegistry(registry); log.success(`Repository ${repo.name} ${active ? 'activated' : 'deactivated'}`); return true; } // Search across multiple repositories export async function searchAcrossRepos(query) { const startTime = Date.now(); const registry = await loadRepoRegistry(); // Filter repositories to search let reposToSearch = registry.repos; if (query.repos && query.repos.length > 0) { reposToSearch = registry.repos.filter(repo => query.repos.includes(repo.id) || query.repos.includes(repo.name)); } if (!query.includeInactive) { reposToSearch = reposToSearch.filter(repo => repo.active); } const results = []; const maxResults = query.maxResultsPerRepo || 5; // Search each repository for (const repo of reposToSearch) { try { // Check if index exists const indexPath = path.join(repo.path, ".termcode-index.json"); await fs.access(indexPath); // Generate query embedding (simplified - in real implementation would use embedding model) const chunks = await retrieve(repo.path, [0.1, 0.2, 0.3], maxResults); // Dummy embedding if (chunks.length > 0) { results.push({ repoId: repo.id, repoName: repo.name, chunks: chunks.map(chunk => ({ ...chunk, file: path.relative(repo.path, path.join(repo.path, chunk.file)) // Make paths relative })) }); } } catch (error) { log.warn(`Failed to search repository ${repo.name}:`, error); } } const searchTime = Date.now() - startTime; const totalResults = results.reduce((sum, result) => sum + result.chunks.length, 0); return { results, totalResults, searchTime }; } // Get repository statistics export async function getMultiRepoStats() { const registry = await loadRepoRegistry(); const reposByType = {}; let totalIndexedFiles = 0; for (const repo of registry.repos) { reposByType[repo.type] = (reposByType[repo.type] || 0) + 1; try { // Count indexed files const indexPath = path.join(repo.path, ".termcode-index.json"); const indexData = JSON.parse(await fs.readFile(indexPath, "utf8")); totalIndexedFiles += indexData.length || 0; } catch { // Index doesn't exist or is invalid } } return { totalRepos: registry.repos.length, activeRepos: registry.repos.filter(r => r.active).length, reposByType, totalIndexedFiles, lastActivity: registry.workspace.lastUsed }; } // Sync repository with remote export async function syncRepository(repoIdOrName) { const registry = await loadRepoRegistry(); const repo = registry.repos.find(r => r.id === repoIdOrName || r.name === repoIdOrName); if (!repo) { return false; } try { const { runShell } = await import("../tools/shell.js"); // Git fetch log.step("Syncing repository", `fetching latest changes...`); const fetchResult = await runShell(["git", "fetch", "origin"], repo.path); if (!fetchResult.ok) { const errorMsg = 'error' in fetchResult ? fetchResult.error : "Unknown error"; log.warn(`Failed to fetch from remote: ${errorMsg}`); return false; } // Check for updates const statusResult = await runShell(["git", "status", "--porcelain", "-b"], repo.path); if (statusResult.ok && statusResult.data.stdout.includes("behind")) { log.info(`Repository ${repo.name} has remote updates available`); log.info("Run 'git pull' in the repository to update"); } // Re-index if needed log.step("Re-indexing", "updating search index..."); // await buildIndex(repo.path); // Update last indexed time repo.lastIndexed = new Date().toISOString(); await saveRepoRegistry(registry); log.success(`Repository ${repo.name} synced successfully`); return true; } catch (error) { log.error(`Failed to sync repository ${repo.name}:`, error); return false; } } // Apply changes across multiple repositories export async function applyAcrossRepos(changes, options = {}) { const registry = await loadRepoRegistry(); const appliedRepos = []; const failedRepos = []; for (const change of changes) { const repo = registry.repos.find(r => r.id === change.repoId); if (!repo) { failedRepos.push({ repoId: change.repoId, error: "Repository not found" }); continue; } try { const { applyUnifiedDiff } = await import("../agent/diff.js"); // Create branch if requested if (options.createBranch && options.branchName) { const { runShell } = await import("../tools/shell.js"); await runShell(["git", "checkout", "-b", options.branchName], repo.path); } // Apply diff const filePath = path.join(repo.path, change.filePath); await applyUnifiedDiff(filePath, change.diff); // Commit if message provided if (options.commitMessage) { const { runShell } = await import("../tools/shell.js"); await runShell(["git", "add", change.filePath], repo.path); await runShell(["git", "commit", "-m", options.commitMessage], repo.path); } appliedRepos.push(repo.name); log.success(`Applied changes to ${repo.name}: ${change.filePath}`); } catch (error) { failedRepos.push({ repoId: change.repoId, error: error instanceof Error ? error.message : "Unknown error" }); log.error(`Failed to apply changes to ${repo.name}:`, error); } } return { success: failedRepos.length === 0, appliedRepos, failedRepos }; } // Helper function to generate repository ID function generateRepoId(repoPath) { const basename = path.basename(repoPath); const hash = Math.abs(hashCode(repoPath)).toString(16).substring(0, 8); return `${basename}-${hash}`; } function hashCode(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } return hash; }