diff --git a/.github/renovate-automerge.json5 b/.github/renovate-automerge.json5 deleted file mode 100644 index 730aa7f..0000000 --- a/.github/renovate-automerge.json5 +++ /dev/null @@ -1,55 +0,0 @@ -{ - // Automerge configuration for Renovate - "packageRules": [ - // Automerge for non-major devDependencies - { - "description": "Automatically merge minor and patch updates for dev dependencies", - "matchDepTypes": ["devDependencies"], - "matchUpdateTypes": ["minor", "patch"], - "automerge": true, - "platformAutomerge": true - }, - - // Automerge for patch-only production dependencies - { - "description": "Automatically merge patch updates for production dependencies", - "matchDepTypes": ["dependencies"], - "matchUpdateTypes": ["patch"], - "automerge": true, - "platformAutomerge": true, - "prCreation": "immediate" - }, - - // Automerge GitHub Action updates - { - "description": "Automatically merge GitHub Actions", - "matchManagers": ["github-actions"], - "matchUpdateTypes": ["minor", "patch"], - "automerge": true, - "platformAutomerge": true - }, - - // Skip certain dependency types or packages from automerge - { - "description": "Don't automerge major dependency updates", - "matchUpdateTypes": ["major"], - "automerge": false - }, - - // Handle peer dependencies correctly - { - "description": "Widen ranges for peer dependencies", - "matchDepTypes": ["peerDependencies"], - "rangeStrategy": "widen" - } - ], - - // Avoid weekend PRs for major changes - "major": { - "schedule": ["after 10pm and before 5am every weekday"] - }, - - // Additional automerge settings - "transitiveRemediation": true, - "minimumReleaseAge": "3 days" -} \ No newline at end of file diff --git a/.github/renovate-groups.json5 b/.github/renovate-groups.json5 deleted file mode 100644 index 261b24d..0000000 --- a/.github/renovate-groups.json5 +++ /dev/null @@ -1,84 +0,0 @@ -{ - // Package grouping configuration for Renovate - "packageRules": [ - // Group all non-major dependencies - { - "description": "Group all non-major dependencies together", - "matchPackagePatterns": ["*"], - "matchUpdateTypes": ["minor", "patch"], - "groupName": "all non-major dependencies", - "groupSlug": "all-minor-patch" - }, - - // Group TypeScript ecosystem - { - "description": "Group TypeScript and related tooling", - "matchPackagePatterns": [ - "^@tsconfig/", - "^typescript", - "^ts-", - "^@types/" - ], - "groupName": "typescript ecosystem", - "groupSlug": "typescript" - }, - - // Group formatting and linting tools - { - "description": "Group formatting and linting tools", - "matchPackagePatterns": [ - "^@biomejs/", - "^eslint", - "^@eslint", - "^prettier" - ], - "groupName": "linting and formatting", - "groupSlug": "linting-formatting" - }, - - // Group testing tools - { - "description": "Group testing dependencies", - "matchPackagePatterns": [ - "^vitest", - "^@vitest/", - "^jest", - "^@jest/" - ], - "groupName": "testing dependencies", - "groupSlug": "testing" - }, - - // Group monorepo tooling - { - "description": "Group monorepo tooling", - "matchPackagePatterns": [ - "^@changesets/", - "^turbo", - "^lerna", - "^nx" - ], - "groupName": "monorepo tooling", - "groupSlug": "monorepo-tools" - }, - - // Group GitHub Actions - { - "description": "Group GitHub Actions", - "matchManagers": ["github-actions"], - "groupName": "GitHub Actions", - "groupSlug": "github-actions" - }, - - // Group internal workspace packages - { - "description": "Group internal workspace packages", - "matchPackagePatterns": [ - "^@monorepo/" - ], - "matchUpdateTypes": ["major", "minor", "patch"], - "groupName": "internal packages", - "groupSlug": "internal-deps" - } - ] -} \ No newline at end of file diff --git a/.github/workflows/renovate.yml b/.github/workflows/renovate.yml deleted file mode 100644 index edb7494..0000000 --- a/.github/workflows/renovate.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Renovate - -on: - # Schedule: run every day at 2am - schedule: - - cron: '0 2 * * *' - - # Run on demand via workflow_dispatch - workflow_dispatch: - inputs: - logLevel: - description: 'Renovate log level' - required: true - default: 'info' - type: choice - options: - - debug - - info - - warn - - error - dryRun: - description: 'Dry run' - type: boolean - default: false - -# Prevent multiple Renovate runs from happening simultaneously -concurrency: - group: renovate - cancel-in-progress: false - -jobs: - renovate: - name: Renovate - runs-on: ubuntu-latest - - # Allow renovate to create PRs - permissions: - contents: write - pull-requests: write - issues: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - # Use custom docker-based renovate to run self-hosted - - name: Self-hosted Renovate - uses: renovatebot/github-action@v40.0.4 - with: - configurationFile: renovate.json5 - token: ${{ secrets.GITHUB_TOKEN }} - env: - LOG_LEVEL: ${{ inputs.logLevel || 'info' }} - DRY_RUN: ${{ inputs.dryRun == true && 'full' || '' }} - RENOVATE_BASE_BRANCHES: 'main' - RENOVATE_EXTENDS: 'config:recommended,group:allNonMajor' - # Include local configuration files - RENOVATE_CONFIG_FILE: 'renovate.json5' - RENOVATE_EXTENDS_FROM: | - .github/renovate-automerge.json5 - .github/renovate-groups.json5 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 9245f03..7cfc81a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -234,4 +234,13 @@ For advanced users or development, you can manually configure the MCP server: - Memory leak prevention (circular buffers for history/metrics) - Graceful shutdown (proper event listener cleanup) - Health monitoring (`dev_health` tool) - - Comprehensive logging with @lytics/kero \ No newline at end of file + - Comprehensive logging with @lytics/kero + +5. **Performance Configuration**: + - Configurable concurrency via environment variables + - Auto-detection based on system resources (CPU/memory) + - Environment variables: + - `DEV_AGENT_CONCURRENCY` - global concurrency setting + - `DEV_AGENT_TYPESCRIPT_CONCURRENCY` - TypeScript file processing + - `DEV_AGENT_INDEXER_CONCURRENCY` - vector embedding batches + - Example: `export DEV_AGENT_CONCURRENCY=10` before running `dev index .` \ No newline at end of file diff --git a/LANGUAGE_SUPPORT.md b/LANGUAGE_SUPPORT.md new file mode 100644 index 0000000..7eb3059 --- /dev/null +++ b/LANGUAGE_SUPPORT.md @@ -0,0 +1,194 @@ +# Adding New Language Support to Dev-Agent + +This guide explains how to add support for new programming languages to dev-agent's code scanning and indexing capabilities. + +## Current Language Support + +- **TypeScript/JavaScript**: Full support (TypeScript Compiler API) +- **Go**: Full support (tree-sitter) +- **Other languages**: Not yet supported + +## Architecture Overview + +Dev-agent supports languages through two mechanisms: + +1. **TypeScript Compiler API**: For TypeScript/JavaScript files +2. **Tree-sitter**: For other languages (currently Go only) + +## Adding Tree-sitter Language Support + +### Prerequisites + +- Language grammar available in [tree-sitter-wasms](https://www.npmjs.com/package/tree-sitter-wasms) +- Understanding of the language's syntax and structure + +### Step-by-Step Process + +#### 1. Update Type Definitions + +Edit `packages/core/src/scanner/tree-sitter.ts`: + +```typescript +// Add your language to the type +export type TreeSitterLanguage = 'go' | 'python' | 'rust'; +``` + +#### 2. Update WASM Bundling + +Edit `packages/dev-agent/scripts/copy-wasm.js`: + +```javascript +// Add your language to supported list +const SUPPORTED_LANGUAGES = ['go', 'python', 'rust']; +``` + +#### 3. Create Language Scanner + +Create `packages/core/src/scanner/{language}.ts`: + +```typescript +import { parseCode, type ParsedTree } from './tree-sitter'; +import type { Document, Scanner } from './types'; + +export class PythonScanner implements Scanner { + canHandle(filePath: string): boolean { + return path.extname(filePath).toLowerCase() === '.py'; + } + + async scan(files: string[], repoRoot: string, logger?: Logger): Promise { + // Implementation using tree-sitter queries + } +} +``` + +#### 4. Add Tree-sitter Queries + +Define language-specific tree-sitter queries for extracting: +- Functions/methods +- Classes/structs +- Interfaces/traits +- Type definitions +- Documentation + +Example for Python: +```typescript +const PYTHON_QUERIES = { + functions: ` + (function_definition + name: (identifier) @name + parameters: (parameters) @params + body: (block) @body) @function + `, + classes: ` + (class_definition + name: (identifier) @name + body: (block) @body) @class + ` +}; +``` + +#### 5. Register Scanner + +Edit `packages/core/src/scanner/index.ts`: + +```typescript +import { PythonScanner } from './python'; + +// Add to scanner registration +export const SCANNERS = [ + new TypeScriptScanner(), + new GoScanner(), + new PythonScanner(), // Add your scanner +]; +``` + +#### 6. Test Language Support + +```bash +# Install dependencies +pnpm install + +# Build with new language support +pnpm build + +# Test with sample files +dev index ./test-project --languages python +``` + +## Configuration Options + +### Environment Variables + +Control scanner behavior: +```bash +# General concurrency +export DEV_AGENT_CONCURRENCY=10 + +# Language-specific concurrency +export DEV_AGENT_PYTHON_CONCURRENCY=5 +``` + +### Bundle Size Considerations + +Each language adds ~200-500KB to the bundle via WASM files. Consider: +- Bundle size impact +- User demand for the language +- Maintenance overhead + +## Best Practices + +### Scanner Implementation + +1. **Error Handling**: Follow the pattern in `GoScanner` for robust error handling +2. **Progress Logging**: Include progress updates for large codebases +3. **Documentation Extraction**: Extract docstrings/comments when available +4. **Performance**: Use efficient tree-sitter queries + +### Testing + +1. **Unit Tests**: Test scanner with various code samples +2. **Integration Tests**: Test full indexing pipeline +3. **Performance Tests**: Benchmark against large codebases + +### Documentation Extraction + +Extract meaningful documentation: +```typescript +// Good: Extract function purpose and parameters +const doc = extractDocumentation(node); + +// Bad: Extract only raw syntax +const doc = node.text; +``` + +## Tree-sitter Resources + +- [Tree-sitter website](https://tree-sitter.github.io/) +- [Available grammars](https://github.com/tree-sitter) +- [Query syntax guide](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax) + +## Contributing + +1. Open an issue discussing the language addition +2. Implement following the steps above +3. Include tests and documentation +4. Submit a pull request + +## Troubleshooting + +### Common Issues + +**WASM files not found**: +- Ensure `tree-sitter-wasms` contains your language +- Check `copy-wasm.js` configuration + +**Parser initialization fails**: +- Verify tree-sitter grammar compatibility +- Check for syntax errors in queries + +**Performance issues**: +- Profile tree-sitter queries +- Consider reducing query complexity +- Adjust concurrency settings + +For help, see existing scanners in `packages/core/src/scanner/` or open an issue. \ No newline at end of file diff --git a/README.md b/README.md index 827ad33..9eec8b7 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,68 @@ dev mcp install # Install for Claude Code dev mcp list # List configured servers ``` +## Configuration + +### Performance Tuning + +Control scanning and indexing performance using environment variables: + +```bash +# Global concurrency setting (applies to all operations) +export DEV_AGENT_CONCURRENCY=10 + +# Language-specific concurrency settings +export DEV_AGENT_TYPESCRIPT_CONCURRENCY=20 # TypeScript file processing +export DEV_AGENT_INDEXER_CONCURRENCY=5 # Vector embedding batches + +# Index with custom settings +dev index . +``` + +**Auto-detection:** If no environment variables are set, dev-agent automatically detects optimal concurrency based on your system's CPU and memory. + +**Recommended settings:** + +| System Type | Global | TypeScript | Indexer | Notes | +|-------------|--------|------------|---------|-------| +| Low memory (<4GB) | 5 | 5 | 2 | Prevents OOM errors | +| Standard (4-8GB) | 15 | 15 | 3 | Balanced performance | +| High-end (8GB+, 8+ cores) | 30 | 30 | 5 | Maximum speed | + +### Language Support + +Current language support: + +- **TypeScript/JavaScript**: Full support (`.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, `.cjs`) +- **Go**: Full support (`.go`) + +To add new languages, see [LANGUAGE_SUPPORT.md](LANGUAGE_SUPPORT.md). + +### Troubleshooting + +**Indexing too slow:** +```bash +# Try increasing concurrency (if you have enough memory) +export DEV_AGENT_CONCURRENCY=20 +dev index . +``` + +**Out of memory errors:** +```bash +# Reduce concurrency +export DEV_AGENT_CONCURRENCY=5 +export DEV_AGENT_TYPESCRIPT_CONCURRENCY=5 +export DEV_AGENT_INDEXER_CONCURRENCY=2 +dev index . +``` + +**Go scanner not working:** +```bash +# Check if WASM files are bundled (after installation/build) +ls -la ~/.local/share/dev-agent/dist/wasm/tree-sitter-go.wasm +# If missing, try reinstalling or rebuilding from source +``` + ## Project Structure ``` @@ -303,10 +365,17 @@ pnpm typecheck ## Version History -- **v0.6.0** - Go Language Support +- **v0.6.0** - Go Language Support & Performance Improvements - Go scanner with tree-sitter WASM (functions, methods, structs, interfaces, generics) + - Configurable concurrency via environment variables (`DEV_AGENT_*_CONCURRENCY`) + - Auto-detection of optimal performance settings based on system resources + - Enhanced error handling and user feedback across all scanners + - Improved Go scanner with runtime WASM validation and better error messages + - Parallel processing optimizations for TypeScript scanning and indexing - Indexer logging with `--verbose` flag and progress spinners - Go-specific exclusions (*.pb.go, *.gen.go, mocks/, testdata/) + - Comprehensive language support documentation (`LANGUAGE_SUPPORT.md`) + - Build-time validation to prevent silent WASM dependency failures - Infrastructure for future Python/Rust support - **v0.4.0** - Intelligent Git History release - New `dev_history` tool for semantic commit search diff --git a/docs/DEPENDENCY_MANAGEMENT.md b/docs/DEPENDENCY_MANAGEMENT.md deleted file mode 100644 index 149e673..0000000 --- a/docs/DEPENDENCY_MANAGEMENT.md +++ /dev/null @@ -1,92 +0,0 @@ -# Dependency Management - -This project uses [Renovate](https://docs.renovatebot.com/) to automate dependency updates across the monorepo. This document describes how dependency updates are managed and how to work with the system. - -## Overview - -Renovate automatically: - -1. Monitors all dependencies in the monorepo -2. Creates pull requests for updates -3. Groups related updates together to reduce PR noise -4. Automatically merges safe updates -5. Maintains a dependency dashboard - -## Configuration Files - -The Renovate configuration is split across several files: - -- `renovate.json5` - Main configuration file -- `.github/renovate-automerge.json5` - Auto-merge settings -- `.github/renovate-groups.json5` - Package grouping settings -- `.github/workflows/renovate.yml` - GitHub workflow for self-hosted Renovate - -## Update Schedule - -Renovate is configured to run: - -- Daily at 2:00 AM UTC via GitHub Actions -- Only creates PRs during off-hours (after 10pm and before 5am on weekdays, and on weekends) -- Lock file maintenance on Monday mornings -- Can be triggered manually via GitHub Actions workflow dispatch - -## Update Strategy - -The system follows these strategies: - -1. **Minor and patch updates** are grouped together and auto-merged if tests pass -2. **Dev dependencies** are automatically merged for minor and patch versions -3. **Production dependencies** have stricter rules, with only patch versions auto-merged -4. **Major updates** require manual review and approval -5. **GitHub Actions** are automatically updated for minor and patch versions - -## Package Grouping - -Dependencies are grouped into logical categories: - -- TypeScript ecosystem (TypeScript, tsconfig, type definitions) -- Linting and formatting tools (Biome, ESLint, Prettier) -- Testing tools (Vitest, Jest) -- Monorepo tooling (Changesets, Turborepo) -- Internal workspace packages - -## Working with Renovate - -### Dependency Dashboard - -Renovate maintains a dependency dashboard as a pinned issue in the repository. This provides: - -- Overview of all pending updates -- PRs that need attention -- Rejected/ignored updates -- Upcoming major updates - -### Manual Control - -You can influence Renovate behavior: - -- Add `renovate:disable` comment in a PR to prevent Renovate from rebasing it -- Add specific packages to `ignoreDeps` in configuration to prevent updates -- Use the dashboard to manually trigger specific updates - -### Best Practices - -1. **Review major updates carefully** - These can contain breaking changes -2. **Don't modify package.json versions directly** - Let Renovate manage this -3. **Use the dashboard** to monitor and manage update flow -4. **Create changeset entries** for dependency updates that affect consumers - -## Troubleshooting - -If Renovate isn't working as expected: - -1. Check the Renovate logs in GitHub Actions -2. Verify configuration files are valid JSON5 -3. Look at the dependency dashboard for errors -4. Run Renovate manually via workflow dispatch with debug logging - -## Further Reading - -- [Renovate Documentation](https://docs.renovatebot.com/) -- [Renovate GitHub Action](https://github.com/renovatebot/github-action) -- [Monorepo Support in Renovate](https://docs.renovatebot.com/getting-started/installing-onboarding/#monorepos) \ No newline at end of file diff --git a/packages/cli/src/utils/cursor-config.test.ts b/packages/cli/src/utils/cursor-config.test.ts index 38a95ff..49a2c4d 100644 --- a/packages/cli/src/utils/cursor-config.test.ts +++ b/packages/cli/src/utils/cursor-config.test.ts @@ -264,7 +264,7 @@ describe('Cursor Config Utilities', () => { it('should handle HTTP servers', async () => { // Add an HTTP server (like Figma) (type assertion for test data) const config = await cursorConfig.readCursorConfig(); - (config.mcpServers as any)['figma'] = { + (config.mcpServers as any).figma = { url: 'https://mcp.figma.com/mcp', headers: {}, }; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index bf719c1..d515106 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -6,7 +6,12 @@ "composite": true, "types": ["node", "vitest/globals"] }, - "references": [{ "path": "../core" }, { "path": "../mcp-server" }, { "path": "../subagents" }, { "path": "../logger" }], + "references": [ + { "path": "../core" }, + { "path": "../mcp-server" }, + { "path": "../subagents" }, + { "path": "../logger" } + ], "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } diff --git a/packages/core/package.json b/packages/core/package.json index 85da46a..ea03dd3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -30,6 +30,7 @@ "test:watch": "vitest" }, "devDependencies": { + "tree-sitter-wasms": "^0.1.13", "@types/mdast": "^4.0.4", "@types/node": "^24.10.1", "typescript": "^5.3.3" @@ -42,7 +43,6 @@ "remark": "^15.0.1", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", - "tree-sitter-wasms": "^0.1.13", "ts-morph": "^27.0.2", "unified": "^11.0.5", "web-tree-sitter": "^0.25.10" diff --git a/packages/core/src/indexer/index.ts b/packages/core/src/indexer/index.ts index 8cdafad..43805f2 100644 --- a/packages/core/src/indexer/index.ts +++ b/packages/core/src/indexer/index.ts @@ -7,8 +7,9 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; import { scanRepository } from '../scanner'; import type { Document } from '../scanner/types'; +import { getCurrentSystemResources, getOptimalConcurrency } from '../utils/concurrency'; import { VectorStorage } from '../vector'; -import type { SearchOptions, SearchResult } from '../vector/types'; +import type { EmbeddingDocument, SearchOptions, SearchResult } from '../vector/types'; import type { FileMetadata, IndexError, @@ -69,7 +70,7 @@ export class RepositoryIndexer { const errors: IndexError[] = []; let filesScanned = 0; let documentsExtracted = 0; - let documentsIndexed = 0; + const _documentsIndexed = 0; try { // Phase 1: Scan repository @@ -90,7 +91,7 @@ export class RepositoryIndexer { logger: options.logger, }); - filesScanned = scanResult.documents.length; + filesScanned = scanResult.stats.filesScanned; documentsExtracted = scanResult.documents.length; // Phase 2: Prepare documents for embedding @@ -127,44 +128,90 @@ export class RepositoryIndexer { const batchSize = options.batchSize || this.config.batchSize; const totalBatches = Math.ceil(embeddingDocuments.length / batchSize); - let batchNum = 0; + // Process batches in parallel for better performance + // Similar to TypeScript scanner: process multiple batches concurrently + const CONCURRENCY = this.getOptimalConcurrency('indexer'); // Configurable concurrency + + // Create batches + const batches: EmbeddingDocument[][] = []; for (let i = 0; i < embeddingDocuments.length; i += batchSize) { - const batch = embeddingDocuments.slice(i, i + batchSize); - batchNum++; + batches.push(embeddingDocuments.slice(i, i + batchSize)); + } - try { - await this.vectorStorage.addDocuments(batch); - documentsIndexed += batch.length; - - // Log progress every 10 batches or on last batch - if (batchNum % 10 === 0 || batchNum === totalBatches) { - logger?.info( - { batch: batchNum, totalBatches, documentsIndexed, total: embeddingDocuments.length }, - `Embedded ${documentsIndexed}/${embeddingDocuments.length} documents` - ); + // Process batches in parallel groups + let documentsIndexed = 0; + const batchGroups: EmbeddingDocument[][][] = []; + for (let i = 0; i < batches.length; i += CONCURRENCY) { + batchGroups.push(batches.slice(i, i + CONCURRENCY)); + } + + for (let groupIndex = 0; groupIndex < batchGroups.length; groupIndex++) { + const batchGroup = batchGroups[groupIndex]; + + // Process all batches in this group concurrently + const results = await Promise.allSettled( + batchGroup.map(async (batch, batchIndexInGroup) => { + const batchNum = groupIndex * CONCURRENCY + batchIndexInGroup + 1; + try { + await this.vectorStorage.addDocuments(batch); + return { success: true, count: batch.length, batchNum }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + errors.push({ + type: 'storage', + message: `Failed to store batch ${batchNum}: ${errorMessage}`, + error: error instanceof Error ? error : undefined, + timestamp: new Date(), + }); + logger?.error({ batch: batchNum, error: errorMessage }, 'Batch embedding failed'); + return { success: false, count: 0, batchNum }; + } + }) + ); + + // Update progress after each group + for (const result of results) { + if (result.status === 'fulfilled' && result.value.success) { + documentsIndexed += result.value.count; } + } - onProgress?.({ - phase: 'storing', - filesProcessed: filesScanned, - totalFiles: filesScanned, - documentsIndexed, - totalDocuments: embeddingDocuments.length, - percentComplete: 66 + (documentsIndexed / embeddingDocuments.length) * 33, - }); - } catch (error) { - errors.push({ - type: 'storage', - message: `Failed to store batch: ${error instanceof Error ? error.message : String(error)}`, - error: error instanceof Error ? error : undefined, - timestamp: new Date(), - }); - logger?.error( - { batch: batchNum, error: error instanceof Error ? error.message : String(error) }, - 'Batch embedding failed' + // Log progress with time estimates every 5 batches or on last group + const currentBatchNum = (groupIndex + 1) * CONCURRENCY; + if (currentBatchNum % 5 === 0 || groupIndex === batchGroups.length - 1) { + const elapsed = Date.now() - startTime.getTime(); + const docsPerSecond = documentsIndexed / (elapsed / 1000); + const remainingDocs = embeddingDocuments.length - documentsIndexed; + const etaSeconds = Math.ceil(remainingDocs / docsPerSecond); + const etaMinutes = Math.floor(etaSeconds / 60); + const etaSecondsRemainder = etaSeconds % 60; + + const etaText = + etaMinutes > 0 ? `${etaMinutes}m ${etaSecondsRemainder}s` : `${etaSecondsRemainder}s`; + + logger?.info( + { + batch: Math.min(currentBatchNum, totalBatches), + totalBatches, + documentsIndexed, + total: embeddingDocuments.length, + docsPerSecond: Math.round(docsPerSecond * 10) / 10, + eta: etaText, + }, + `Embedded ${documentsIndexed}/${embeddingDocuments.length} documents (${Math.round(docsPerSecond)} docs/sec, ETA: ${etaText})` ); } + + // Update progress callback + onProgress?.({ + phase: 'storing', + filesProcessed: filesScanned, + totalFiles: filesScanned, + documentsIndexed, + totalDocuments: embeddingDocuments.length, + percentComplete: 66 + (documentsIndexed / embeddingDocuments.length) * 33, + }); } logger?.info({ documentsIndexed, errors: errors.length }, 'Embedding complete'); @@ -521,6 +568,17 @@ export class RepositoryIndexer { return { changed, added: uniqueAdded, deleted }; } + /** + * Get optimal concurrency level based on system resources and environment variables + */ + private getOptimalConcurrency(context: string): number { + return getOptimalConcurrency({ + context, + systemResources: getCurrentSystemResources(), + environmentVariables: process.env, + }); + } + /** * Get file extension for a language */ diff --git a/packages/core/src/scanner/go.ts b/packages/core/src/scanner/go.ts index 5ef3bf2..77b7312 100644 --- a/packages/core/src/scanner/go.ts +++ b/packages/core/src/scanner/go.ts @@ -5,9 +5,20 @@ * Uses tree-sitter queries for declarative pattern matching (similar to Aider's approach). */ -import * as fs from 'node:fs'; import * as path from 'node:path'; -import { extractGoDocComment, type ParsedTree, parseCode } from './tree-sitter'; +import type { Logger } from '@lytics/kero'; +import { + type FileSystemValidator, + NodeFileSystemValidator, + validateFile, +} from '../utils/file-validator'; +import { + extractGoDocComment, + initTreeSitter, + loadLanguage, + type ParsedTree, + parseCode, +} from './tree-sitter'; import type { Document, Scanner, ScannerCapabilities } from './types'; /** @@ -105,18 +116,110 @@ export class GoScanner implements Scanner { /** Maximum lines for code snippets */ private static readonly MAX_SNIPPET_LINES = 50; + /** File validator (injected for testability) */ + private fileValidator: FileSystemValidator; + + constructor(fileValidator: FileSystemValidator = new NodeFileSystemValidator()) { + this.fileValidator = fileValidator; + } + canHandle(filePath: string): boolean { const ext = path.extname(filePath).toLowerCase(); return ext === '.go'; } - async scan(files: string[], repoRoot: string): Promise { + /** + * Validate that Go scanning support is available + */ + private async validateGoSupport(): Promise { + try { + // Try to initialize tree-sitter and load Go language + await initTreeSitter(); + await loadLanguage('go'); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes('tree-sitter WASM') || errorMessage.includes('Failed to locate')) { + throw new Error( + 'Go tree-sitter WASM files not found. ' + + 'tree-sitter-go.wasm is required for Go code parsing.' + ); + } + throw error; + } + } + + async scan(files: string[], repoRoot: string, logger?: Logger): Promise { const documents: Document[] = []; + const total = files.length; + const errors: Array<{ + file: string; + absolutePath: string; + error: string; + phase: string; + stack?: string; + }> = []; + + // Runtime check: Ensure Go support is available + try { + await this.validateGoSupport(); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + logger?.error({ error: errorMessage }, 'Go scanner initialization failed'); + throw new Error( + `Go scanner cannot function: ${errorMessage}\n` + + 'This usually means tree-sitter WASM files are missing.\n' + + 'If you installed dev-agent from source, run: pnpm build\n' + + 'If you installed via npm, try reinstalling: npm install -g dev-agent' + ); + } + + const startTime = Date.now(); + + for (let i = 0; i < total; i++) { + const file = files[i]; + + // Log progress every 50 files (more frequent feedback) + if (logger && i > 0 && i % 50 === 0) { + const elapsed = Date.now() - startTime; + const filesPerSecond = i / (elapsed / 1000); + const remainingFiles = total - i; + const etaSeconds = Math.ceil(remainingFiles / filesPerSecond); + const etaMinutes = Math.floor(etaSeconds / 60); + const etaSecondsRemainder = etaSeconds % 60; + + const etaText = + etaMinutes > 0 ? `${etaMinutes}m ${etaSecondsRemainder}s` : `${etaSecondsRemainder}s`; + + const percent = Math.round((i / total) * 100); + logger.info( + { + filesProcessed: i, + total, + percent, + documents: documents.length, + filesPerSecond: Math.round(filesPerSecond * 10) / 10, + eta: etaText, + }, + `go ${i}/${total} (${percent}%) - ${documents.length} docs extracted, ${Math.round(filesPerSecond)} files/sec, ETA: ${etaText}` + ); + } - for (const file of files) { try { const absolutePath = path.join(repoRoot, file); - const sourceText = fs.readFileSync(absolutePath, 'utf-8'); + + // Validate file using testable utility + const validation = validateFile(file, absolutePath, this.fileValidator); + if (!validation.isValid) { + errors.push({ + file, + absolutePath, + error: validation.error || 'Unknown validation error', + phase: validation.phase || 'fileValidation', + }); + continue; + } + + const sourceText = this.fileValidator.readText(absolutePath); // Skip generated files if (this.isGeneratedFile(sourceText)) { @@ -126,9 +229,63 @@ export class GoScanner implements Scanner { const fileDocs = await this.extractFromFile(sourceText, file); documents.push(...fileDocs); } catch (error) { - // Log error but continue with other files - console.error(`Error scanning ${file}:`, error); + // Collect detailed error information + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + + errors.push({ + file, + absolutePath: path.join(repoRoot, file), + error: errorMessage, + phase: 'extractFromFile', + stack: errorStack, + }); + + // Log first 10 errors at INFO level, rest at DEBUG + if (errors.length <= 10) { + logger?.info( + { + file, + absolutePath: path.join(repoRoot, file), + error: errorMessage, + phase: 'extractFromFile', + errorNumber: errors.length, + }, + `[${errors.length}] Skipped Go file (extractFromFile): ${file}` + ); + } else { + logger?.debug( + { file, error: errorMessage, phase: 'extractFromFile' }, + `Skipped Go file (extractFromFile): ${file}` + ); + } + } + } + + // Log final summary + const successCount = documents.length; + const failureCount = errors.length; + + if (failureCount > 0) { + // Group errors by type for summary + const errorsByPhase = new Map(); + for (const err of errors) { + errorsByPhase.set(err.phase, (errorsByPhase.get(err.phase) || 0) + 1); } + + const errorSummary = Array.from(errorsByPhase.entries()) + .map(([phase, count]) => `${count} ${phase}`) + .join(', '); + + logger?.info( + { successCount, failureCount, total, errorSummary }, + `Go scan complete: ${successCount}/${total} files processed successfully. Skipped: ${errorSummary}` + ); + } else { + logger?.info( + { successCount, total }, + `Go scan complete: ${successCount}/${total} files processed successfully` + ); } return documents; diff --git a/packages/core/src/scanner/markdown.ts b/packages/core/src/scanner/markdown.ts index 3596c1c..0bf3897 100644 --- a/packages/core/src/scanner/markdown.ts +++ b/packages/core/src/scanner/markdown.ts @@ -1,5 +1,6 @@ import * as fs from 'node:fs/promises'; import * as path from 'node:path'; +import type { Logger } from '@lytics/kero'; import type { Code, Heading, Paragraph, Root } from 'mdast'; import remarkParse from 'remark-parse'; import { unified } from 'unified'; @@ -21,7 +22,7 @@ export class MarkdownScanner implements Scanner { return ext === '.md' || ext === '.mdx'; } - async scan(files: string[], repoRoot: string): Promise { + async scan(files: string[], repoRoot: string, _logger?: Logger): Promise { const documents: Document[] = []; for (const file of files) { diff --git a/packages/core/src/scanner/registry.ts b/packages/core/src/scanner/registry.ts index 47eb17b..1ac9ca8 100644 --- a/packages/core/src/scanner/registry.ts +++ b/packages/core/src/scanner/registry.ts @@ -132,7 +132,7 @@ export class ScannerRegistry { }); try { - const documents = await scanner.scan(scannerFiles, options.repoRoot); + const documents = await scanner.scan(scannerFiles, options.repoRoot, logger); allDocuments.push(...documents); totalFilesScanned += scannerFiles.length; diff --git a/packages/core/src/scanner/tree-sitter.ts b/packages/core/src/scanner/tree-sitter.ts index 9d74540..49f7dae 100644 --- a/packages/core/src/scanner/tree-sitter.ts +++ b/packages/core/src/scanner/tree-sitter.ts @@ -5,7 +5,10 @@ * Used by GoScanner and future language scanners (Python, Rust). */ +import * as fs from 'node:fs'; +import { createRequire } from 'node:module'; import * as path from 'node:path'; +import { findLanguageWasm, findWebTreeSitterWasm } from '../utils/wasm-resolver'; // web-tree-sitter types type ParserType = import('web-tree-sitter').Parser; @@ -22,14 +25,30 @@ let parserInitialized = false; /** * Supported languages for tree-sitter parsing + * + * Currently supported: + * - 'go': Go language parsing (bundled in production) + * + * To add new languages: + * 1. Add language to this type definition + * 2. Update SUPPORTED_LANGUAGES in packages/dev-agent/scripts/copy-wasm.js + * 3. Ensure tree-sitter-wasms contains the required WASM file + * 4. Add language-specific scanner in packages/core/src/scanner/ */ -export type TreeSitterLanguage = 'go' | 'python' | 'rust'; +export type TreeSitterLanguage = 'go'; /** * Cache of loaded language grammars */ const languageCache = new Map(); +/** + * Get the path to web-tree-sitter's WASM binding file + */ +function getTreeSitterWasmPath(): string { + return findWebTreeSitterWasm(__dirname); +} + /** * Initialize the tree-sitter parser (must be called before parsing) * This is idempotent - safe to call multiple times @@ -37,12 +56,66 @@ const languageCache = new Map(); export async function initTreeSitter(): Promise { if (parserInitialized && ParserClass && LanguageClass && QueryClass) return; - const TreeSitter = await import('web-tree-sitter'); + let TreeSitter: typeof import('web-tree-sitter'); + try { + // Strategy 1: Standard import (works in most dev environments) + TreeSitter = await import('web-tree-sitter'); + } catch (importError) { + // Strategy 2: Bundled Vendor Fallback + // When bundled with tsup, external modules might be missing or broken. + // We explicitly look for our vendored copy in dist/vendor/web-tree-sitter. + try { + const require = createRequire(__filename); + const vendorPath = path.join(__dirname, 'vendor', 'web-tree-sitter'); + + if (fs.existsSync(vendorPath)) { + // Try requiring the directory (uses package.json if present) + try { + TreeSitter = require(vendorPath); + } catch { + // Fallback to explicit file requirement + const cjsFile = path.join(vendorPath, 'tree-sitter.cjs'); + const jsFile = path.join(vendorPath, 'tree-sitter.js'); + if (fs.existsSync(cjsFile)) TreeSitter = require(cjsFile); + else if (fs.existsSync(jsFile)) TreeSitter = require(jsFile); + else throw new Error('No entry file found in vendor directory'); + } + } else { + // Strategy 3: Standard require resolution (last resort for dev/test) + const modulePath = require.resolve('web-tree-sitter'); + TreeSitter = require(modulePath); + } + } catch (fallbackError) { + console.error(`[scanner] Failed to load web-tree-sitter: ${importError} / ${fallbackError}`); + throw new Error(`Could not load web-tree-sitter: ${importError}`); + } + } + ParserClass = TreeSitter.Parser; LanguageClass = TreeSitter.Language; QueryClass = TreeSitter.Query; - await ParserClass.init(); + // Get the path to web-tree-sitter's WASM binding file + let wasmPath: string; + try { + wasmPath = getTreeSitterWasmPath(); + } catch (err) { + console.error(`[scanner] Failed to find web-tree-sitter WASM: ${err}`); + throw err; + } + + const absolutePath = path.resolve(wasmPath); + + // Try initializing with the absolute path directly first + try { + await ParserClass.init({ + locateFile: () => absolutePath, + }); + } catch (error) { + console.error(`[scanner] Parser.init({ locateFile }) failed: ${error}`); + throw error; + } + parserInitialized = true; } @@ -50,41 +123,97 @@ export async function initTreeSitter(): Promise { * Get the WASM file path for a language from tree-sitter-wasms package */ function getWasmPath(language: TreeSitterLanguage): string { - // tree-sitter-wasms package structure: node_modules/tree-sitter-wasms/out/tree-sitter-{lang}.wasm - const wasmFileName = `tree-sitter-${language}.wasm`; - - // Try to resolve from node_modules - try { - const packagePath = require.resolve('tree-sitter-wasms/package.json'); - const packageDir = path.dirname(packagePath); - return path.join(packageDir, 'out', wasmFileName); - } catch { - // Fallback: assume it's in a standard location - return path.join(process.cwd(), 'node_modules', 'tree-sitter-wasms', 'out', wasmFileName); - } + return findLanguageWasm(language, __dirname); } /** * Load a language grammar for tree-sitter */ export async function loadLanguage(language: TreeSitterLanguage): Promise { - // Return cached if available - const cached = languageCache.get(language); - if (cached) return cached; + // Declare variables outside try block so they're accessible in catch + let wasmPath: string | undefined; + let absolutePath: string | undefined; + let fileUrl: string | undefined; - // Ensure parser is initialized - await initTreeSitter(); + try { + // Return cached if available + const cached = languageCache.get(language); + if (cached) return cached; - if (!LanguageClass) { - throw new Error('Tree-sitter not initialized'); - } + // Ensure parser is initialized + await initTreeSitter(); - // Load the language WASM - const wasmPath = getWasmPath(language); - const lang = await LanguageClass.load(wasmPath); + if (!LanguageClass) { + throw new Error('Tree-sitter not initialized'); + } + + // Load the language WASM + try { + wasmPath = getWasmPath(language); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to locate tree-sitter WASM file for ${language}: ${errorMessage}`); + } + + // Validate path (should never be undefined if getWasmPath succeeded, but double-check) + if (!wasmPath || typeof wasmPath !== 'string') { + throw new Error(`getWasmPath returned invalid path for ${language}: ${String(wasmPath)}`); + } + + // Check if WASM file exists + if (!fs.existsSync(wasmPath)) { + throw new Error( + `Tree-sitter WASM file not found for ${language}: ${wasmPath}. ` + + `Make sure tree-sitter-wasms package is installed.` + ); + } + + // Convert to absolute path + // Note: In Node.js environment (which we are in), web-tree-sitter's Language.load expects a file path, NOT a URL. + // Passing a file:// URL causes fs.open to fail with ENOENT. + absolutePath = path.resolve(wasmPath); + + // Validate absolute path + if (!absolutePath || typeof absolutePath !== 'string') { + throw new Error( + `Invalid WASM path resolved for ${language}: ${String(absolutePath)} (original: ${wasmPath})` + ); + } + + // Validate LanguageClass and load method exist + if (!LanguageClass) { + throw new Error('LanguageClass is null - tree-sitter not initialized'); + } + if (typeof LanguageClass.load !== 'function') { + throw new Error(`LanguageClass.load is not a function: ${typeof LanguageClass.load}`); + } - languageCache.set(language, lang); - return lang; + const lang = await LanguageClass.load(absolutePath); + languageCache.set(language, lang); + return lang; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorCode = (error as NodeJS.ErrnoException)?.code; + const errorStack = error instanceof Error ? error.stack : undefined; + + // Include detailed context in error message + // Note: wasmPath, absolutePath, fileUrl might not be defined if error happened earlier + const context = [ + `Failed to load tree-sitter language ${language}`, + errorMessage, + errorCode ? `(code: ${errorCode})` : '', + typeof wasmPath !== 'undefined' ? `WASM path: ${wasmPath}` : 'WASM path: not resolved', + typeof absolutePath !== 'undefined' + ? `Absolute path: ${absolutePath}` + : 'Absolute path: not resolved', + typeof fileUrl !== 'undefined' ? `File URL: ${fileUrl}` : 'File URL: not resolved', + errorStack ? `Stack: ${errorStack}` : '', + ] + .filter(Boolean) + .join('\n'); + + throw new Error(context); + } } /** @@ -153,39 +282,57 @@ export async function parseCode( sourceText: string, language: TreeSitterLanguage ): Promise { - const parser = await createParser(language); - const tree = parser.parse(sourceText); - const lang = await loadLanguage(language); + try { + // Validate input + if (typeof sourceText !== 'string') { + throw new Error(`Invalid sourceText: expected string, got ${typeof sourceText}`); + } - if (!tree) { - throw new Error(`Failed to parse ${language} code`); - } + const parser = await createParser(language); + const tree = parser.parse(sourceText); + const lang = await loadLanguage(language); - if (!QueryClass) { - throw new Error('Tree-sitter not initialized'); - } + if (!tree) { + throw new Error(`Failed to parse ${language} code: parser returned null`); + } - // Cache the QueryClass reference for use in the closure - const QueryCls = QueryClass; - - return { - rootNode: tree.rootNode as unknown as TreeSitterNode, - sourceText, - query(queryString: string): QueryMatch[] { - // Use new Query(language, source) instead of deprecated lang.query() - const query = new QueryCls(lang, queryString); - const matches = query.matches(tree.rootNode); - - // Convert web-tree-sitter matches to our QueryMatch format - return matches.map((match) => ({ - pattern: match.pattern, - captures: match.captures.map((cap) => ({ - name: cap.name, - node: cap.node as unknown as TreeSitterNode, - })), - })); - }, - }; + if (!QueryClass) { + throw new Error('Tree-sitter not initialized: QueryClass is null'); + } + + // Cache the QueryClass reference for use in the closure + const QueryCls = QueryClass; + + return { + rootNode: tree.rootNode as unknown as TreeSitterNode, + sourceText, + query(queryString: string): QueryMatch[] { + try { + // Use new Query(language, source) instead of deprecated lang.query() + const query = new QueryCls(lang, queryString); + const matches = query.matches(tree.rootNode); + + // Convert web-tree-sitter matches to our QueryMatch format + return matches.map((match) => ({ + pattern: match.pattern, + captures: match.captures.map((cap) => ({ + name: cap.name, + node: cap.node as unknown as TreeSitterNode, + })), + })); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Query execution failed: ${errorMessage}`); + } + }, + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorCode = (error as NodeJS.ErrnoException)?.code; + throw new Error( + `Failed to parse ${language} code: ${errorMessage}${errorCode ? ` (code: ${errorCode})` : ''}` + ); + } } /** diff --git a/packages/core/src/scanner/types.ts b/packages/core/src/scanner/types.ts index 54dec34..1cb83e3 100644 --- a/packages/core/src/scanner/types.ts +++ b/packages/core/src/scanner/types.ts @@ -84,8 +84,11 @@ export interface Scanner { /** * Scan files and extract documents + * @param files - List of files to scan (relative paths) + * @param repoRoot - Repository root path + * @param logger - Optional logger for progress output */ - scan(files: string[], repoRoot: string): Promise; + scan(files: string[], repoRoot: string, logger?: Logger): Promise; /** * Check if this scanner can handle a file diff --git a/packages/core/src/scanner/typescript.ts b/packages/core/src/scanner/typescript.ts index 37fb4b3..3a768c2 100644 --- a/packages/core/src/scanner/typescript.ts +++ b/packages/core/src/scanner/typescript.ts @@ -1,4 +1,5 @@ import * as path from 'node:path'; +import type { Logger } from '@lytics/kero'; import { type ArrowFunction, type CallExpression, @@ -15,6 +16,7 @@ import { type VariableDeclaration, type VariableStatement, } from 'ts-morph'; +import { getCurrentSystemResources, getOptimalConcurrency } from '../utils/concurrency'; import type { CalleeInfo, Document, Scanner, ScannerCapabilities } from './types'; /** @@ -47,27 +49,226 @@ export class TypeScriptScanner implements Scanner { ); } - async scan(files: string[], repoRoot: string): Promise { - // Initialize project + /** + * Get optimal concurrency level for TypeScript processing + */ + private getOptimalConcurrency(context: string): number { + return getOptimalConcurrency({ + context, + systemResources: getCurrentSystemResources(), + environmentVariables: process.env, + }); + } + + async scan(files: string[], repoRoot: string, logger?: Logger): Promise { + // Initialize project with lenient type checking enabled + // - Allows cross-file symbol resolution for better callee extraction + // - Keeps strict checks disabled to avoid blocking on type errors this.project = new Project({ - tsConfigFilePath: path.join(repoRoot, 'tsconfig.json'), skipAddingFilesFromTsConfig: true, + skipFileDependencyResolution: false, // Enable dependency resolution for type checking + compilerOptions: { + allowJs: true, + checkJs: false, // Don't type-check JS files (too noisy) + noEmit: true, + skipLibCheck: true, // Skip checking .d.ts files for speed + noResolve: false, // Enable module resolution for type checking + // Lenient type checking - don't fail on errors + noImplicitAny: false, + strictNullChecks: false, + strict: false, + }, }); - // Add files to project - const absoluteFiles = files.map((f) => path.join(repoRoot, f)); - this.project.addSourceFilesAtPaths(absoluteFiles); - const documents: Document[] = []; - - // Extract documents from each file + const errors: Array<{ + file: string; + absolutePath: string; + error: string; + phase: string; + stack?: string; + }> = []; + const total = files.length; + let processedCount = 0; + let successCount = 0; + let failureCount = 0; + const startTime = Date.now(); + + // Process files in parallel batches for better performance + // Strategy: Add files to project sequentially (ts-morph state management), then extract in parallel + // Promise.all allows the event loop to interleave CPU-bound work, providing 2-3x speedup + const CONCURRENCY = this.getOptimalConcurrency('typescript'); // Configurable concurrency + + // Step 1: Add all files to project sequentially (required for ts-morph state management) + const sourceFiles = new Map(); for (const file of files) { const absolutePath = path.join(repoRoot, file); - const sourceFile = this.project.getSourceFile(absolutePath); + try { + const sourceFile = this.project.addSourceFileAtPath(absolutePath); + if (sourceFile) { + sourceFiles.set(file, sourceFile); + } + } catch (_error) { + // File failed to add - will be handled in extraction phase + } + } + + // Step 2: Process files in parallel batches for extraction + const fileEntries = Array.from(sourceFiles.entries()); + const batches: Array<[string, SourceFile][]> = []; + for (let i = 0; i < fileEntries.length; i += CONCURRENCY) { + batches.push(fileEntries.slice(i, i + CONCURRENCY)); + } + + // Helper to extract from a single file + const extractFile = async ( + file: string, + sourceFile: SourceFile + ): Promise<{ + documents: Document[]; + error?: { file: string; absolutePath: string; error: string; phase: string; stack?: string }; + }> => { + const absolutePath = path.join(repoRoot, file); - if (!sourceFile) continue; + try { + const fileDocs = this.extractFromSourceFile(sourceFile, file, repoRoot); + return { documents: fileDocs }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + const errorStack = error instanceof Error ? error.stack : undefined; + + return { + documents: [], + error: { + file, + absolutePath, + error: errorMessage, + phase: 'extractFromSourceFile', + stack: errorStack, + }, + }; + } + }; - documents.push(...this.extractFromSourceFile(sourceFile, file, repoRoot)); + // Process batches sequentially, files within batch in parallel + for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) { + const batch = batches[batchIndex]; + const results = await Promise.all( + batch.map(([file, sourceFile]) => extractFile(file, sourceFile)) + ); + + // Collect results + for (const result of results) { + processedCount++; + if (result.error) { + errors.push(result.error); + failureCount++; + + // Log first 10 errors at INFO level, rest at DEBUG + if (errors.length <= 10) { + logger?.info( + { + file: result.error.file, + absolutePath: result.error.absolutePath, + error: result.error.error, + phase: result.error.phase, + errorNumber: errors.length, + }, + `[${errors.length}] Skipped file (${result.error.phase}): ${result.error.file}` + ); + } else { + logger?.debug( + { file: result.error.file, error: result.error.error, phase: result.error.phase }, + `Skipped file (${result.error.phase})` + ); + } + } else { + documents.push(...result.documents); + successCount++; + } + } + + // Log progress after each batch (or every 50 files) + if (logger && (processedCount % 50 === 0 || batchIndex === batches.length - 1)) { + const elapsed = Date.now() - startTime; + const filesPerSecond = processedCount / (elapsed / 1000); + const remainingFiles = total - processedCount; + const etaSeconds = Math.ceil(remainingFiles / filesPerSecond); + const etaMinutes = Math.floor(etaSeconds / 60); + const etaSecondsRemainder = etaSeconds % 60; + + const etaText = + etaMinutes > 0 ? `${etaMinutes}m ${etaSecondsRemainder}s` : `${etaSecondsRemainder}s`; + + const percent = Math.round((processedCount / total) * 100); + logger.info( + { + filesProcessed: processedCount, + total, + percent, + documents: documents.length, + successCount, + failureCount, + batch: `${batchIndex + 1}/${batches.length}`, + concurrency: CONCURRENCY, + filesPerSecond: Math.round(filesPerSecond * 10) / 10, + eta: etaText, + }, + `typescript ${processedCount}/${total} (${percent}%) - ${documents.length} docs, ${Math.round(filesPerSecond)} files/sec, ETA: ${etaText} [batch ${batchIndex + 1}/${batches.length}]` + ); + } + } + + // Handle files that failed to add to project + const failedToAdd = files.length - sourceFiles.size; + if (failedToAdd > 0) { + failureCount += failedToAdd; + for (const file of files) { + if (!sourceFiles.has(file)) { + errors.push({ + file, + absolutePath: path.join(repoRoot, file), + error: 'Failed to add file to project', + phase: 'addSourceFileAtPath', + }); + } + } + } + + // Log summary with grouped error context + if (errors.length > 0) { + // Group errors by type for summary + const errorsByPhase = new Map(); + for (const err of errors) { + errorsByPhase.set(err.phase, (errorsByPhase.get(err.phase) || 0) + 1); + } + + const errorSummary = Array.from(errorsByPhase.entries()) + .map(([phase, count]) => `${count} ${phase}`) + .join(', '); + + logger?.info( + { successCount, failureCount, total, errorSummary, documentsExtracted: documents.length }, + `TypeScript scan complete: ${successCount}/${total} files processed successfully. Skipped: ${errorSummary}` + ); + + // Provide helpful suggestions for common errors + const addSourceFileErrors = errorsByPhase.get('addSourceFileAtPath'); + if (addSourceFileErrors && addSourceFileErrors > 10) { + logger?.info( + 'Many files failed to parse. Consider checking for syntax errors or incompatible TypeScript versions.' + ); + } + } else { + logger?.info( + { successCount, total, documentsExtracted: documents.length }, + `TypeScript scan complete: ${successCount}/${total} files processed successfully` + ); + } + + // Partial success is acceptable - only fail if zero documents extracted + if (errors.length > 0 && documents.length === 0) { + throw new Error(`TypeScript scan failed: ${errors[0].error} (in ${errors[0].file})`); } return documents; @@ -81,75 +282,118 @@ export class TypeScriptScanner implements Scanner { const documents: Document[] = []; // Extract file-level imports once (shared by all components in this file) - const imports = this.extractImports(sourceFile); + let imports: string[] = []; + try { + imports = this.extractImports(sourceFile); + } catch { + // Continue with empty imports if extraction fails + } + + // Helper to safely iterate and extract - ts-morph can throw on complex/malformed code + const safeIterate = (getter: () => T[], processor: (item: T) => void): void => { + try { + const items = getter(); + for (const item of items) { + try { + processor(item); + } catch { + // Skip this item if processing fails + } + } + } catch { + // Skip this category if iteration fails + } + }; // Extract functions - for (const fn of sourceFile.getFunctions()) { - const doc = this.extractFunction(fn, relativeFile, imports, sourceFile); - if (doc) documents.push(doc); - } + safeIterate( + () => sourceFile.getFunctions(), + (fn) => { + const doc = this.extractFunction(fn, relativeFile, imports, sourceFile); + if (doc) documents.push(doc); + } + ); - // Extract classes - for (const cls of sourceFile.getClasses()) { - const doc = this.extractClass(cls, relativeFile, imports); - if (doc) documents.push(doc); - - // Extract methods - for (const method of cls.getMethods()) { - const methodDoc = this.extractMethod( - method, - cls.getName() || 'Anonymous', - relativeFile, - imports, - sourceFile + // Extract classes and their methods + safeIterate( + () => sourceFile.getClasses(), + (cls) => { + const doc = this.extractClass(cls, relativeFile, imports); + if (doc) documents.push(doc); + + // Extract methods + safeIterate( + () => cls.getMethods(), + (method) => { + const methodDoc = this.extractMethod( + method, + cls.getName() || 'Anonymous', + relativeFile, + imports, + sourceFile + ); + if (methodDoc) documents.push(methodDoc); + } ); - if (methodDoc) documents.push(methodDoc); } - } + ); // Extract interfaces - for (const iface of sourceFile.getInterfaces()) { - const doc = this.extractInterface(iface, relativeFile, imports); - if (doc) documents.push(doc); - } + safeIterate( + () => sourceFile.getInterfaces(), + (iface) => { + const doc = this.extractInterface(iface, relativeFile, imports); + if (doc) documents.push(doc); + } + ); // Extract type aliases - for (const typeAlias of sourceFile.getTypeAliases()) { - const doc = this.extractTypeAlias(typeAlias, relativeFile, imports); - if (doc) documents.push(doc); - } + safeIterate( + () => sourceFile.getTypeAliases(), + (typeAlias) => { + const doc = this.extractTypeAlias(typeAlias, relativeFile, imports); + if (doc) documents.push(doc); + } + ); // Extract variables with arrow functions, function expressions, or exported constants - for (const varStmt of sourceFile.getVariableStatements()) { - for (const decl of varStmt.getDeclarations()) { - const initializer = decl.getInitializer(); - if (!initializer) continue; - - const kind = initializer.getKind(); - - // Arrow functions and function expressions (any export status) - if (kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression) { - const doc = this.extractVariableWithFunction( - decl, - varStmt, - relativeFile, - imports, - sourceFile - ); - if (doc) documents.push(doc); - } - // Exported constants with object/array/call expression initializers - else if ( - varStmt.isExported() && - (kind === SyntaxKind.ObjectLiteralExpression || - kind === SyntaxKind.ArrayLiteralExpression || - kind === SyntaxKind.CallExpression) - ) { - const doc = this.extractExportedConstant(decl, varStmt, relativeFile, imports); - if (doc) documents.push(doc); + safeIterate( + () => sourceFile.getVariableStatements(), + (varStmt) => { + for (const decl of varStmt.getDeclarations()) { + try { + const initializer = decl.getInitializer(); + if (!initializer) continue; + + const kind = initializer.getKind(); + + // Arrow functions and function expressions (any export status) + if (kind === SyntaxKind.ArrowFunction || kind === SyntaxKind.FunctionExpression) { + const doc = this.extractVariableWithFunction( + decl, + varStmt, + relativeFile, + imports, + sourceFile + ); + if (doc) documents.push(doc); + } + // Exported constants with object/array/call expression initializers + else if ( + varStmt.isExported() && + (kind === SyntaxKind.ObjectLiteralExpression || + kind === SyntaxKind.ArrayLiteralExpression || + kind === SyntaxKind.CallExpression) + ) { + const doc = this.extractExportedConstant(decl, varStmt, relativeFile, imports); + if (doc) documents.push(doc); + } + } catch { + // Skip this declaration if processing fails + } } } - } + ); return documents; } @@ -605,36 +849,49 @@ export class TypeScriptScanner implements Scanner { const callees: CalleeInfo[] = []; const seenCalls = new Set(); // Deduplicate by name+line - // Get all call expressions within this node - const callExpressions = node.getDescendantsOfKind(SyntaxKind.CallExpression); - - for (const callExpr of callExpressions) { - const calleeInfo = this.extractCalleeFromExpression(callExpr, sourceFile); - if (calleeInfo) { - const key = `${calleeInfo.name}:${calleeInfo.line}`; - if (!seenCalls.has(key)) { - seenCalls.add(key); - callees.push(calleeInfo); + try { + // Get all call expressions within this node + const callExpressions = node.getDescendantsOfKind(SyntaxKind.CallExpression); + + for (const callExpr of callExpressions) { + try { + const calleeInfo = this.extractCalleeFromExpression(callExpr, sourceFile); + if (calleeInfo) { + const key = `${calleeInfo.name}:${calleeInfo.line}`; + if (!seenCalls.has(key)) { + seenCalls.add(key); + callees.push(calleeInfo); + } + } + } catch { + // Skip this call expression if it fails } } - } - // Also handle new expressions (constructor calls) - const newExpressions = node.getDescendantsOfKind(SyntaxKind.NewExpression); - for (const newExpr of newExpressions) { - const expression = newExpr.getExpression(); - const name = expression.getText(); - const line = newExpr.getStartLineNumber(); - const key = `new ${name}:${line}`; - - if (!seenCalls.has(key)) { - seenCalls.add(key); - callees.push({ - name: `new ${name}`, - line, - file: undefined, // Could resolve via type checker if needed - }); + // Also handle new expressions (constructor calls) + const newExpressions = node.getDescendantsOfKind(SyntaxKind.NewExpression); + for (const newExpr of newExpressions) { + try { + const expression = newExpr.getExpression(); + const name = expression.getText(); + const line = newExpr.getStartLineNumber(); + const key = `new ${name}:${line}`; + + if (!seenCalls.has(key)) { + seenCalls.add(key); + callees.push({ + name: `new ${name}`, + line, + file: undefined, // Could resolve via type checker if needed + }); + } + } catch { + // Skip this new expression if it fails + } } + } catch { + // If callee extraction fails entirely, return empty array + // This is better than crashing the entire scan } return callees; @@ -667,15 +924,21 @@ export class TypeScriptScanner implements Scanner { let file: string | undefined; try { // Get the symbol and find its declaration + // Note: getSymbol() can throw internally when accessing escapedName on undefined nodes const symbol = expression.getSymbol(); if (symbol) { const declarations = symbol.getDeclarations(); - if (declarations.length > 0) { - const declSourceFile = declarations[0].getSourceFile(); - const filePath = declSourceFile.getFilePath(); - // Only include if it's within the project (not node_modules) - if (!filePath.includes('node_modules')) { - file = filePath; + if (declarations && declarations.length > 0) { + const firstDecl = declarations[0]; + if (firstDecl) { + const declSourceFile = firstDecl.getSourceFile(); + if (declSourceFile) { + const filePath = declSourceFile.getFilePath(); + // Only include if it's within the project (not node_modules) + if (filePath && !filePath.includes('node_modules')) { + file = filePath; + } + } } } } diff --git a/packages/core/src/utils/__tests__/concurrency.test.ts b/packages/core/src/utils/__tests__/concurrency.test.ts new file mode 100644 index 0000000..b1d6828 --- /dev/null +++ b/packages/core/src/utils/__tests__/concurrency.test.ts @@ -0,0 +1,110 @@ +/** + * Tests for concurrency calculation utilities + */ + +import { describe, expect, it } from 'vitest'; +import { + type ConcurrencyConfig, + calculateOptimalConcurrency, + getOptimalConcurrency, + parseConcurrencyFromEnv, + type SystemResources, +} from '../concurrency'; + +describe('calculateOptimalConcurrency', () => { + it('should return low concurrency for low memory systems', () => { + const lowMemorySystem: SystemResources = { cpuCount: 4, memoryGB: 2 }; + + expect(calculateOptimalConcurrency('typescript', lowMemorySystem)).toBe(5); + expect(calculateOptimalConcurrency('indexer', lowMemorySystem)).toBe(2); + }); + + it('should return medium concurrency for standard systems', () => { + const standardSystem: SystemResources = { cpuCount: 4, memoryGB: 6 }; + + expect(calculateOptimalConcurrency('typescript', standardSystem)).toBe(15); + expect(calculateOptimalConcurrency('indexer', standardSystem)).toBe(3); + }); + + it('should return high concurrency for high-end systems', () => { + const highEndSystem: SystemResources = { cpuCount: 12, memoryGB: 16 }; + + expect(calculateOptimalConcurrency('typescript', highEndSystem)).toBe(30); + expect(calculateOptimalConcurrency('indexer', highEndSystem)).toBe(5); + }); + + it('should handle edge case values', () => { + const extremeSystem: SystemResources = { cpuCount: 1, memoryGB: 0.5 }; + + expect(calculateOptimalConcurrency('typescript', extremeSystem)).toBe(5); + expect(calculateOptimalConcurrency('indexer', extremeSystem)).toBe(2); + }); +}); + +describe('parseConcurrencyFromEnv', () => { + it('should parse valid environment variables', () => { + const env = { + DEV_AGENT_TYPESCRIPT_CONCURRENCY: '20', + DEV_AGENT_CONCURRENCY: '10', + }; + + expect(parseConcurrencyFromEnv('typescript', env)).toBe(20); + expect(parseConcurrencyFromEnv('go', env)).toBe(10); // falls back to general + }); + + it('should return null for invalid values', () => { + const invalidEnv = { + DEV_AGENT_TYPESCRIPT_CONCURRENCY: 'invalid', + DEV_AGENT_INDEXER_CONCURRENCY: '0', + DEV_AGENT_GO_CONCURRENCY: '101', + }; + + expect(parseConcurrencyFromEnv('typescript', invalidEnv)).toBeNull(); + expect(parseConcurrencyFromEnv('indexer', invalidEnv)).toBeNull(); + expect(parseConcurrencyFromEnv('go', invalidEnv)).toBeNull(); + }); + + it('should respect max value cap', () => { + const env = { DEV_AGENT_CONCURRENCY: '100' }; + + expect(parseConcurrencyFromEnv('typescript', env, 30)).toBe(30); + }); + + it('should return null when no relevant env vars exist', () => { + const emptyEnv = {}; + + expect(parseConcurrencyFromEnv('typescript', emptyEnv)).toBeNull(); + }); +}); + +describe('getOptimalConcurrency', () => { + it('should prefer environment variable over system detection', () => { + const config: ConcurrencyConfig = { + context: 'typescript', + systemResources: { cpuCount: 16, memoryGB: 32 }, // Would give 30 + environmentVariables: { DEV_AGENT_TYPESCRIPT_CONCURRENCY: '5' }, + }; + + expect(getOptimalConcurrency(config)).toBe(5); + }); + + it('should fall back to system detection when no env var', () => { + const config: ConcurrencyConfig = { + context: 'typescript', + systemResources: { cpuCount: 8, memoryGB: 16 }, + environmentVariables: {}, + }; + + expect(getOptimalConcurrency(config)).toBe(30); + }); + + it('should use general env var as fallback', () => { + const config: ConcurrencyConfig = { + context: 'go', + systemResources: { cpuCount: 4, memoryGB: 8 }, + environmentVariables: { DEV_AGENT_CONCURRENCY: '12' }, + }; + + expect(getOptimalConcurrency(config)).toBe(12); + }); +}); diff --git a/packages/core/src/utils/__tests__/file-validator.test.ts b/packages/core/src/utils/__tests__/file-validator.test.ts new file mode 100644 index 0000000..37a6b13 --- /dev/null +++ b/packages/core/src/utils/__tests__/file-validator.test.ts @@ -0,0 +1,156 @@ +/** + * Tests for file validation utilities + */ + +import { describe, expect, it } from 'vitest'; +import { type FileSystemValidator, validateFile, validateFiles } from '../file-validator'; + +// Mock filesystem validator for testing +class MockFileSystemValidator implements FileSystemValidator { + constructor( + private existingFiles: Set = new Set(), + private fileContents: Map = new Map(), + private directories: Set = new Set() + ) {} + + exists(path: string): boolean { + return this.existingFiles.has(path) || this.directories.has(path); + } + + isFile(path: string): boolean { + return this.existingFiles.has(path); + } + + readText(path: string): string { + const content = this.fileContents.get(path); + if (content === undefined) { + throw new Error(`Cannot read file: ${path}`); + } + return content; + } + + addFile(path: string, content: string): this { + this.existingFiles.add(path); + this.fileContents.set(path, content); + return this; + } + + addDirectory(path: string): this { + this.directories.add(path); + return this; + } + + addExistingFile(path: string): this { + this.existingFiles.add(path); + return this; + } +} + +describe('validateFile', () => { + it('should validate existing file with content', () => { + const mockFs = new MockFileSystemValidator().addFile( + '/path/to/file.go', + 'package main\n\nfunc main() {}' + ); + + const result = validateFile('file.go', '/path/to/file.go', mockFs); + + expect(result.isValid).toBe(true); + expect(result.error).toBeUndefined(); + }); + + it('should fail for non-existent file', () => { + const mockFs = new MockFileSystemValidator(); + + const result = validateFile('missing.go', '/path/to/missing.go', mockFs); + + expect(result.isValid).toBe(false); + expect(result.error).toBe('File does not exist'); + expect(result.phase).toBe('fileValidation'); + }); + + it('should fail for directory instead of file', () => { + const mockFs = new MockFileSystemValidator().addDirectory('/path/to/dir'); + + const result = validateFile('dir', '/path/to/dir', mockFs); + + expect(result.isValid).toBe(false); + expect(result.error).toBe('Not a file (directory or special file)'); + expect(result.phase).toBe('fileValidation'); + }); + + it('should fail for empty file', () => { + const mockFs = new MockFileSystemValidator().addFile('/path/to/empty.go', ''); + + const result = validateFile('empty.go', '/path/to/empty.go', mockFs); + + expect(result.isValid).toBe(false); + expect(result.error).toBe('Empty file'); + expect(result.phase).toBe('fileValidation'); + }); + + it('should fail for whitespace-only file', () => { + const mockFs = new MockFileSystemValidator().addFile('/path/to/whitespace.go', ' \n\t \n '); + + const result = validateFile('whitespace.go', '/path/to/whitespace.go', mockFs); + + expect(result.isValid).toBe(false); + expect(result.error).toBe('Empty file'); + expect(result.phase).toBe('fileValidation'); + }); + + it('should handle read errors', () => { + const mockFs = new MockFileSystemValidator(); + // Add file to exists check but not to contents map + mockFs.addExistingFile('/path/to/unreadable.go'); + + const result = validateFile('unreadable.go', '/path/to/unreadable.go', mockFs); + + expect(result.isValid).toBe(false); + expect(result.error).toContain('Failed to read file'); + expect(result.phase).toBe('content'); + }); +}); + +describe('validateFiles', () => { + it('should validate multiple files', () => { + const mockFs = new MockFileSystemValidator() + .addFile('/path/good.go', 'package main') + .addFile('/path/empty.go', '') + .addDirectory('/path/dir'); + + const files = [ + { filePath: 'good.go', absolutePath: '/path/good.go' }, + { filePath: 'empty.go', absolutePath: '/path/empty.go' }, + { filePath: 'dir', absolutePath: '/path/dir' }, + { filePath: 'missing.go', absolutePath: '/path/missing.go' }, + ]; + + const results = validateFiles(files, mockFs); + + expect(results).toHaveLength(4); + + expect(results[0].filePath).toBe('good.go'); + expect(results[0].result.isValid).toBe(true); + + expect(results[1].filePath).toBe('empty.go'); + expect(results[1].result.isValid).toBe(false); + expect(results[1].result.error).toBe('Empty file'); + + expect(results[2].filePath).toBe('dir'); + expect(results[2].result.isValid).toBe(false); + expect(results[2].result.error).toBe('Not a file (directory or special file)'); + + expect(results[3].filePath).toBe('missing.go'); + expect(results[3].result.isValid).toBe(false); + expect(results[3].result.error).toBe('File does not exist'); + }); + + it('should handle empty file list', () => { + const mockFs = new MockFileSystemValidator(); + + const results = validateFiles([], mockFs); + + expect(results).toHaveLength(0); + }); +}); diff --git a/packages/core/src/utils/__tests__/wasm-resolver.test.ts b/packages/core/src/utils/__tests__/wasm-resolver.test.ts new file mode 100644 index 0000000..3ea5919 --- /dev/null +++ b/packages/core/src/utils/__tests__/wasm-resolver.test.ts @@ -0,0 +1,163 @@ +/** + * Tests for WASM resolution utilities + */ + +import { describe, expect, it } from 'vitest'; +import { + type FileSystemAdapter, + findLanguageWasm, + findWasmFile, + findWebTreeSitterWasm, + type WasmResolutionConfig, +} from '../wasm-resolver'; + +// Mock filesystem adapter for testing +class MockFileSystemAdapter implements FileSystemAdapter { + constructor( + private existingFiles: Set = new Set(), + private resolvedPaths: Map = new Map() + ) {} + + existsSync(path: string): boolean { + return this.existingFiles.has(path); + } + + requireResolve(id: string): string { + const resolved = this.resolvedPaths.get(id); + if (!resolved) { + throw new Error(`Cannot resolve module '${id}'`); + } + return resolved; + } + + addFile(path: string): this { + this.existingFiles.add(path); + return this; + } + + addResolvedPath(id: string, path: string): this { + this.resolvedPaths.set(id, path); + return this; + } +} + +describe('findWasmFile', () => { + it('should find file in bundled location', () => { + const mockFs = new MockFileSystemAdapter().addFile('/dist/wasm/test.wasm'); + + const config: WasmResolutionConfig = { + filename: 'test.wasm', + currentDirectory: '/dist', + fileSystem: mockFs, + }; + + expect(findWasmFile(config)).toBe('/dist/wasm/test.wasm'); + }); + + it('should find file in vendor directory', () => { + const mockFs = new MockFileSystemAdapter().addFile('/dist/vendor/my-package/test.wasm'); + + const config: WasmResolutionConfig = { + filename: 'test.wasm', + packageName: 'my-package', + currentDirectory: '/dist', + fileSystem: mockFs, + }; + + expect(findWasmFile(config)).toBe('/dist/vendor/my-package/test.wasm'); + }); + + it('should find file in node_modules', () => { + const mockFs = new MockFileSystemAdapter() + .addFile('/project/node_modules/my-package/test.wasm') + .addResolvedPath('my-package', '/project/node_modules/my-package/index.js'); + + const config: WasmResolutionConfig = { + filename: 'test.wasm', + packageName: 'my-package', + currentDirectory: '/dist', + fileSystem: mockFs, + }; + + expect(findWasmFile(config)).toBe('/project/node_modules/my-package/test.wasm'); + }); + + it('should try multiple node_modules locations', () => { + const mockFs = new MockFileSystemAdapter() + .addFile('/project/node_modules/my-package/lib/test.wasm') + .addResolvedPath('my-package', '/project/node_modules/my-package/index.js'); + + const config: WasmResolutionConfig = { + filename: 'test.wasm', + packageName: 'my-package', + currentDirectory: '/dist', + fileSystem: mockFs, + }; + + expect(findWasmFile(config)).toBe('/project/node_modules/my-package/lib/test.wasm'); + }); + + it('should throw detailed error when file not found', () => { + const mockFs = new MockFileSystemAdapter(); + + const config: WasmResolutionConfig = { + filename: 'missing.wasm', + packageName: 'my-package', + currentDirectory: '/dist', + fileSystem: mockFs, + }; + + expect(() => findWasmFile(config)).toThrow( + /Failed to locate WASM file.*missing\.wasm.*from package my-package/ + ); + }); +}); + +describe('findLanguageWasm', () => { + it('should find bundled Go WASM file', () => { + const mockFs = new MockFileSystemAdapter().addFile('/dist/wasm/tree-sitter-go.wasm'); + + expect(findLanguageWasm('go', '/dist', mockFs)).toBe('/dist/wasm/tree-sitter-go.wasm'); + }); + + it('should fall back to tree-sitter-wasms package', () => { + const mockFs = new MockFileSystemAdapter() + .addFile('/node_modules/tree-sitter-wasms/out/tree-sitter-python.wasm') + .addResolvedPath( + 'tree-sitter-wasms/package.json', + '/node_modules/tree-sitter-wasms/package.json' + ); + + expect(findLanguageWasm('python', '/dist', mockFs)).toBe( + '/node_modules/tree-sitter-wasms/out/tree-sitter-python.wasm' + ); + }); + + it('should throw error when language WASM not found', () => { + const mockFs = new MockFileSystemAdapter(); + + expect(() => findLanguageWasm('nonexistent', '/dist', mockFs)).toThrow( + /Failed to locate tree-sitter WASM file for nonexistent/ + ); + }); +}); + +describe('findWebTreeSitterWasm', () => { + it('should find web-tree-sitter WASM file', () => { + const mockFs = new MockFileSystemAdapter().addFile( + '/dist/vendor/web-tree-sitter/tree-sitter.wasm' + ); + + expect(findWebTreeSitterWasm('/dist', mockFs)).toBe( + '/dist/vendor/web-tree-sitter/tree-sitter.wasm' + ); + }); + + it('should throw error when not found', () => { + const mockFs = new MockFileSystemAdapter(); + + expect(() => findWebTreeSitterWasm('/dist', mockFs)).toThrow( + /Failed to locate WASM file.*tree-sitter\.wasm.*from package web-tree-sitter/ + ); + }); +}); diff --git a/packages/core/src/utils/concurrency.ts b/packages/core/src/utils/concurrency.ts new file mode 100644 index 0000000..43ffeb0 --- /dev/null +++ b/packages/core/src/utils/concurrency.ts @@ -0,0 +1,88 @@ +/** + * Concurrency calculation utilities + * Separated for testability and reuse across scanners and indexers + */ + +export interface SystemResources { + cpuCount: number; + memoryGB: number; +} + +export interface ConcurrencyConfig { + context: string; + systemResources: SystemResources; + environmentVariables: Record; +} + +/** + * Calculate optimal concurrency based on system resources + */ +export function calculateOptimalConcurrency( + context: string, + systemResources: SystemResources +): number { + const { cpuCount, memoryGB } = systemResources; + + // Context-specific logic + if (context === 'indexer') { + // Indexer is more memory-intensive + if (memoryGB < 4) return 2; + if (memoryGB < 8) return 3; + if (cpuCount >= 8) return 5; + return 4; + } else { + // Scanner contexts (typescript, go, etc.) + if (memoryGB < 4) return 5; + if (memoryGB < 8) return 15; + if (cpuCount >= 8) return 30; + return 20; + } +} + +/** + * Parse environment variable for concurrency setting + */ +export function parseConcurrencyFromEnv( + context: string, + environmentVariables: Record, + maxValue: number = 50 +): number | null { + const envVar = `DEV_AGENT_${context.toUpperCase()}_CONCURRENCY`; + const envValue = environmentVariables[envVar] || environmentVariables.DEV_AGENT_CONCURRENCY; + + if (!envValue) return null; + + const parsed = parseInt(envValue, 10); + if (Number.isNaN(parsed) || parsed <= 0 || parsed > 100) { + return null; + } + + return Math.min(parsed, maxValue); +} + +/** + * Get optimal concurrency with environment variable override + */ +export function getOptimalConcurrency(config: ConcurrencyConfig): number { + const { context, systemResources, environmentVariables } = config; + + // Try environment variable first + const envConcurrency = parseConcurrencyFromEnv(context, environmentVariables); + if (envConcurrency !== null) { + return envConcurrency; + } + + // Fall back to system-based calculation + return calculateOptimalConcurrency(context, systemResources); +} + +/** + * Get current system resources (wrapper for testing) + */ +export function getCurrentSystemResources(): SystemResources { + const os = require('node:os'); + return { + cpuCount: os.cpus().length, + memoryGB: os.totalmem() / (1024 * 1024 * 1024), + }; +} diff --git a/packages/core/src/utils/file-validator.ts b/packages/core/src/utils/file-validator.ts new file mode 100644 index 0000000..a88674e --- /dev/null +++ b/packages/core/src/utils/file-validator.ts @@ -0,0 +1,103 @@ +/** + * File validation utilities + * Separated for testability and consistency across scanners + */ + +export interface FileValidationResult { + isValid: boolean; + error?: string; + phase?: 'fileValidation' | 'content'; +} + +export interface FileSystemValidator { + exists(path: string): boolean; + isFile(path: string): boolean; + readText(path: string): string; +} + +/** + * Default filesystem validator using Node.js APIs + */ +export class NodeFileSystemValidator implements FileSystemValidator { + private fs = require('node:fs'); + + exists(path: string): boolean { + return this.fs.existsSync(path); + } + + isFile(path: string): boolean { + try { + const stats = this.fs.statSync(path); + return stats.isFile(); + } catch { + return false; + } + } + + readText(path: string): string { + return this.fs.readFileSync(path, 'utf-8'); + } +} + +/** + * Validate a single file for processing + */ +export function validateFile( + _filePath: string, + absolutePath: string, + validator: FileSystemValidator +): FileValidationResult { + // Check if file exists + if (!validator.exists(absolutePath)) { + return { + isValid: false, + error: 'File does not exist', + phase: 'fileValidation', + }; + } + + // Check if it's a file (not a directory) + if (!validator.isFile(absolutePath)) { + return { + isValid: false, + error: 'Not a file (directory or special file)', + phase: 'fileValidation', + }; + } + + // Check file content + let content: string; + try { + content = validator.readText(absolutePath); + } catch (error) { + return { + isValid: false, + error: `Failed to read file: ${error}`, + phase: 'content', + }; + } + + // Check if file is empty + if (!content || content.trim().length === 0) { + return { + isValid: false, + error: 'Empty file', + phase: 'fileValidation', + }; + } + + return { isValid: true }; +} + +/** + * Batch validate multiple files + */ +export function validateFiles( + files: Array<{ filePath: string; absolutePath: string }>, + validator: FileSystemValidator +): Array<{ filePath: string; result: FileValidationResult }> { + return files.map(({ filePath, absolutePath }) => ({ + filePath, + result: validateFile(filePath, absolutePath, validator), + })); +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 437fef2..0a7212b 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -1,11 +1,8 @@ /** - * Core utility functions + * Utility modules for dev-agent core */ -export { - RetryError, - type RetryOptions, - RetryPredicates, - retryWithDelays, - withRetry, -} from './retry'; +export * from './concurrency'; +export * from './file-validator'; +export * from './retry'; +export * from './wasm-resolver'; diff --git a/packages/core/src/utils/wasm-resolver.ts b/packages/core/src/utils/wasm-resolver.ts new file mode 100644 index 0000000..83e46ba --- /dev/null +++ b/packages/core/src/utils/wasm-resolver.ts @@ -0,0 +1,136 @@ +/** + * WASM file resolution utilities + * Separated for testability and easier maintenance + */ + +export interface FileSystemAdapter { + existsSync(path: string): boolean; + requireResolve(id: string): string; +} + +export interface WasmResolutionConfig { + filename: string; + packageName?: string; + currentDirectory: string; + fileSystem: FileSystemAdapter; +} + +/** + * Default filesystem adapter using Node.js fs and require + */ +export class NodeFileSystemAdapter implements FileSystemAdapter { + existsSync(path: string): boolean { + return require('node:fs').existsSync(path); + } + + requireResolve(id: string): string { + return require.resolve(id); + } +} + +/** + * Find WASM files with multiple fallback strategies + */ +export function findWasmFile(config: WasmResolutionConfig): string { + const { filename, packageName, currentDirectory, fileSystem } = config; + const triedPaths: string[] = []; + + // Strategy 1: Check bundled location (dist/wasm/) + const bundledPath = require('node:path').join(currentDirectory, 'wasm', filename); + triedPaths.push(`bundled: ${bundledPath}`); + if (fileSystem.existsSync(bundledPath)) { + return bundledPath; + } + + // Strategy 2: Check vendor directory (dist/vendor/) + if (packageName) { + const vendorPath = require('node:path').join(currentDirectory, 'vendor', packageName, filename); + triedPaths.push(`vendor: ${vendorPath}`); + if (fileSystem.existsSync(vendorPath)) { + return vendorPath; + } + } + + // Strategy 3: Development environment - resolve via node_modules + if (packageName) { + try { + const mainPath = fileSystem.requireResolve(packageName); + const packageDir = require('node:path').dirname(mainPath); + + // Check common locations within package + const locations = [ + require('node:path').join(packageDir, filename), + require('node:path').join(packageDir, 'lib', filename), + require('node:path').join(require('node:path').dirname(packageDir), filename), // parent directory + ]; + + for (const location of locations) { + if (fileSystem.existsSync(location)) { + return location; + } + } + + triedPaths.push(`node_modules: checked ${locations.join(', ')}`); + } catch (e) { + triedPaths.push(`node_modules: resolution failed - ${e}`); + } + } + + throw new Error( + `Failed to locate WASM file (${filename})${packageName ? ` from package ${packageName}` : ''}. ` + + `Tried:\n ${triedPaths.join('\n ')}` + ); +} + +/** + * Tree-sitter language-specific WASM resolution + */ +export function findLanguageWasm( + language: string, + currentDirectory: string, + fileSystem: FileSystemAdapter = new NodeFileSystemAdapter() +): string { + const wasmFileName = `tree-sitter-${language}.wasm`; + + try { + // First try bundled location using our utility + return findWasmFile({ + filename: wasmFileName, + currentDirectory, + fileSystem, + }); + } catch { + // Fall back to tree-sitter-wasms package + try { + const packagePath = fileSystem.requireResolve('tree-sitter-wasms/package.json'); + const packageDir = require('node:path').dirname(packagePath); + const wasmPath = require('node:path').join(packageDir, 'out', wasmFileName); + + if (fileSystem.existsSync(wasmPath)) { + return wasmPath; + } + + throw new Error(`WASM file not found in tree-sitter-wasms/out: ${wasmPath}`); + } catch (packageError) { + throw new Error( + `Failed to locate tree-sitter WASM file for ${language} (${wasmFileName}). ` + + `Make sure tree-sitter-wasms package is installed. Error: ${packageError}` + ); + } + } +} + +/** + * Web tree-sitter core WASM resolution + */ +export function findWebTreeSitterWasm( + currentDirectory: string, + fileSystem: FileSystemAdapter = new NodeFileSystemAdapter() +): string { + return findWasmFile({ + filename: 'tree-sitter.wasm', + packageName: 'web-tree-sitter', + currentDirectory, + fileSystem, + }); +} diff --git a/packages/dev-agent/package.json b/packages/dev-agent/package.json index eab907e..0c53f3b 100644 --- a/packages/dev-agent/package.json +++ b/packages/dev-agent/package.json @@ -39,15 +39,17 @@ "LICENSE" ], "scripts": { - "build": "tsup", + "build": "tsup && node scripts/copy-wasm.js", "prepublishOnly": "pnpm run build" }, "dependencies": { "@lancedb/lancedb": "^0.22.3", "@xenova/transformers": "^2.17.2", - "ts-morph": "^27.0.2" + "ts-morph": "^27.0.2", + "web-tree-sitter": "^0.25.10" }, "devDependencies": { + "tree-sitter-wasms": "^0.1.13", "@lytics/dev-agent-cli": "workspace:*", "@lytics/dev-agent-mcp": "workspace:*", "@types/node": "^22.0.0", diff --git a/packages/dev-agent/scripts/copy-wasm.js b/packages/dev-agent/scripts/copy-wasm.js new file mode 100644 index 0000000..780a055 --- /dev/null +++ b/packages/dev-agent/scripts/copy-wasm.js @@ -0,0 +1,223 @@ +#!/usr/bin/env node +/** + * Copy tree-sitter WASM files to dist/wasm/ for bundled CLI + * This ensures WASM files are available when the CLI is installed as an npm package + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +let wasmSourceDir; +try { + // Try to find tree-sitter-wasms package location + // We need to look for a known file inside it. "out/tree-sitter-go.wasm" is a good candidate if we knew it existed. + // Or "package.json" but require.resolve might point to main. + // tree-sitter-wasms package.json has "main": "index.js" maybe? + + // Let's try to find package.json first + try { + const pkgPath = require.resolve('tree-sitter-wasms/package.json'); + wasmSourceDir = path.join(path.dirname(pkgPath), 'out'); + } catch (_e) { + // If that fails (e.g. exports restricted), try resolving the module root + const _mainPath = require.resolve('tree-sitter-wasms'); + // Usually main is in root or dist. Let's assume root for now or walk up. + // But tree-sitter-wasms typically just has "out" folder. + + // Fallback to the hardcoded path if resolve fails, but improve search + wasmSourceDir = path.join(__dirname, '..', 'node_modules', 'tree-sitter-wasms', 'out'); + } +} catch (_e) { + // Fallback + wasmSourceDir = path.join(__dirname, '..', 'node_modules', 'tree-sitter-wasms', 'out'); +} + +// Robust search for tree-sitter-wasms if previous attempts failed or dir doesn't exist +if (!fs.existsSync(wasmSourceDir)) { + // Try walking up directories to find node_modules/tree-sitter-wasms + let current = path.dirname(__dirname); // packages/dev-agent + for (let i = 0; i < 5; i++) { + const testPath = path.join(current, 'node_modules', 'tree-sitter-wasms', 'out'); + if (fs.existsSync(testPath)) { + wasmSourceDir = testPath; + break; + } + + // Also check .pnpm structure + const pnpmDir = path.join(current, 'node_modules', '.pnpm'); + if (fs.existsSync(pnpmDir)) { + try { + const entries = fs.readdirSync(pnpmDir); + const entry = entries.find((e) => e.startsWith('tree-sitter-wasms@')); + if (entry) { + const testPath2 = path.join(pnpmDir, entry, 'node_modules', 'tree-sitter-wasms', 'out'); + if (fs.existsSync(testPath2)) { + wasmSourceDir = testPath2; + break; + } + } + } catch (_err) {} + } + + const parent = path.dirname(current); + if (parent === current) break; + current = parent; + } +} + +console.log(`Using WASM source dir: ${wasmSourceDir}`); + +const distDir = path.join(__dirname, '..', 'dist'); +const wasmDestDir = path.join(distDir, 'wasm'); +const vendorDestDir = path.join(distDir, 'vendor', 'web-tree-sitter'); + +// Create destination directories +if (!fs.existsSync(wasmDestDir)) { + fs.mkdirSync(wasmDestDir, { recursive: true }); +} +if (!fs.existsSync(vendorDestDir)) { + fs.mkdirSync(vendorDestDir, { recursive: true }); +} + +// Check if source directory exists +if (!fs.existsSync(wasmSourceDir)) { + console.error(`Error: tree-sitter-wasms not found at ${wasmSourceDir}`); + console.error('WASM files cannot be bundled. Go scanner will not work.'); + console.error('Please run: npm install tree-sitter-wasms'); + process.exit(1); +} + +// Supported languages to whitelist (Keep this small to reduce bundle size!) +// +// To add a new language: +// 1. Add language to this array (e.g., 'python', 'rust') +// 2. Update TreeSitterLanguage type in packages/core/src/scanner/tree-sitter.ts +// 3. Ensure tree-sitter-wasms package contains tree-sitter-{lang}.wasm +// 4. Create a language-specific scanner in packages/core/src/scanner/{lang}.ts +// 5. Update scanner registration in packages/core/src/scanner/index.ts +const SUPPORTED_LANGUAGES = ['go']; +const SUPPORTED_FILES = new Set([ + ...SUPPORTED_LANGUAGES.map((lang) => `tree-sitter-${lang}.wasm`), + 'tree-sitter.wasm', // Runtime if present +]); + +// Copy whitelisted WASM files +const wasmFiles = fs.readdirSync(wasmSourceDir).filter((file) => { + return file.endsWith('.wasm') && SUPPORTED_FILES.has(file); +}); + +if (wasmFiles.length === 0) { + console.error('Error: No supported WASM files found in tree-sitter-wasms/out'); + console.error(`Expected files: ${Array.from(SUPPORTED_FILES).join(', ')}`); + console.error('Go scanner will not work without these WASM files.'); + process.exit(1); +} + +let copied = 0; +for (const file of wasmFiles) { + const sourcePath = path.join(wasmSourceDir, file); + const destPath = path.join(wasmDestDir, file); + fs.copyFileSync(sourcePath, destPath); + copied++; +} + +// Copy web-tree-sitter files to dist/vendor +let webTreeSitterPath = null; +try { + const pkgPath = require.resolve('web-tree-sitter/package.json'); + webTreeSitterPath = path.dirname(pkgPath); + console.log(`Found web-tree-sitter via require.resolve: ${webTreeSitterPath}`); +} catch (e) { + console.warn(`require.resolve failed: ${e.message}`); + // Fallback search + let current = path.dirname(__dirname); // packages/dev-agent + for (let i = 0; i < 10; i++) { + // Check standard node_modules + const p = path.join(current, 'node_modules', 'web-tree-sitter'); + if (fs.existsSync(path.join(p, 'package.json'))) { + webTreeSitterPath = p; + break; + } + + // Check .pnpm + const pnpmDir = path.join(current, 'node_modules', '.pnpm'); + if (fs.existsSync(pnpmDir)) { + try { + const entries = fs.readdirSync(pnpmDir); + const entry = entries.find((e) => e.startsWith('web-tree-sitter@')); + if (entry) { + const p2 = path.join(pnpmDir, entry, 'node_modules', 'web-tree-sitter'); + if (fs.existsSync(path.join(p2, 'package.json'))) { + webTreeSitterPath = p2; + break; + } + } + } catch (_err) {} + } + + const parent = path.dirname(current); + if (parent === current) break; + current = parent; + } +} + +if (webTreeSitterPath) { + // Copy package.json + fs.copyFileSync( + path.join(webTreeSitterPath, 'package.json'), + path.join(vendorDestDir, 'package.json') + ); + + // Copy tree-sitter.js / tree-sitter.cjs (main entry) + const pkg = require(path.join(webTreeSitterPath, 'package.json')); + + // Check exports for require entry + let mainFile = 'tree-sitter.js'; + if (pkg.exports?.['.']?.require) { + mainFile = pkg.exports['.'].require; + } else if (pkg.main) { + mainFile = pkg.main; + } else if (fs.existsSync(path.join(webTreeSitterPath, 'tree-sitter.cjs'))) { + mainFile = 'tree-sitter.cjs'; + } + + // Handle relative paths like "./tree-sitter.cjs" + if (mainFile.startsWith('./')) { + mainFile = mainFile.substring(2); + } + + const srcMain = path.join(webTreeSitterPath, mainFile); + if (fs.existsSync(srcMain)) { + fs.copyFileSync(srcMain, path.join(vendorDestDir, path.basename(mainFile))); + console.log(`✓ Copied main file: ${mainFile}`); + } else { + console.warn(`Warning: Main file not found: ${srcMain}`); + } + + // Also copy tree-sitter.wasm if it exists in root + const wasmFile = path.join(webTreeSitterPath, 'tree-sitter.wasm'); + if (fs.existsSync(wasmFile)) { + fs.copyFileSync(wasmFile, path.join(vendorDestDir, 'tree-sitter.wasm')); + // Also copy to dist/wasm/ for getTreeSitterWasmPath strategy 0 + fs.copyFileSync(wasmFile, path.join(wasmDestDir, 'tree-sitter.wasm')); + } + + // Copy lib directory if it exists (contains tree-sitter.wasm sometimes) + const libDir = path.join(webTreeSitterPath, 'lib'); + if (fs.existsSync(libDir)) { + const destLibDir = path.join(vendorDestDir, 'lib'); + if (!fs.existsSync(destLibDir)) fs.mkdirSync(destLibDir); + const files = fs.readdirSync(libDir); + for (const file of files) { + fs.copyFileSync(path.join(libDir, file), path.join(destLibDir, file)); + // NOTE: Do NOT copy lib/tree-sitter.wasm to dist/wasm/ if we already copied from root. + // Root tree-sitter.cjs expects root tree-sitter.wasm. + } + } + + console.log(`✓ Copied web-tree-sitter to dist/vendor/`); +} else { + console.warn('Warning: Could not find web-tree-sitter to copy to vendor dir'); +} + +console.log(`✓ Copied ${copied} WASM file(s) to dist/wasm/`); diff --git a/packages/dev-agent/tsconfig.json b/packages/dev-agent/tsconfig.json index c9f0f70..c48e93c 100644 --- a/packages/dev-agent/tsconfig.json +++ b/packages/dev-agent/tsconfig.json @@ -5,4 +5,3 @@ }, "include": ["tsup.config.ts"] } - diff --git a/packages/dev-agent/tsup.config.ts b/packages/dev-agent/tsup.config.ts index f2d11cd..c4b2dc2 100644 --- a/packages/dev-agent/tsup.config.ts +++ b/packages/dev-agent/tsup.config.ts @@ -1,4 +1,4 @@ -import { readFileSync } from 'fs'; +import { readFileSync } from 'node:fs'; import { defineConfig } from 'tsup'; // Read version from package.json at build time @@ -18,6 +18,7 @@ const external = [ // These have native bindings or complex loading 'ts-morph', 'typescript', + 'web-tree-sitter', ]; export default defineConfig([ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9fa567..4ad1762 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -109,9 +109,6 @@ importers: remark-stringify: specifier: ^11.0.0 version: 11.0.0 - tree-sitter-wasms: - specifier: ^0.1.13 - version: 0.1.13 ts-morph: specifier: ^27.0.2 version: 27.0.2 @@ -128,6 +125,9 @@ importers: '@types/node': specifier: ^24.10.1 version: 24.10.1 + tree-sitter-wasms: + specifier: ^0.1.13 + version: 0.1.13 typescript: specifier: ^5.3.3 version: 5.9.3 @@ -143,6 +143,9 @@ importers: ts-morph: specifier: ^27.0.2 version: 27.0.2 + web-tree-sitter: + specifier: ^0.25.10 + version: 0.25.10 devDependencies: '@lytics/dev-agent-cli': specifier: workspace:* @@ -153,6 +156,9 @@ importers: '@types/node': specifier: ^22.0.0 version: 22.19.1 + tree-sitter-wasms: + specifier: ^0.1.13 + version: 0.1.13 tsup: specifier: ^8.3.0 version: 8.5.1(typescript@5.9.3) @@ -4573,7 +4579,7 @@ packages: /tree-sitter-wasms@0.1.13: resolution: {integrity: sha512-wT+cR6DwaIz80/vho3AvSF0N4txuNx/5bcRKoXouOfClpxh/qqrF4URNLQXbbt8MaAxeksZcZd1j8gcGjc+QxQ==} - dev: false + dev: true /trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} diff --git a/renovate.json b/renovate.json deleted file mode 100644 index c8226ca..0000000 --- a/renovate.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended", ":semanticCommits", "helpers:pinGitHubActionDigests"], - "baseBranches": ["main"], - "rangeStrategy": "bump", - "dependencyDashboard": true, - "schedule": ["after 10pm and before 5am every weekday", "every weekend"], - "prConcurrentLimit": 5, - "prHourlyLimit": 2, - - "packageRules": [ - { - "matchDepTypes": ["devDependencies"], - "matchUpdateTypes": ["minor", "patch"], - "automerge": true - }, - { - "matchPackagePatterns": ["*"], - "matchUpdateTypes": ["minor", "patch"], - "groupName": "all non-major dependencies", - "groupSlug": "all-minor-patch" - }, - { - "matchPackagePatterns": ["^@biomejs/", "^typescript"], - "groupName": "typescript and formatting dependencies", - "groupSlug": "typescript-formatting" - }, - { - "matchPackagePatterns": ["^vitest", "^@vitest/"], - "groupName": "testing dependencies", - "groupSlug": "testing-deps" - }, - { - "matchPackagePatterns": ["^@changesets/", "turbo"], - "groupName": "monorepo tooling", - "groupSlug": "monorepo-tools" - }, - { - "matchDepTypes": ["peerDependencies"], - "rangeStrategy": "widen" - } - ], - - "lockFileMaintenance": { - "enabled": true, - "schedule": ["before 5am on monday"] - }, - - "ignoreDeps": ["pnpm"], - - "commitMessagePrefix": "chore(deps):", - "commitMessageAction": "update", - "commitMessageTopic": "{{depName}}", - "commitBodyTable": true, - - "pnpmShrinkwrap": true, - "npmrc": "@lytics:registry=https://registry.npmjs.org/" -} diff --git a/renovate.json5 b/renovate.json5 deleted file mode 100644 index 925b3aa..0000000 --- a/renovate.json5 +++ /dev/null @@ -1,103 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - // Use recommended presets - "extends": [ - "config:recommended", - ":semanticCommits", - "helpers:pinGitHubActionDigests" - ], - - // Base configuration - "baseBranches": ["main"], - "rangeStrategy": "bump", - - // Dashboard configuration with advanced options - "dependencyDashboard": true, - "dependencyDashboardTitle": "📦 Dependency Dashboard", - "dependencyDashboardLabels": ["dependencies", "renovate"], - "dependencyDashboardAutoclose": true, - - // Schedule configuration - "timezone": "America/New_York", - "schedule": ["after 10pm and before 5am every weekday", "every weekend"], - - // Rate limiting to prevent CI overload - "prConcurrentLimit": 5, - "prHourlyLimit": 2, - - // Package rules - "packageRules": [ - // Automerge for dev dependencies - { - "matchDepTypes": ["devDependencies"], - "matchUpdateTypes": ["minor", "patch"], - "automerge": true, - "platformAutomerge": true - }, - - // Group minor and patch updates to reduce PR noise - { - "matchPackagePatterns": ["*"], - "matchUpdateTypes": ["minor", "patch"], - "groupName": "all non-major dependencies", - "groupSlug": "all-minor-patch" - }, - - // Group TypeScript related tools - { - "matchPackagePatterns": ["^@biomejs/", "^typescript"], - "groupName": "typescript and formatting dependencies", - "groupSlug": "typescript-formatting" - }, - - // Group testing tools - { - "matchPackagePatterns": ["^vitest", "^@vitest/"], - "groupName": "testing dependencies", - "groupSlug": "testing-deps" - }, - - // Group monorepo tools - { - "matchPackagePatterns": ["^@changesets/", "turbo"], - "groupName": "monorepo tooling", - "groupSlug": "monorepo-tools" - }, - - // Handle peer dependencies correctly - { - "matchDepTypes": ["peerDependencies"], - "rangeStrategy": "widen" - }, - - // Handle Node.js updates specially - { - "matchPackageNames": ["node"], - "separateMinorPatch": true - } - ], - - // Lock file maintenance - "lockFileMaintenance": { - "enabled": true, - "schedule": ["before 5am on monday"], - "commitMessageAction": "update", - "commitMessageExtra": "lock file", - "commitMessageTopic": "dependencies" - }, - - // Ignored dependencies - "ignoreDeps": [ - "pnpm" - ], - - // Commit message configuration - "commitMessagePrefix": "chore(deps):", - "commitMessageAction": "update", - "commitMessageTopic": "{{depName}}", - "commitBodyTable": true, - - // PNPM specific configuration - "pnpmShrinkwrap": true, - "npmrc": "@monorepo:registry=https://registry.npmjs.org/" -} \ No newline at end of file diff --git a/website/app/globals.css b/website/app/globals.css index 174996b..3b02375 100644 --- a/website/app/globals.css +++ b/website/app/globals.css @@ -2,4 +2,3 @@ --nextra-primary-hue: 212deg; --nextra-primary-saturation: 100%; } - diff --git a/website/package.json b/website/package.json index b1d19e9..00344f4 100644 --- a/website/package.json +++ b/website/package.json @@ -22,4 +22,3 @@ "typescript": "^5.7.2" } } - diff --git a/website/tsconfig.json b/website/tsconfig.json index 25ed6d5..eea7ea5 100644 --- a/website/tsconfig.json +++ b/website/tsconfig.json @@ -21,4 +21,3 @@ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] } -