From 9c34738803429db679c09d14540d215611593103 Mon Sep 17 00:00:00 2001 From: lior Date: Mon, 24 Aug 2026 12:49:06 +0300 Subject: [PATCH] feat: add `codegraph clone` command for copying indexes to worktrees Git worktrees don't inherit the parent repo's .codegraph/ index, leaving dispatched agents without code intelligence. `codegraph clone` copies just the codegraph.db (no WAL/SHM) from a source project to a target, with --auto to detect the parent worktree automatically. Co-Authored-By: Claude Opus 4.6 --- src/bin/codegraph.ts | 95 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index e4038200d..a30995699 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -38,6 +38,7 @@ try { } catch { /* cache is best-effort */ } import { Command } from 'commander'; +import { execFileSync } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; import { getCodeGraphDir, isInitialized, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, hasStructuralKeyword, extractCodeTokens } from '../directory'; @@ -291,6 +292,15 @@ function formatDuration(ms: number): string { return `${minutes}m ${remainingSeconds.toFixed(0)}s`; } +/** + * Format a byte count for CLI output. + */ +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes}B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; +} + // Shimmer progress renderer (runs in a worker thread for smooth animation) // Imported at top of file from '../ui/shimmer-progress' @@ -758,6 +768,91 @@ program } }); +/** + * codegraph clone [target] + */ +program + .command('clone [target]') + .description('Copy a CodeGraph index from another project or worktree') + .option('-s, --source ', 'Source project with an existing .codegraph index') + .option('-f, --force', 'Overwrite if target already has an index') + .option('--auto', 'Auto-detect source from git worktree parent') + .action(async (targetArg: string | undefined, options: { source?: string; force?: boolean; auto?: boolean }) => { + const targetPath = path.resolve(targetArg || process.cwd()); + let sourcePath: string | undefined; + + if (options.source) { + sourcePath = path.resolve(options.source); + } else if (options.auto) { + try { + const output = execFileSync('git', ['worktree', 'list', '--porcelain'], { + cwd: targetPath, + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + timeout: 5000, + }).trim(); + const mainLine = output.split('\n').find((line) => line.startsWith('worktree ')); + if (mainLine) { + const mainWorktree = mainLine.slice('worktree '.length).trim(); + const resolvedMainWorktree = path.isAbsolute(mainWorktree) + ? mainWorktree + : path.resolve(targetPath, mainWorktree); + if (isInitialized(resolvedMainWorktree)) { + sourcePath = resolvedMainWorktree; + } + } + } catch { + // Report the same actionable error below when git or the worktree is unavailable. + } + } else { + error('A source is required. Pass --source or use --auto.'); + info('Usage: codegraph clone --source [target]'); + process.exit(1); + } + + if (!sourcePath || !isInitialized(sourcePath)) { + error(`No CodeGraph index found in the source${sourcePath ? ` at ${sourcePath}` : ''}.`); + if (options.auto) { + info('Auto-detection looks for an initialized index in the main git worktree.'); + } + process.exit(1); + } + + if (isInitialized(targetPath) && !options.force) { + warn(`Target already has a CodeGraph index at ${getCodeGraphDir(targetPath)}; skipping.`); + info('Pass --force to overwrite it.'); + return; + } + + try { + const targetCodeGraphDir = getCodeGraphDir(targetPath); + fs.mkdirSync(targetCodeGraphDir, { recursive: true }); + + const gitignorePath = path.join(targetCodeGraphDir, '.gitignore'); + if (!fs.existsSync(gitignorePath)) { + fs.writeFileSync( + gitignorePath, + '# CodeGraph data files — local to each machine, not for committing.\n' + + '# Ignore everything in .codegraph/ except this file itself, so transient\n' + + '# files (the database, daemon.pid, sockets, logs) never show up in git.\n' + + '*\n' + + '!.gitignore\n', + 'utf8' + ); + } + + const sourceDbPath = path.join(getCodeGraphDir(sourcePath), 'codegraph.db'); + const targetDbPath = path.join(targetCodeGraphDir, 'codegraph.db'); + fs.copyFileSync(sourceDbPath, targetDbPath); + const size = fs.statSync(targetDbPath).size; + + success(`Copied CodeGraph index to ${targetPath} (${formatBytes(size)})`); + } catch (err) { + error(`Failed to clone CodeGraph index: ${err instanceof Error ? err.message : String(err)}`); + process.exit(1); + } + }); + /** * codegraph index [path] */