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
JavaScript
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;
}