import * as vscode from "vscode"; import * as path from "node:path"; import * as fs from "node:fs"; import { SqliteReader } from "./backend/sqlite"; import type { GraphNode } from "./backend/sqlite"; import { CliWrapper } from "./backend/cli"; import { CodeGraphTreeProvider, BlastRadiusTreeProvider, StatsTreeProvider, } from "./views/treeView"; import { GraphWebviewPanel } from "./views/graphWebview"; import { Installer } from "./onboarding/installer"; import { registerWalkthroughCommands, showWelcomeIfNeeded } from "./onboarding/welcome"; import { StatusBar } from "./views/statusBar"; import { ScmDecorationProvider } from "./features/scmDecorations"; let sqliteReader: SqliteReader | undefined; let autoUpdateTimer: ReturnType | undefined; let scmDecorationProvider: ScmDecorationProvider | undefined; /** * Locate the graph database file in the workspace. * Checks `.code-review-graph/graph.db` first, then falls back to `.code-review-graph.db`. */ function findGraphDb(workspaceRoot: string): string | undefined { const primary = path.join(workspaceRoot, ".code-review-graph", "graph.db"); if (fs.existsSync(primary)) { return primary; } const fallback = path.join(workspaceRoot, ".code-review-graph.db"); if (fs.existsSync(fallback)) { return fallback; } return undefined; } /** * Get the workspace root folder path, or undefined if no workspace is open. * Checks all workspace folders for a graph database (multi-root support). */ function getWorkspaceRoot(): string | undefined { const folders = vscode.workspace.workspaceFolders; if (!folders) { return undefined; } // Prefer the folder that has a graph database for (const folder of folders) { if (findGraphDb(folder.uri.fsPath)) { return folder.uri.fsPath; } } // Fall back to first folder return folders[0]?.uri.fsPath; } /** * Navigate to a node's source file location. */ async function navigateToNode(node: GraphNode): Promise { const workspaceRoot = getWorkspaceRoot(); const filePath = workspaceRoot ? path.join(workspaceRoot, node.filePath) : node.filePath; const doc = await vscode.workspace.openTextDocument(filePath); const line = Math.max(0, (node.lineStart ?? 1) - 1); await vscode.window.showTextDocument(doc, { selection: new vscode.Range(line, 0, line, 0), }); } /** * Register all extension commands. */ function registerCommands( context: vscode.ExtensionContext, cli: CliWrapper ): void { context.subscriptions.push( vscode.commands.registerCommand( "codeReviewGraph.buildGraph", async () => { const workspaceRoot = getWorkspaceRoot(); if (!workspaceRoot) { vscode.window.showErrorMessage("No workspace folder is open."); return; } const result = await cli.buildGraph(workspaceRoot); if (result.success) { await reinitialize(context); vscode.window.showInformationMessage("Code Graph: Build complete."); } else { vscode.window.showErrorMessage( `Code Graph: Build failed. ${result.stderr}` ); } } ) ); context.subscriptions.push( vscode.commands.registerCommand( "codeReviewGraph.updateGraph", async () => { const workspaceRoot = getWorkspaceRoot(); if (!workspaceRoot) { vscode.window.showErrorMessage("No workspace folder is open."); return; } await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: "Code Graph: Updating graph...", cancellable: false, }, async () => { const result = await cli.updateGraph(workspaceRoot); if (!result.success) { vscode.window.showErrorMessage( `Code Graph: Update failed. ${result.stderr}` ); } } ); } ) ); context.subscriptions.push( vscode.commands.registerCommand( "codeReviewGraph.showBlastRadius", async (qualifiedNameOrUri?: string | vscode.Uri) => { if (!sqliteReader) { vscode.window.showWarningMessage( "Code Graph: No graph database loaded." ); return; } let qualifiedName: string | undefined; if (typeof qualifiedNameOrUri === "string") { qualifiedName = qualifiedNameOrUri; } else { qualifiedName = await vscode.window.showInputBox({ prompt: "Enter the qualified name (e.g., my_module.MyClass.my_method)", placeHolder: "my_module.my_function", }); } if (!qualifiedName) { return; } // Find the file for this node and compute impact radius const node = sqliteReader.getNode(qualifiedName); if (!node) { vscode.window.showInformationMessage( `Code Graph: Node "${qualifiedName}" not found.` ); return; } const config = vscode.workspace.getConfiguration("codeReviewGraph"); const depth = config.get("blastRadiusDepth", 2); const impact = sqliteReader.getImpactRadius([node.filePath], depth); if (impact.impactedNodes.length === 0) { vscode.window.showInformationMessage( `Code Graph: No blast radius found for "${qualifiedName}".` ); return; } await vscode.commands.executeCommand( "codeReviewGraph.blastRadius.focus" ); } ) ); context.subscriptions.push( vscode.commands.registerCommand( "codeReviewGraph.findCallers", async (qualifiedName?: string) => { if (!sqliteReader) { vscode.window.showWarningMessage( "Code Graph: No graph database loaded." ); return; } if (!qualifiedName) { qualifiedName = await vscode.window.showInputBox({ prompt: "Enter the qualified name to find callers for", placeHolder: "my_module.my_function", }); } if (!qualifiedName) { return; } const edges = sqliteReader.getEdgesByTarget(qualifiedName); const callerEdges = edges.filter((e) => e.kind === "CALLS"); if (callerEdges.length === 0) { vscode.window.showInformationMessage( `Code Graph: No callers found for "${qualifiedName}".` ); return; } const items = callerEdges.map((e) => { const callerNode = sqliteReader!.getNode(e.sourceQualified); return { label: callerNode?.name ?? e.sourceQualified, description: callerNode?.filePath ?? e.filePath, detail: `Line ${callerNode?.lineStart ?? e.line}`, node: callerNode, edge: e, }; }); const selected = await vscode.window.showQuickPick(items, { placeHolder: `Callers of ${qualifiedName}`, }); if (selected?.node) { await navigateToNode(selected.node); } } ) ); context.subscriptions.push( vscode.commands.registerCommand( "codeReviewGraph.findTests", async (qualifiedName?: string) => { if (!sqliteReader) { vscode.window.showWarningMessage( "Code Graph: No graph database loaded." ); return; } if (!qualifiedName) { qualifiedName = await vscode.window.showInputBox({ prompt: "Enter the qualified name to find tests for", placeHolder: "my_module.my_function", }); } if (!qualifiedName) { return; } // Find tests via TESTED_BY edges const edges = sqliteReader.getEdgesByTarget(qualifiedName); const testEdges = edges.filter((e) => e.kind === "TESTED_BY"); // Also check reverse: source is the node, target is the test const outEdges = sqliteReader.getEdgesBySource(qualifiedName); const outTestEdges = outEdges.filter((e) => e.kind === "TESTED_BY"); const allTestQualifiedNames = new Set([ ...testEdges.map((e) => e.sourceQualified), ...outTestEdges.map((e) => e.targetQualified), ]); if (allTestQualifiedNames.size === 0) { vscode.window.showInformationMessage( `Code Graph: No tests found for "${qualifiedName}".` ); return; } const items: Array<{ label: string; description: string; detail: string; node: GraphNode | undefined; }> = []; for (const tqn of allTestQualifiedNames) { const testNode = sqliteReader.getNode(tqn); items.push({ label: testNode?.name ?? tqn, description: testNode?.filePath ?? "", detail: `Line ${testNode?.lineStart ?? "?"}`, node: testNode, }); } const selected = await vscode.window.showQuickPick(items, { placeHolder: `Tests for ${qualifiedName}`, }); if (selected?.node) { await navigateToNode(selected.node); } } ) ); // ----------------------------------------------------------------- // codeReviewGraph.queryGraph — expose all 8 query patterns // ----------------------------------------------------------------- context.subscriptions.push( vscode.commands.registerCommand("codeReviewGraph.queryGraph", async () => { if (!sqliteReader) { vscode.window.showWarningMessage("Code Graph: No graph database loaded."); return; } const patterns = [ { label: "callers_of", description: "Find functions calling the target" }, { label: "callees_of", description: "Find functions called by the target" }, { label: "imports_of", description: "Find modules imported by a file" }, { label: "importers_of", description: "Find files importing from the target" }, { label: "children_of", description: "Find nodes contained in a file or class" }, { label: "tests_for", description: "Find tests for a function or class" }, { label: "inheritors_of", description: "Find classes inheriting/implementing the target" }, { label: "file_summary", description: "List all nodes in a file" }, ]; const pattern = await vscode.window.showQuickPick(patterns, { placeHolder: "Select a query pattern", }); if (!pattern) { return; } const target = await vscode.window.showInputBox({ prompt: `Enter the target for ${pattern.label}`, placeHolder: "e.g., my_module.py::my_function or path/to/file.py", }); if (!target) { return; } // Map pattern to edge kind + direction type QueryDef = { edgeKind: string; direction: "incoming" | "outgoing"; nodeFilter?: string }; const queryMap: Record = { callers_of: { edgeKind: "CALLS", direction: "incoming" }, callees_of: { edgeKind: "CALLS", direction: "outgoing" }, imports_of: { edgeKind: "IMPORTS_FROM", direction: "outgoing" }, importers_of: { edgeKind: "IMPORTS_FROM", direction: "incoming" }, children_of: { edgeKind: "CONTAINS", direction: "outgoing" }, tests_for: { edgeKind: "TESTED_BY", direction: "incoming" }, inheritors_of: { edgeKind: "INHERITS", direction: "incoming" }, file_summary: { edgeKind: "CONTAINS", direction: "outgoing" }, }; const qdef = queryMap[pattern.label]; if (!qdef) { return; } // Try exact match, then search let node = sqliteReader.getNode(target); if (!node) { const matches = sqliteReader.searchNodes(target, 5); if (matches.length === 1) { node = matches[0]; } else if (matches.length > 1) { const selected = await vscode.window.showQuickPick( matches.map(m => ({ label: m.name, description: `${m.kind} · ${m.filePath}`, node: m, })), { placeHolder: `Multiple matches for "${target}" — select one` }, ); if (!selected) { return; } node = selected.node; } } if (!node) { vscode.window.showInformationMessage(`Code Graph: "${target}" not found.`); return; } const edges = qdef.direction === "incoming" ? sqliteReader.getEdgesByTarget(node.qualifiedName) : sqliteReader.getEdgesBySource(node.qualifiedName); const filtered = edges.filter(e => e.kind === qdef.edgeKind); if (filtered.length === 0) { vscode.window.showInformationMessage( `Code Graph: No ${pattern.label} results for "${node.name}".` ); return; } const items = filtered.map(e => { const relatedQn = qdef.direction === "incoming" ? e.sourceQualified : e.targetQualified; const relatedNode = sqliteReader!.getNode(relatedQn); return { label: relatedNode?.name ?? relatedQn, description: relatedNode ? `${relatedNode.kind} · ${relatedNode.filePath}` : "", detail: `Line ${relatedNode?.lineStart ?? e.line}`, node: relatedNode, }; }); const selected = await vscode.window.showQuickPick(items, { placeHolder: `${pattern.label}: ${node.name} (${filtered.length} results)`, }); if (selected?.node) { await navigateToNode(selected.node); } }) ); // ----------------------------------------------------------------- // codeReviewGraph.findCallees // ----------------------------------------------------------------- context.subscriptions.push( vscode.commands.registerCommand( "codeReviewGraph.findCallees", async (qualifiedName?: string) => { if (!sqliteReader) { vscode.window.showWarningMessage("Code Graph: No graph database loaded."); return; } if (!qualifiedName) { qualifiedName = await vscode.window.showInputBox({ prompt: "Enter the qualified name to find callees for", placeHolder: "my_module.my_function", }); } if (!qualifiedName) { return; } const edges = sqliteReader.getEdgesBySource(qualifiedName); const calleeEdges = edges.filter(e => e.kind === "CALLS"); if (calleeEdges.length === 0) { vscode.window.showInformationMessage( `Code Graph: No callees found for "${qualifiedName}".` ); return; } const items = calleeEdges.map(e => { const calleeNode = sqliteReader!.getNode(e.targetQualified); return { label: calleeNode?.name ?? e.targetQualified, description: calleeNode?.filePath ?? e.filePath, detail: `Line ${calleeNode?.lineStart ?? e.line}`, node: calleeNode, }; }); const selected = await vscode.window.showQuickPick(items, { placeHolder: `Callees of ${qualifiedName}`, }); if (selected?.node) { await navigateToNode(selected.node); } } ) ); // ----------------------------------------------------------------- // codeReviewGraph.findLargeFunctions // ----------------------------------------------------------------- context.subscriptions.push( vscode.commands.registerCommand( "codeReviewGraph.findLargeFunctions", async () => { if (!sqliteReader) { vscode.window.showWarningMessage("Code Graph: No graph database loaded."); return; } const minLinesStr = await vscode.window.showInputBox({ prompt: "Minimum line count threshold", placeHolder: "50", value: "50", }); if (!minLinesStr) { return; } const minLines = parseInt(minLinesStr, 10); if (isNaN(minLines) || minLines < 1) { vscode.window.showWarningMessage("Code Graph: Invalid line count."); return; } const results = sqliteReader.getNodesBySize(minLines, undefined, undefined, 50); if (results.length === 0) { vscode.window.showInformationMessage( `Code Graph: No functions found with ${minLines}+ lines.` ); return; } const items = results.map(r => ({ label: `${r.name} (${r.lineCount} lines)`, description: `${r.kind} · ${r.filePath}`, detail: `Lines ${r.lineStart ?? "?"}–${r.lineEnd ?? "?"}`, node: r as GraphNode, })); const selected = await vscode.window.showQuickPick(items, { placeHolder: `${results.length} nodes with ${minLines}+ lines`, }); if (selected?.node) { await navigateToNode(selected.node); } } ) ); // ----------------------------------------------------------------- // codeReviewGraph.embedGraph // ----------------------------------------------------------------- context.subscriptions.push( vscode.commands.registerCommand("codeReviewGraph.embedGraph", async () => { const workspaceRoot = getWorkspaceRoot(); if (!workspaceRoot) { vscode.window.showErrorMessage("No workspace folder is open."); return; } const result = await cli.embedGraph(workspaceRoot); if (result.success) { vscode.window.showInformationMessage("Code Graph: Embeddings computed."); } else { const msg = result.stderr.includes("not installed") ? "Install embeddings support: pip install code-review-graph[embeddings]" : `Embedding failed: ${result.stderr}`; vscode.window.showErrorMessage(`Code Graph: ${msg}`); } }) ); // ----------------------------------------------------------------- // codeReviewGraph.watchGraph // ----------------------------------------------------------------- context.subscriptions.push( vscode.commands.registerCommand("codeReviewGraph.watchGraph", async () => { const workspaceRoot = getWorkspaceRoot(); if (!workspaceRoot) { vscode.window.showErrorMessage("No workspace folder is open."); return; } vscode.window.showInformationMessage("Code Graph: Watch mode started."); const result = await cli.watchGraph(workspaceRoot); if (!result.success) { vscode.window.showErrorMessage( `Code Graph: Watch failed. ${result.stderr}` ); } }) ); context.subscriptions.push( vscode.commands.registerCommand("codeReviewGraph.showGraph", async () => { if (!sqliteReader) { vscode.window.showWarningMessage( "Code Graph: No graph database loaded. Run 'Code Graph: Build Graph' first." ); return; } GraphWebviewPanel.createOrShow(context.extensionUri, sqliteReader); }) ); context.subscriptions.push( vscode.commands.registerCommand("codeReviewGraph.search", async () => { if (!sqliteReader) { vscode.window.showWarningMessage( "Code Graph: No graph database loaded." ); return; } const query = await vscode.window.showInputBox({ prompt: "Search the code graph", placeHolder: "Enter a function, class, or module name", }); if (!query) { return; } const results = sqliteReader.searchNodes(query); if (results.length === 0) { vscode.window.showInformationMessage( `Code Graph: No results found for "${query}".` ); return; } const items = results.map((r) => ({ label: r.name, description: r.kind, detail: r.filePath ? `${r.filePath}:${r.lineStart ?? ""}` : undefined, result: r, })); const selected = await vscode.window.showQuickPick(items, { placeHolder: `Results for "${query}"`, }); if (selected?.result) { await navigateToNode(selected.result); } }) ); context.subscriptions.push( vscode.commands.registerCommand( "codeReviewGraph.reviewChanges", async () => { if (!sqliteReader) { vscode.window.showWarningMessage( "Code Graph: No graph database loaded." ); return; } const workspaceRoot = getWorkspaceRoot(); if (!workspaceRoot) { vscode.window.showErrorMessage("No workspace folder is open."); return; } await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: "Code Graph: Analyzing changes...", cancellable: false, }, async () => { const { execFile } = await import("node:child_process"); const { promisify } = await import("node:util"); const execFileAsync = promisify(execFile); let changedFiles: string[] = []; try { const r1 = await execFileAsync( "git", ["diff", "--name-only", "HEAD"], { cwd: workspaceRoot } ); const r2 = await execFileAsync( "git", ["diff", "--cached", "--name-only"], { cwd: workspaceRoot } ); changedFiles = [...new Set([ ...r1.stdout.trim().split("\n").filter(Boolean), ...r2.stdout.trim().split("\n").filter(Boolean), ])]; } catch { // git not available or not a git repo } if (changedFiles.length === 0) { vscode.window.showInformationMessage( "Code Graph: No changes detected." ); return; } const absFiles = changedFiles.map(f => path.join(workspaceRoot, f)); const impact = sqliteReader!.getImpactRadius(absFiles); // --- Generate review guidance --- const guidance: string[] = []; const impactedFileCount = new Set( impact.impactedNodes.map(n => n.filePath) ).size; // Test coverage check const untestedFns: string[] = []; for (const node of impact.changedNodes) { if (node.kind !== "Function" || node.isTest) { continue; } const edges = sqliteReader!.getEdgesByTarget(node.qualifiedName); const hasCoverage = edges.some(e => e.kind === "TESTED_BY"); if (!hasCoverage) { const out = sqliteReader!.getEdgesBySource(node.qualifiedName); if (!out.some(e => e.kind === "TESTED_BY")) { untestedFns.push(node.name); } } } if (untestedFns.length > 0) { guidance.push( `\u26a0\ufe0f **${untestedFns.length} changed function(s) lack test coverage**: ${untestedFns.slice(0, 5).join(", ")}${untestedFns.length > 5 ? "..." : ""}` ); } // Wide blast radius warning if (impactedFileCount > 10) { guidance.push( `\u26a0\ufe0f **Wide blast radius**: ${impactedFileCount} files impacted — consider splitting this change.` ); } // Inheritance changes const inheritanceChanges = impact.edges.filter( e => e.kind === "INHERITS" || e.kind === "IMPLEMENTS" ); if (inheritanceChanges.length > 0) { guidance.push( `\u26a0\ufe0f **Inheritance chain affected**: ${inheritanceChanges.length} inheritance/implementation edge(s) touched.` ); } // Cross-file impacts if (impact.impactedNodes.length > 0) { guidance.push( `\u2139\ufe0f ${impact.impactedNodes.length} nodes in ${impactedFileCount} file(s) may be affected by these changes.` ); } // Show guidance in output channel const channel = vscode.window.createOutputChannel("Code Graph Review", { log: true }); channel.appendLine("# Review Guidance"); channel.appendLine(""); channel.appendLine(`Changed files: ${changedFiles.length}`); channel.appendLine(`Changed nodes: ${impact.changedNodes.length}`); channel.appendLine(`Impacted nodes: ${impact.impactedNodes.length}`); channel.appendLine(`Impacted files: ${impactedFileCount}`); channel.appendLine(""); if (guidance.length > 0) { for (const g of guidance) { channel.appendLine(g); } } else { channel.appendLine("\u2705 No concerns detected."); } channel.show(true); // Also show in graph GraphWebviewPanel.createOrShow( context.extensionUri, sqliteReader!, impact ); // Update SCM decorations if (scmDecorationProvider && sqliteReader) { await scmDecorationProvider.update(sqliteReader, workspaceRoot); } } ); } ) ); } /** * Reinitialize the reader and tree providers after a graph rebuild. */ async function reinitialize( context: vscode.ExtensionContext ): Promise { const workspaceRoot = getWorkspaceRoot(); if (!workspaceRoot) { return; } const dbPath = findGraphDb(workspaceRoot); if (!dbPath) { return; } sqliteReader?.close(); sqliteReader = new SqliteReader(dbPath); // Refresh tree views await vscode.commands.executeCommand( "codeReviewGraph.codeGraph.refresh" ); } /** * Set up a FileSystemWatcher to detect changes to the graph database. */ function watchGraphDb(context: vscode.ExtensionContext): void { const watcher = vscode.workspace.createFileSystemWatcher( "**/.code-review-graph/graph.db" ); const dbPathRef = { current: "" }; const workspaceRoot = getWorkspaceRoot(); if (workspaceRoot) { const dbPath = findGraphDb(workspaceRoot); if (dbPath) { dbPathRef.current = dbPath; } } watcher.onDidChange(() => { // Close and reopen to pick up external writes if (sqliteReader && dbPathRef.current) { sqliteReader.close(); sqliteReader = new SqliteReader(dbPathRef.current); vscode.commands.executeCommand("codeReviewGraph.codeGraph.refresh"); } }); watcher.onDidCreate(async () => { const wsRoot = getWorkspaceRoot(); if (wsRoot && !sqliteReader) { const dbPath = findGraphDb(wsRoot); if (dbPath) { dbPathRef.current = dbPath; sqliteReader = new SqliteReader(dbPath); vscode.commands.executeCommand("codeReviewGraph.codeGraph.refresh"); } } }); watcher.onDidDelete(() => { sqliteReader?.close(); sqliteReader = undefined; dbPathRef.current = ""; }); context.subscriptions.push(watcher); } /** * Set up debounced auto-update on file save. */ function setupAutoUpdate( context: vscode.ExtensionContext, cli: CliWrapper ): void { const AUTO_UPDATE_DEBOUNCE_MS = 2000; const onSave = vscode.workspace.onDidSaveTextDocument(() => { const config = vscode.workspace.getConfiguration("codeReviewGraph"); if (!config.get("autoUpdate", true)) { return; } if (autoUpdateTimer) { clearTimeout(autoUpdateTimer); } autoUpdateTimer = setTimeout(async () => { const wsRoot = getWorkspaceRoot(); if (!wsRoot || !sqliteReader) { return; } try { await cli.updateGraph(wsRoot); } catch { // Silently ignore update errors on save; user can manually update } }, AUTO_UPDATE_DEBOUNCE_MS); }); context.subscriptions.push(onSave); } /** * Extension activation entry point. */ export async function activate( context: vscode.ExtensionContext ): Promise { const cli = new CliWrapper(); const installer = new Installer(cli); // Register walkthrough commands registerWalkthroughCommands(context, cli, installer); const workspaceRoot = getWorkspaceRoot(); if (workspaceRoot) { const dbPath = findGraphDb(workspaceRoot); if (dbPath) { // Graph database found - initialize sqliteReader = new SqliteReader(dbPath); // Schema compatibility check const schemaWarning = sqliteReader.checkSchemaCompatibility(); if (schemaWarning) { const choice = await vscode.window.showWarningMessage( `Code Graph: ${schemaWarning}`, "Rebuild Graph", "Dismiss" ); if (choice === "Rebuild Graph") { await vscode.commands.executeCommand("codeReviewGraph.buildGraph"); } } // Register tree view providers const codeGraphProvider = new CodeGraphTreeProvider( sqliteReader, workspaceRoot ); const blastRadiusProvider = new BlastRadiusTreeProvider(); const statsProvider = new StatsTreeProvider(sqliteReader); context.subscriptions.push( vscode.window.registerTreeDataProvider( "codeReviewGraph.codeGraph", codeGraphProvider ), vscode.window.registerTreeDataProvider( "codeReviewGraph.blastRadius", blastRadiusProvider ), vscode.window.registerTreeDataProvider( "codeReviewGraph.stats", statsProvider ) ); // Create status bar const statusBar = new StatusBar(); statusBar.update(sqliteReader); statusBar.show(); context.subscriptions.push(statusBar); // Register SCM file decoration provider scmDecorationProvider = new ScmDecorationProvider(); context.subscriptions.push( vscode.window.registerFileDecorationProvider(scmDecorationProvider) ); } else { // No graph database found - show welcome showWelcomeIfNeeded(context); } } // Register revealInTree command for bidirectional graph→tree sync context.subscriptions.push( vscode.commands.registerCommand( "codeReviewGraph.revealInTree", (_qualifiedName: string) => { // When a node is clicked in the graph, highlight it in the graph // The graph webview already calls this; the tree view sync relies // on the file navigation that nodeClicked also triggers. // This command is a hook for future tree reveal integration. GraphWebviewPanel.highlightNode(_qualifiedName); } ) ); // Register commands (always, even without a database) registerCommands(context, cli); // Watch for graph.db changes watchGraphDb(context); // Set up auto-update on save setupAutoUpdate(context, cli); } /** * Extension deactivation cleanup. */ export function deactivate(): void { if (autoUpdateTimer) { clearTimeout(autoUpdateTimer); autoUpdateTimer = undefined; } sqliteReader?.close(); sqliteReader = undefined; }