Init
This commit is contained in:
1 parent
08ccabb5a9
commit
cc6e7a5c89
57 files changed
+23315
-1
No files matched your search
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Grammar Loading and Caching
|
||||
*
|
||||
* Manages tree-sitter language grammars.
|
||||
*/
|
||||
|
||||
import Parser from 'tree-sitter';
|
||||
import { Language } from '../types';
|
||||
|
||||
// Grammar module imports
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const TypeScript = require('tree-sitter-typescript').typescript;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const TSX = require('tree-sitter-typescript').tsx;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const JavaScript = require('tree-sitter-javascript');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Python = require('tree-sitter-python');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Go = require('tree-sitter-go');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Rust = require('tree-sitter-rust');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Java = require('tree-sitter-java');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const C = require('tree-sitter-c');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Cpp = require('tree-sitter-cpp');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const CSharp = require('tree-sitter-c-sharp');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const PHP = require('tree-sitter-php').php;
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Ruby = require('tree-sitter-ruby');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Swift = require('tree-sitter-swift');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Kotlin = require('tree-sitter-kotlin');
|
||||
|
||||
/**
|
||||
* Mapping of Language to tree-sitter grammar
|
||||
*/
|
||||
const GRAMMAR_MAP: Record<string, unknown> = {
|
||||
typescript: TypeScript,
|
||||
tsx: TSX,
|
||||
javascript: JavaScript,
|
||||
jsx: JavaScript, // JSX uses the JavaScript grammar
|
||||
python: Python,
|
||||
go: Go,
|
||||
rust: Rust,
|
||||
java: Java,
|
||||
c: C,
|
||||
cpp: Cpp,
|
||||
csharp: CSharp,
|
||||
php: PHP,
|
||||
ruby: Ruby,
|
||||
swift: Swift,
|
||||
kotlin: Kotlin,
|
||||
};
|
||||
|
||||
/**
|
||||
* File extension to Language mapping
|
||||
*/
|
||||
export const EXTENSION_MAP: Record<string, Language> = {
|
||||
'.ts': 'typescript',
|
||||
'.tsx': 'tsx',
|
||||
'.js': 'javascript',
|
||||
'.mjs': 'javascript',
|
||||
'.cjs': 'javascript',
|
||||
'.jsx': 'jsx',
|
||||
'.py': 'python',
|
||||
'.pyw': 'python',
|
||||
'.go': 'go',
|
||||
'.rs': 'rust',
|
||||
'.java': 'java',
|
||||
'.c': 'c',
|
||||
'.h': 'c', // Could also be C++, defaulting to C
|
||||
'.cpp': 'cpp',
|
||||
'.cc': 'cpp',
|
||||
'.cxx': 'cpp',
|
||||
'.hpp': 'cpp',
|
||||
'.hxx': 'cpp',
|
||||
'.cs': 'csharp',
|
||||
'.php': 'php',
|
||||
'.rb': 'ruby',
|
||||
'.rake': 'ruby',
|
||||
'.swift': 'swift',
|
||||
'.kt': 'kotlin',
|
||||
'.kts': 'kotlin',
|
||||
};
|
||||
|
||||
/**
|
||||
* Cache for initialized parsers
|
||||
*/
|
||||
const parserCache = new Map<Language, Parser>();
|
||||
|
||||
/**
|
||||
* Get a parser for the specified language
|
||||
*/
|
||||
export function getParser(language: Language): Parser | null {
|
||||
// Check cache first
|
||||
if (parserCache.has(language)) {
|
||||
return parserCache.get(language)!;
|
||||
}
|
||||
|
||||
// Get grammar for language
|
||||
const grammar = GRAMMAR_MAP[language];
|
||||
if (!grammar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create and cache parser
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(grammar as Parameters<typeof parser.setLanguage>[0]);
|
||||
parserCache.set(language, parser);
|
||||
|
||||
return parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect language from file extension
|
||||
*/
|
||||
export function detectLanguage(filePath: string): Language {
|
||||
const ext = filePath.substring(filePath.lastIndexOf('.')).toLowerCase();
|
||||
return EXTENSION_MAP[ext] || 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a language is supported
|
||||
*/
|
||||
export function isLanguageSupported(language: Language): boolean {
|
||||
return language !== 'unknown' && language in GRAMMAR_MAP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all supported languages
|
||||
*/
|
||||
export function getSupportedLanguages(): Language[] {
|
||||
return Object.keys(GRAMMAR_MAP) as Language[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the parser cache (useful for testing)
|
||||
*/
|
||||
export function clearParserCache(): void {
|
||||
parserCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get language display name
|
||||
*/
|
||||
export function getLanguageDisplayName(language: Language): string {
|
||||
const names: Record<Language, string> = {
|
||||
typescript: 'TypeScript',
|
||||
javascript: 'JavaScript',
|
||||
tsx: 'TypeScript (TSX)',
|
||||
jsx: 'JavaScript (JSX)',
|
||||
python: 'Python',
|
||||
go: 'Go',
|
||||
rust: 'Rust',
|
||||
java: 'Java',
|
||||
c: 'C',
|
||||
cpp: 'C++',
|
||||
csharp: 'C#',
|
||||
php: 'PHP',
|
||||
ruby: 'Ruby',
|
||||
swift: 'Swift',
|
||||
kotlin: 'Kotlin',
|
||||
unknown: 'Unknown',
|
||||
};
|
||||
return names[language] || language;
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
/**
|
||||
* Extraction Orchestrator
|
||||
*
|
||||
* Coordinates file scanning, parsing, and database storage.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as crypto from 'crypto';
|
||||
import {
|
||||
Language,
|
||||
FileRecord,
|
||||
ExtractionResult,
|
||||
ExtractionError,
|
||||
CodeGraphConfig,
|
||||
} from '../types';
|
||||
import { QueryBuilder } from '../db/queries';
|
||||
import { extractFromSource } from './tree-sitter';
|
||||
import { detectLanguage, isLanguageSupported } from './grammars';
|
||||
import { logDebug } from '../errors';
|
||||
|
||||
/**
|
||||
* Progress callback for indexing operations
|
||||
*/
|
||||
export interface IndexProgress {
|
||||
phase: 'scanning' | 'parsing' | 'storing' | 'resolving';
|
||||
current: number;
|
||||
total: number;
|
||||
currentFile?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of an indexing operation
|
||||
*/
|
||||
export interface IndexResult {
|
||||
success: boolean;
|
||||
filesIndexed: number;
|
||||
filesSkipped: number;
|
||||
nodesCreated: number;
|
||||
edgesCreated: number;
|
||||
errors: ExtractionError[];
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of a sync operation
|
||||
*/
|
||||
export interface SyncResult {
|
||||
filesChecked: number;
|
||||
filesAdded: number;
|
||||
filesModified: number;
|
||||
filesRemoved: number;
|
||||
nodesUpdated: number;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate SHA256 hash of file contents
|
||||
*/
|
||||
export function hashContent(content: string): string {
|
||||
return crypto.createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path matches any glob pattern (simplified)
|
||||
*/
|
||||
function matchesGlob(filePath: string, pattern: string): boolean {
|
||||
// Convert glob to regex (simplified)
|
||||
const regexStr = pattern
|
||||
.replace(/\./g, '\\.')
|
||||
.replace(/\*\*/g, '<<<GLOBSTAR>>>')
|
||||
.replace(/\*/g, '[^/]*')
|
||||
.replace(/<<<GLOBSTAR>>>/g, '.*')
|
||||
.replace(/\?/g, '.');
|
||||
const regex = new RegExp(`^${regexStr}$`);
|
||||
return regex.test(filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file should be included based on config
|
||||
*/
|
||||
export function shouldIncludeFile(
|
||||
filePath: string,
|
||||
config: CodeGraphConfig
|
||||
): boolean {
|
||||
// Check exclude patterns first
|
||||
for (const pattern of config.exclude) {
|
||||
if (matchesGlob(filePath, pattern)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check include patterns
|
||||
for (const pattern of config.include) {
|
||||
if (matchesGlob(filePath, pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scan directory for source files
|
||||
*/
|
||||
export function scanDirectory(
|
||||
rootDir: string,
|
||||
config: CodeGraphConfig,
|
||||
onProgress?: (current: number, file: string) => void
|
||||
): string[] {
|
||||
const files: string[] = [];
|
||||
let count = 0;
|
||||
|
||||
function walk(dir: string): void {
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
logDebug('Skipping unreadable directory', { dir, error: String(error) });
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const relativePath = path.relative(rootDir, fullPath);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Check if directory should be excluded
|
||||
const dirPattern = relativePath + '/';
|
||||
let excluded = false;
|
||||
for (const pattern of config.exclude) {
|
||||
if (matchesGlob(dirPattern, pattern) || matchesGlob(relativePath, pattern)) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!excluded) {
|
||||
walk(fullPath);
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
if (shouldIncludeFile(relativePath, config)) {
|
||||
files.push(relativePath);
|
||||
count++;
|
||||
if (onProgress) {
|
||||
onProgress(count, relativePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(rootDir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extraction orchestrator
|
||||
*/
|
||||
export class ExtractionOrchestrator {
|
||||
private rootDir: string;
|
||||
private config: CodeGraphConfig;
|
||||
private queries: QueryBuilder;
|
||||
|
||||
constructor(rootDir: string, config: CodeGraphConfig, queries: QueryBuilder) {
|
||||
this.rootDir = rootDir;
|
||||
this.config = config;
|
||||
this.queries = queries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index all files in the project
|
||||
*/
|
||||
async indexAll(
|
||||
onProgress?: (progress: IndexProgress) => void,
|
||||
signal?: AbortSignal
|
||||
): Promise<IndexResult> {
|
||||
const startTime = Date.now();
|
||||
const errors: ExtractionError[] = [];
|
||||
let filesIndexed = 0;
|
||||
let filesSkipped = 0;
|
||||
let totalNodes = 0;
|
||||
let totalEdges = 0;
|
||||
|
||||
// Phase 1: Scan for files
|
||||
onProgress?.({
|
||||
phase: 'scanning',
|
||||
current: 0,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const files = scanDirectory(this.rootDir, this.config, (current, file) => {
|
||||
onProgress?.({
|
||||
phase: 'scanning',
|
||||
current,
|
||||
total: 0,
|
||||
currentFile: file,
|
||||
});
|
||||
});
|
||||
|
||||
if (signal?.aborted) {
|
||||
return {
|
||||
success: false,
|
||||
filesIndexed: 0,
|
||||
filesSkipped: 0,
|
||||
nodesCreated: 0,
|
||||
edgesCreated: 0,
|
||||
errors: [{ message: 'Aborted', severity: 'error' }],
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
// Phase 2: Parse files
|
||||
const total = files.length;
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (signal?.aborted) {
|
||||
return {
|
||||
success: false,
|
||||
filesIndexed,
|
||||
filesSkipped,
|
||||
nodesCreated: totalNodes,
|
||||
edgesCreated: totalEdges,
|
||||
errors: [{ message: 'Aborted', severity: 'error' }, ...errors],
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
const filePath = files[i]!;
|
||||
onProgress?.({
|
||||
phase: 'parsing',
|
||||
current: i + 1,
|
||||
total,
|
||||
currentFile: filePath,
|
||||
});
|
||||
|
||||
const result = await this.indexFile(filePath);
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
errors.push(...result.errors);
|
||||
}
|
||||
|
||||
if (result.nodes.length > 0) {
|
||||
filesIndexed++;
|
||||
totalNodes += result.nodes.length;
|
||||
totalEdges += result.edges.length;
|
||||
} else if (result.errors.length === 0) {
|
||||
filesSkipped++;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Resolve references
|
||||
onProgress?.({
|
||||
phase: 'resolving',
|
||||
current: 0,
|
||||
total: 1,
|
||||
});
|
||||
|
||||
// TODO: Implement reference resolution in Phase 3
|
||||
|
||||
return {
|
||||
success: errors.filter((e) => e.severity === 'error').length === 0,
|
||||
filesIndexed,
|
||||
filesSkipped,
|
||||
nodesCreated: totalNodes,
|
||||
edgesCreated: totalEdges,
|
||||
errors,
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Index specific files
|
||||
*/
|
||||
async indexFiles(filePaths: string[]): Promise<IndexResult> {
|
||||
const startTime = Date.now();
|
||||
const errors: ExtractionError[] = [];
|
||||
let filesIndexed = 0;
|
||||
let filesSkipped = 0;
|
||||
let totalNodes = 0;
|
||||
let totalEdges = 0;
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const result = await this.indexFile(filePath);
|
||||
|
||||
if (result.errors.length > 0) {
|
||||
errors.push(...result.errors);
|
||||
}
|
||||
|
||||
if (result.nodes.length > 0) {
|
||||
filesIndexed++;
|
||||
totalNodes += result.nodes.length;
|
||||
totalEdges += result.edges.length;
|
||||
} else {
|
||||
filesSkipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: errors.filter((e) => e.severity === 'error').length === 0,
|
||||
filesIndexed,
|
||||
filesSkipped,
|
||||
nodesCreated: totalNodes,
|
||||
edgesCreated: totalEdges,
|
||||
errors,
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Index a single file
|
||||
*/
|
||||
async indexFile(relativePath: string): Promise<ExtractionResult> {
|
||||
const fullPath = path.join(this.rootDir, relativePath);
|
||||
|
||||
// Check file exists and is readable
|
||||
let content: string;
|
||||
let stats: fs.Stats;
|
||||
try {
|
||||
stats = fs.statSync(fullPath);
|
||||
content = fs.readFileSync(fullPath, 'utf-8');
|
||||
} catch (error) {
|
||||
return {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
unresolvedReferences: [],
|
||||
errors: [
|
||||
{
|
||||
message: `Failed to read file: ${error instanceof Error ? error.message : String(error)}`,
|
||||
severity: 'error',
|
||||
},
|
||||
],
|
||||
durationMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Check file size
|
||||
if (stats.size > this.config.maxFileSize) {
|
||||
return {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
unresolvedReferences: [],
|
||||
errors: [
|
||||
{
|
||||
message: `File exceeds max size (${stats.size} > ${this.config.maxFileSize})`,
|
||||
severity: 'warning',
|
||||
},
|
||||
],
|
||||
durationMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Detect language
|
||||
const language = detectLanguage(relativePath);
|
||||
if (!isLanguageSupported(language)) {
|
||||
return {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
unresolvedReferences: [],
|
||||
errors: [],
|
||||
durationMs: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Extract from source
|
||||
const result = extractFromSource(relativePath, content, language);
|
||||
|
||||
// Store in database
|
||||
if (result.nodes.length > 0 || result.errors.length === 0) {
|
||||
this.storeExtractionResult(relativePath, content, language, stats, result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store extraction result in database
|
||||
*/
|
||||
private storeExtractionResult(
|
||||
filePath: string,
|
||||
content: string,
|
||||
language: Language,
|
||||
stats: fs.Stats,
|
||||
result: ExtractionResult
|
||||
): void {
|
||||
const contentHash = hashContent(content);
|
||||
|
||||
// Check if file already exists and hasn't changed
|
||||
const existingFile = this.queries.getFileByPath(filePath);
|
||||
if (existingFile && existingFile.contentHash === contentHash) {
|
||||
return; // No changes
|
||||
}
|
||||
|
||||
// Delete existing data for this file
|
||||
if (existingFile) {
|
||||
this.queries.deleteFile(filePath);
|
||||
}
|
||||
|
||||
// Insert nodes
|
||||
if (result.nodes.length > 0) {
|
||||
this.queries.insertNodes(result.nodes);
|
||||
}
|
||||
|
||||
// Insert edges
|
||||
if (result.edges.length > 0) {
|
||||
this.queries.insertEdges(result.edges);
|
||||
}
|
||||
|
||||
// Insert unresolved references
|
||||
for (const ref of result.unresolvedReferences) {
|
||||
this.queries.insertUnresolvedRef(ref);
|
||||
}
|
||||
|
||||
// Insert file record
|
||||
const fileRecord: FileRecord = {
|
||||
path: filePath,
|
||||
contentHash,
|
||||
language,
|
||||
size: stats.size,
|
||||
modifiedAt: stats.mtimeMs,
|
||||
indexedAt: Date.now(),
|
||||
nodeCount: result.nodes.length,
|
||||
errors: result.errors.length > 0 ? result.errors : undefined,
|
||||
};
|
||||
this.queries.upsertFile(fileRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync with current file state
|
||||
*/
|
||||
async sync(onProgress?: (progress: IndexProgress) => void): Promise<SyncResult> {
|
||||
const startTime = Date.now();
|
||||
let filesChecked = 0;
|
||||
let filesAdded = 0;
|
||||
let filesModified = 0;
|
||||
let filesRemoved = 0;
|
||||
let nodesUpdated = 0;
|
||||
|
||||
// Get current files on disk
|
||||
onProgress?.({
|
||||
phase: 'scanning',
|
||||
current: 0,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
const currentFiles = new Set(scanDirectory(this.rootDir, this.config));
|
||||
filesChecked = currentFiles.size;
|
||||
|
||||
// Get tracked files from database
|
||||
const trackedFiles = this.queries.getAllFiles();
|
||||
|
||||
// Find files to remove (in DB but not on disk)
|
||||
for (const tracked of trackedFiles) {
|
||||
if (!currentFiles.has(tracked.path)) {
|
||||
this.queries.deleteFile(tracked.path);
|
||||
filesRemoved++;
|
||||
}
|
||||
}
|
||||
|
||||
// Find files to add or update
|
||||
const filesToIndex: string[] = [];
|
||||
|
||||
for (const filePath of currentFiles) {
|
||||
const fullPath = path.join(this.rootDir, filePath);
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync(fullPath, 'utf-8');
|
||||
} catch (error) {
|
||||
logDebug('Skipping unreadable file during sync', { filePath, error: String(error) });
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentHash = hashContent(content);
|
||||
const tracked = trackedFiles.find((f) => f.path === filePath);
|
||||
|
||||
if (!tracked) {
|
||||
// New file
|
||||
filesToIndex.push(filePath);
|
||||
filesAdded++;
|
||||
} else if (tracked.contentHash !== contentHash) {
|
||||
// Modified file
|
||||
filesToIndex.push(filePath);
|
||||
filesModified++;
|
||||
}
|
||||
}
|
||||
|
||||
// Index changed files
|
||||
const total = filesToIndex.length;
|
||||
for (let i = 0; i < filesToIndex.length; i++) {
|
||||
const filePath = filesToIndex[i]!;
|
||||
onProgress?.({
|
||||
phase: 'parsing',
|
||||
current: i + 1,
|
||||
total,
|
||||
currentFile: filePath,
|
||||
});
|
||||
|
||||
const result = await this.indexFile(filePath);
|
||||
nodesUpdated += result.nodes.length;
|
||||
}
|
||||
|
||||
return {
|
||||
filesChecked,
|
||||
filesAdded,
|
||||
filesModified,
|
||||
filesRemoved,
|
||||
nodesUpdated,
|
||||
durationMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get files that have changed since last index
|
||||
*/
|
||||
getChangedFiles(): { added: string[]; modified: string[]; removed: string[] } {
|
||||
const currentFiles = new Set(scanDirectory(this.rootDir, this.config));
|
||||
const trackedFiles = this.queries.getAllFiles();
|
||||
|
||||
const added: string[] = [];
|
||||
const modified: string[] = [];
|
||||
const removed: string[] = [];
|
||||
|
||||
// Find removed files
|
||||
for (const tracked of trackedFiles) {
|
||||
if (!currentFiles.has(tracked.path)) {
|
||||
removed.push(tracked.path);
|
||||
}
|
||||
}
|
||||
|
||||
// Find added and modified files
|
||||
for (const filePath of currentFiles) {
|
||||
const fullPath = path.join(this.rootDir, filePath);
|
||||
let content: string;
|
||||
try {
|
||||
content = fs.readFileSync(fullPath, 'utf-8');
|
||||
} catch (error) {
|
||||
logDebug('Skipping unreadable file while detecting changes', { filePath, error: String(error) });
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentHash = hashContent(content);
|
||||
const tracked = trackedFiles.find((f) => f.path === filePath);
|
||||
|
||||
if (!tracked) {
|
||||
added.push(filePath);
|
||||
} else if (tracked.contentHash !== contentHash) {
|
||||
modified.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
return { added, modified, removed };
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export useful types and functions
|
||||
export { extractFromSource } from './tree-sitter';
|
||||
export { detectLanguage, isLanguageSupported, getSupportedLanguages } from './grammars';
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user