neuronlayer 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of neuronlayer might be problematic. Click here for more details.

Files changed (80) hide show
  1. package/README.md +95 -269
  2. package/dist/index.js +478 -126
  3. package/dist/index.js.map +7 -0
  4. package/package.json +7 -2
  5. package/esbuild.config.js +0 -26
  6. package/src/cli/commands.ts +0 -518
  7. package/src/core/adr-exporter.ts +0 -253
  8. package/src/core/architecture/architecture-enforcement.ts +0 -228
  9. package/src/core/architecture/duplicate-detector.ts +0 -288
  10. package/src/core/architecture/index.ts +0 -6
  11. package/src/core/architecture/pattern-learner.ts +0 -306
  12. package/src/core/architecture/pattern-library.ts +0 -403
  13. package/src/core/architecture/pattern-validator.ts +0 -324
  14. package/src/core/change-intelligence/bug-correlator.ts +0 -544
  15. package/src/core/change-intelligence/change-intelligence.ts +0 -264
  16. package/src/core/change-intelligence/change-tracker.ts +0 -334
  17. package/src/core/change-intelligence/fix-suggester.ts +0 -340
  18. package/src/core/change-intelligence/index.ts +0 -5
  19. package/src/core/code-verifier.ts +0 -843
  20. package/src/core/confidence/confidence-scorer.ts +0 -251
  21. package/src/core/confidence/conflict-checker.ts +0 -289
  22. package/src/core/confidence/index.ts +0 -5
  23. package/src/core/confidence/source-tracker.ts +0 -263
  24. package/src/core/confidence/warning-detector.ts +0 -241
  25. package/src/core/context-rot/compaction.ts +0 -284
  26. package/src/core/context-rot/context-health.ts +0 -243
  27. package/src/core/context-rot/context-rot-prevention.ts +0 -213
  28. package/src/core/context-rot/critical-context.ts +0 -221
  29. package/src/core/context-rot/drift-detector.ts +0 -255
  30. package/src/core/context-rot/index.ts +0 -7
  31. package/src/core/context.ts +0 -263
  32. package/src/core/decision-extractor.ts +0 -339
  33. package/src/core/decisions.ts +0 -69
  34. package/src/core/deja-vu.ts +0 -421
  35. package/src/core/engine.ts +0 -1646
  36. package/src/core/feature-context.ts +0 -726
  37. package/src/core/ghost-mode.ts +0 -465
  38. package/src/core/learning.ts +0 -519
  39. package/src/core/living-docs/activity-tracker.ts +0 -296
  40. package/src/core/living-docs/architecture-generator.ts +0 -428
  41. package/src/core/living-docs/changelog-generator.ts +0 -348
  42. package/src/core/living-docs/component-generator.ts +0 -230
  43. package/src/core/living-docs/doc-engine.ts +0 -134
  44. package/src/core/living-docs/doc-validator.ts +0 -282
  45. package/src/core/living-docs/index.ts +0 -8
  46. package/src/core/project-manager.ts +0 -301
  47. package/src/core/refresh/activity-gate.ts +0 -256
  48. package/src/core/refresh/git-staleness-checker.ts +0 -108
  49. package/src/core/refresh/index.ts +0 -27
  50. package/src/core/summarizer.ts +0 -290
  51. package/src/core/test-awareness/change-validator.ts +0 -499
  52. package/src/core/test-awareness/index.ts +0 -5
  53. package/src/index.ts +0 -49
  54. package/src/indexing/ast.ts +0 -868
  55. package/src/indexing/embeddings.ts +0 -85
  56. package/src/indexing/indexer.ts +0 -270
  57. package/src/indexing/watcher.ts +0 -78
  58. package/src/server/gateways/aggregator.ts +0 -374
  59. package/src/server/gateways/index.ts +0 -473
  60. package/src/server/gateways/memory-ghost.ts +0 -343
  61. package/src/server/gateways/memory-query.ts +0 -452
  62. package/src/server/gateways/memory-record.ts +0 -346
  63. package/src/server/gateways/memory-review.ts +0 -410
  64. package/src/server/gateways/memory-status.ts +0 -517
  65. package/src/server/gateways/memory-verify.ts +0 -392
  66. package/src/server/gateways/router.ts +0 -434
  67. package/src/server/gateways/types.ts +0 -610
  68. package/src/server/mcp.ts +0 -154
  69. package/src/server/resources.ts +0 -85
  70. package/src/server/tools.ts +0 -2460
  71. package/src/storage/database.ts +0 -271
  72. package/src/storage/tier1.ts +0 -135
  73. package/src/storage/tier2.ts +0 -972
  74. package/src/storage/tier3.ts +0 -123
  75. package/src/types/documentation.ts +0 -619
  76. package/src/types/index.ts +0 -222
  77. package/src/utils/config.ts +0 -193
  78. package/src/utils/files.ts +0 -117
  79. package/src/utils/time.ts +0 -37
  80. package/src/utils/tokens.ts +0 -52
@@ -1,282 +0,0 @@
1
- import { existsSync } from 'fs';
2
- import { join } from 'path';
3
- import type Database from 'better-sqlite3';
4
- import type { Tier2Storage } from '../../storage/tier2.js';
5
- import type { CodeSymbol } from '../../types/index.js';
6
- import type {
7
- ValidationResult,
8
- OutdatedDoc,
9
- UndocumentedItem,
10
- DocSuggestion
11
- } from '../../types/documentation.js';
12
-
13
- export class DocValidator {
14
- private projectPath: string;
15
- private tier2: Tier2Storage;
16
- private db: Database.Database;
17
-
18
- constructor(projectPath: string, tier2: Tier2Storage, db: Database.Database) {
19
- this.projectPath = projectPath;
20
- this.tier2 = tier2;
21
- this.db = db;
22
- }
23
-
24
- async validate(): Promise<ValidationResult> {
25
- const outdated = await this.findOutdated();
26
- const undocumented = await this.findUndocumented();
27
- const suggestions = this.generateSuggestions(outdated, undocumented);
28
- const score = this.calculateScore(outdated, undocumented);
29
-
30
- return {
31
- isValid: score >= 70,
32
- outdatedDocs: outdated,
33
- undocumentedCode: undocumented,
34
- suggestions,
35
- score
36
- };
37
- }
38
-
39
- async findOutdated(): Promise<OutdatedDoc[]> {
40
- const outdated: OutdatedDoc[] = [];
41
-
42
- try {
43
- // Get all documentation records
44
- const stmt = this.db.prepare(`
45
- SELECT d.file_id, d.generated_at, f.path, f.last_modified
46
- FROM documentation d
47
- JOIN files f ON d.file_id = f.id
48
- `);
49
-
50
- const rows = stmt.all() as Array<{
51
- file_id: number;
52
- generated_at: number;
53
- path: string;
54
- last_modified: number;
55
- }>;
56
-
57
- for (const row of rows) {
58
- // Check if code changed after docs were generated
59
- if (row.last_modified > row.generated_at) {
60
- const daysSinceUpdate = Math.floor(
61
- (Date.now() / 1000 - row.generated_at) / (24 * 60 * 60)
62
- );
63
-
64
- outdated.push({
65
- file: row.path,
66
- reason: 'Code modified after documentation was generated',
67
- lastDocUpdate: new Date(row.generated_at * 1000),
68
- lastCodeChange: new Date(row.last_modified * 1000),
69
- severity: daysSinceUpdate > 30 ? 'high' : daysSinceUpdate > 7 ? 'medium' : 'low'
70
- });
71
- }
72
- }
73
- } catch {
74
- // Documentation table might not exist yet
75
- }
76
-
77
- return outdated;
78
- }
79
-
80
- async findUndocumented(options?: {
81
- importance?: 'low' | 'medium' | 'high' | 'all';
82
- type?: 'file' | 'function' | 'class' | 'interface' | 'all';
83
- }): Promise<UndocumentedItem[]> {
84
- const items: UndocumentedItem[] = [];
85
- const files = this.tier2.getAllFiles();
86
-
87
- for (const file of files) {
88
- const symbols = this.tier2.getSymbolsByFile(file.id);
89
-
90
- // Check for undocumented exported symbols
91
- const exportedSymbols = symbols.filter(s => s.exported);
92
-
93
- for (const symbol of exportedSymbols) {
94
- if (!symbol.docstring || symbol.docstring.trim().length === 0) {
95
- const importance = this.calculateImportance(file.path, symbol);
96
-
97
- // Apply filters
98
- if (options?.importance && options.importance !== 'all' && importance !== options.importance) {
99
- continue;
100
- }
101
-
102
- const symbolType = this.mapSymbolKindToType(symbol.kind);
103
- if (options?.type && options.type !== 'all' && symbolType !== options.type) {
104
- continue;
105
- }
106
-
107
- items.push({
108
- file: file.path,
109
- symbol: symbol.name,
110
- type: symbolType,
111
- importance
112
- });
113
- }
114
- }
115
-
116
- // Check for files without any documentation
117
- if (exportedSymbols.length === 0 && symbols.length > 0) {
118
- // File has symbols but none exported - might be internal
119
- continue;
120
- }
121
-
122
- // Check if the file has any JSDoc/docstring at file level
123
- const hasFileDoc = symbols.some(s => s.docstring && s.lineStart <= 5);
124
- if (!hasFileDoc && exportedSymbols.length > 0) {
125
- const importance = this.calculateFileImportance(file.path);
126
-
127
- if (options?.importance && options.importance !== 'all' && importance !== options.importance) {
128
- continue;
129
- }
130
- if (options?.type && options.type !== 'all' && options.type !== 'file') {
131
- continue;
132
- }
133
-
134
- // Only add if the file isn't already represented by undocumented symbols
135
- const hasSymbolEntry = items.some(i => i.file === file.path);
136
- if (!hasSymbolEntry) {
137
- items.push({
138
- file: file.path,
139
- type: 'file',
140
- importance
141
- });
142
- }
143
- }
144
- }
145
-
146
- // Sort by importance
147
- const importanceOrder = { high: 0, medium: 1, low: 2 };
148
- items.sort((a, b) => importanceOrder[a.importance] - importanceOrder[b.importance]);
149
-
150
- return items;
151
- }
152
-
153
- private calculateImportance(filePath: string, symbol: CodeSymbol): 'low' | 'medium' | 'high' {
154
- // Check how many files depend on this file
155
- const dependents = this.tier2.getFileDependents(filePath);
156
-
157
- // High importance: many dependents, or it's a class/interface
158
- if (dependents.length >= 3 || symbol.kind === 'class' || symbol.kind === 'interface') {
159
- return 'high';
160
- }
161
-
162
- // Medium importance: some dependents, or it's a function
163
- if (dependents.length >= 1 || symbol.kind === 'function') {
164
- return 'medium';
165
- }
166
-
167
- return 'low';
168
- }
169
-
170
- private calculateFileImportance(filePath: string): 'low' | 'medium' | 'high' {
171
- const dependents = this.tier2.getFileDependents(filePath);
172
-
173
- // Check if it's an index/entry file
174
- if (filePath.includes('index.') || filePath.includes('/src/')) {
175
- return 'high';
176
- }
177
-
178
- if (dependents.length >= 5) return 'high';
179
- if (dependents.length >= 2) return 'medium';
180
- return 'low';
181
- }
182
-
183
- private mapSymbolKindToType(kind: string): 'file' | 'function' | 'class' | 'interface' {
184
- switch (kind) {
185
- case 'class':
186
- return 'class';
187
- case 'interface':
188
- case 'type':
189
- return 'interface';
190
- case 'function':
191
- case 'method':
192
- return 'function';
193
- default:
194
- return 'function';
195
- }
196
- }
197
-
198
- private generateSuggestions(
199
- outdated: OutdatedDoc[],
200
- undocumented: UndocumentedItem[]
201
- ): DocSuggestion[] {
202
- const suggestions: DocSuggestion[] = [];
203
-
204
- // Suggestions for outdated docs
205
- for (const doc of outdated) {
206
- suggestions.push({
207
- file: doc.file,
208
- suggestion: `Update documentation - code changed ${this.formatTimeDiff(doc.lastCodeChange, doc.lastDocUpdate)} after docs`,
209
- priority: doc.severity
210
- });
211
- }
212
-
213
- // Suggestions for undocumented code
214
- const highPriorityUndoc = undocumented.filter(u => u.importance === 'high');
215
- for (const item of highPriorityUndoc) {
216
- suggestions.push({
217
- file: item.file,
218
- suggestion: item.symbol
219
- ? `Add documentation for exported ${item.type} '${item.symbol}'`
220
- : `Add file-level documentation`,
221
- priority: 'high'
222
- });
223
- }
224
-
225
- // Group medium priority suggestions
226
- const mediumPriorityCount = undocumented.filter(u => u.importance === 'medium').length;
227
- if (mediumPriorityCount > 0) {
228
- const files = [...new Set(undocumented.filter(u => u.importance === 'medium').map(u => u.file))];
229
- if (files.length <= 3) {
230
- for (const file of files) {
231
- suggestions.push({
232
- file,
233
- suggestion: 'Add documentation for exported symbols',
234
- priority: 'medium'
235
- });
236
- }
237
- } else {
238
- suggestions.push({
239
- file: files[0]!,
240
- suggestion: `${mediumPriorityCount} symbols across ${files.length} files need documentation`,
241
- priority: 'medium'
242
- });
243
- }
244
- }
245
-
246
- return suggestions;
247
- }
248
-
249
- private formatTimeDiff(later: Date, earlier: Date): string {
250
- const diffMs = later.getTime() - earlier.getTime();
251
- const diffDays = Math.floor(diffMs / (24 * 60 * 60 * 1000));
252
-
253
- if (diffDays === 0) return 'today';
254
- if (diffDays === 1) return '1 day';
255
- if (diffDays < 7) return `${diffDays} days`;
256
- if (diffDays < 30) return `${Math.floor(diffDays / 7)} weeks`;
257
- return `${Math.floor(diffDays / 30)} months`;
258
- }
259
-
260
- private calculateScore(outdated: OutdatedDoc[], undocumented: UndocumentedItem[]): number {
261
- const totalFiles = this.tier2.getFileCount();
262
- if (totalFiles === 0) return 100;
263
-
264
- // Calculate based on:
265
- // - % of files with outdated docs (30% weight)
266
- // - % of high-importance items undocumented (50% weight)
267
- // - % of medium-importance items undocumented (20% weight)
268
-
269
- const outdatedPenalty = (outdated.length / totalFiles) * 30;
270
-
271
- const highUndoc = undocumented.filter(u => u.importance === 'high').length;
272
- const mediumUndoc = undocumented.filter(u => u.importance === 'medium').length;
273
-
274
- // Assume each file has ~3 documentable items on average
275
- const estimatedTotalItems = totalFiles * 3;
276
- const highPenalty = estimatedTotalItems > 0 ? (highUndoc / estimatedTotalItems) * 50 : 0;
277
- const mediumPenalty = estimatedTotalItems > 0 ? (mediumUndoc / estimatedTotalItems) * 20 : 0;
278
-
279
- const totalPenalty = Math.min(100, outdatedPenalty + highPenalty + mediumPenalty);
280
- return Math.round(100 - totalPenalty);
281
- }
282
- }
@@ -1,8 +0,0 @@
1
- // Living Documentation Module - Barrel Export
2
-
3
- export { LivingDocumentationEngine } from './doc-engine.js';
4
- export { ArchitectureGenerator } from './architecture-generator.js';
5
- export { ComponentGenerator } from './component-generator.js';
6
- export { ChangelogGenerator } from './changelog-generator.js';
7
- export { DocValidator } from './doc-validator.js';
8
- export { ActivityTracker } from './activity-tracker.js';
@@ -1,301 +0,0 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
2
- import { join, basename, resolve } from 'path';
3
- import { createHash } from 'crypto';
4
- import { homedir } from 'os';
5
- import Database from 'better-sqlite3';
6
- import type { MemoryLayerConfig } from '../types/index.js';
7
-
8
- export interface ProjectInfo {
9
- id: string;
10
- name: string;
11
- path: string;
12
- dataDir: string;
13
- lastAccessed: number;
14
- totalFiles: number;
15
- totalDecisions: number;
16
- languages: string[];
17
- }
18
-
19
- export interface ProjectRegistry {
20
- version: number;
21
- activeProject: string | null;
22
- projects: Record<string, ProjectInfo>;
23
- }
24
-
25
- export class ProjectManager {
26
- private registryPath: string;
27
- private registry: ProjectRegistry;
28
- private baseDataDir: string;
29
-
30
- constructor() {
31
- this.baseDataDir = join(homedir(), '.memorylayer');
32
- this.registryPath = join(this.baseDataDir, 'registry.json');
33
-
34
- // Ensure base directory exists
35
- if (!existsSync(this.baseDataDir)) {
36
- mkdirSync(this.baseDataDir, { recursive: true });
37
- }
38
-
39
- this.registry = this.loadRegistry();
40
- }
41
-
42
- private loadRegistry(): ProjectRegistry {
43
- try {
44
- if (existsSync(this.registryPath)) {
45
- const data = JSON.parse(readFileSync(this.registryPath, 'utf-8'));
46
- return data;
47
- }
48
- } catch (error) {
49
- console.error('Error loading registry:', error);
50
- }
51
-
52
- return {
53
- version: 1,
54
- activeProject: null,
55
- projects: {}
56
- };
57
- }
58
-
59
- private saveRegistry(): void {
60
- try {
61
- writeFileSync(this.registryPath, JSON.stringify(this.registry, null, 2));
62
- } catch (error) {
63
- console.error('Error saving registry:', error);
64
- }
65
- }
66
-
67
- private generateProjectId(projectPath: string): string {
68
- const normalizedPath = resolve(projectPath);
69
- return createHash('md5').update(normalizedPath).digest('hex').slice(0, 12);
70
- }
71
-
72
- private getProjectDataDir(projectPath: string): string {
73
- const projectId = this.generateProjectId(projectPath);
74
- const projectName = basename(projectPath);
75
- return join(this.baseDataDir, 'projects', `${projectName}-${projectId}`);
76
- }
77
-
78
- // Register a new project or update existing
79
- registerProject(projectPath: string): ProjectInfo {
80
- const normalizedPath = resolve(projectPath);
81
- const projectId = this.generateProjectId(normalizedPath);
82
- const dataDir = this.getProjectDataDir(normalizedPath);
83
-
84
- // Check if project directory exists
85
- if (!existsSync(normalizedPath)) {
86
- throw new Error(`Project path does not exist: ${normalizedPath}`);
87
- }
88
-
89
- const existingProject = this.registry.projects[projectId];
90
-
91
- const projectInfo: ProjectInfo = {
92
- id: projectId,
93
- name: basename(normalizedPath),
94
- path: normalizedPath,
95
- dataDir,
96
- lastAccessed: Date.now(),
97
- totalFiles: existingProject?.totalFiles || 0,
98
- totalDecisions: existingProject?.totalDecisions || 0,
99
- languages: existingProject?.languages || []
100
- };
101
-
102
- this.registry.projects[projectId] = projectInfo;
103
- this.saveRegistry();
104
-
105
- return projectInfo;
106
- }
107
-
108
- // Remove a project from registry
109
- removeProject(projectId: string): boolean {
110
- if (!this.registry.projects[projectId]) {
111
- return false;
112
- }
113
-
114
- delete this.registry.projects[projectId];
115
-
116
- if (this.registry.activeProject === projectId) {
117
- this.registry.activeProject = null;
118
- }
119
-
120
- this.saveRegistry();
121
- return true;
122
- }
123
-
124
- // Get all registered projects
125
- listProjects(): ProjectInfo[] {
126
- return Object.values(this.registry.projects)
127
- .sort((a, b) => b.lastAccessed - a.lastAccessed);
128
- }
129
-
130
- // Get a specific project
131
- getProject(projectId: string): ProjectInfo | null {
132
- return this.registry.projects[projectId] || null;
133
- }
134
-
135
- // Get project by path
136
- getProjectByPath(projectPath: string): ProjectInfo | null {
137
- const projectId = this.generateProjectId(projectPath);
138
- return this.registry.projects[projectId] || null;
139
- }
140
-
141
- // Set active project
142
- setActiveProject(projectId: string): boolean {
143
- if (!this.registry.projects[projectId]) {
144
- return false;
145
- }
146
-
147
- this.registry.activeProject = projectId;
148
- this.registry.projects[projectId]!.lastAccessed = Date.now();
149
- this.saveRegistry();
150
- return true;
151
- }
152
-
153
- // Get active project
154
- getActiveProject(): ProjectInfo | null {
155
- if (!this.registry.activeProject) {
156
- return null;
157
- }
158
- return this.registry.projects[this.registry.activeProject] || null;
159
- }
160
-
161
- // Update project stats
162
- updateProjectStats(projectId: string, stats: { totalFiles?: number; totalDecisions?: number; languages?: string[] }): void {
163
- const project = this.registry.projects[projectId];
164
- if (!project) return;
165
-
166
- if (stats.totalFiles !== undefined) {
167
- project.totalFiles = stats.totalFiles;
168
- }
169
- if (stats.totalDecisions !== undefined) {
170
- project.totalDecisions = stats.totalDecisions;
171
- }
172
- if (stats.languages !== undefined) {
173
- project.languages = stats.languages;
174
- }
175
-
176
- this.saveRegistry();
177
- }
178
-
179
- // Get config for a project
180
- getProjectConfig(projectPath: string): MemoryLayerConfig {
181
- const normalizedPath = resolve(projectPath);
182
- const dataDir = this.getProjectDataDir(normalizedPath);
183
-
184
- return {
185
- projectPath: normalizedPath,
186
- dataDir,
187
- maxTokens: 6000,
188
- embeddingModel: 'Xenova/all-MiniLM-L6-v2',
189
- watchIgnore: [
190
- '**/node_modules/**',
191
- '**/.git/**',
192
- '**/dist/**',
193
- '**/build/**',
194
- '**/.next/**',
195
- '**/coverage/**',
196
- '**/*.min.js',
197
- '**/*.min.css',
198
- '**/*.map',
199
- '**/package-lock.json',
200
- '**/yarn.lock',
201
- '**/pnpm-lock.yaml',
202
- '**/.env*',
203
- '**/*.log'
204
- ]
205
- };
206
- }
207
-
208
- // Scan for projects in common locations
209
- discoverProjects(): string[] {
210
- const discovered: string[] = [];
211
- const homeDir = homedir();
212
-
213
- // Common project locations
214
- const searchDirs = [
215
- join(homeDir, 'projects'),
216
- join(homeDir, 'Projects'),
217
- join(homeDir, 'code'),
218
- join(homeDir, 'Code'),
219
- join(homeDir, 'dev'),
220
- join(homeDir, 'Development'),
221
- join(homeDir, 'workspace'),
222
- join(homeDir, 'repos'),
223
- join(homeDir, 'github'),
224
- join(homeDir, 'Desktop'),
225
- join(homeDir, 'Documents'),
226
- ];
227
-
228
- for (const searchDir of searchDirs) {
229
- if (!existsSync(searchDir)) continue;
230
-
231
- try {
232
- const entries = readdirSync(searchDir, { withFileTypes: true });
233
-
234
- for (const entry of entries) {
235
- if (!entry.isDirectory()) continue;
236
-
237
- const projectPath = join(searchDir, entry.name);
238
-
239
- // Check if it looks like a project (has common project files)
240
- const projectIndicators = [
241
- 'package.json',
242
- 'Cargo.toml',
243
- 'go.mod',
244
- 'requirements.txt',
245
- 'pyproject.toml',
246
- 'pom.xml',
247
- 'build.gradle',
248
- '.git'
249
- ];
250
-
251
- const isProject = projectIndicators.some(indicator =>
252
- existsSync(join(projectPath, indicator))
253
- );
254
-
255
- if (isProject) {
256
- discovered.push(projectPath);
257
- }
258
- }
259
- } catch {
260
- // Skip directories we can't read
261
- }
262
- }
263
-
264
- return discovered;
265
- }
266
-
267
- // Get cross-project database connections for search
268
- getProjectDatabases(): Array<{ project: ProjectInfo; db: Database.Database }> {
269
- const result: Array<{ project: ProjectInfo; db: Database.Database }> = [];
270
-
271
- for (const project of this.listProjects()) {
272
- // Check both new and old database names
273
- let dbPath = join(project.dataDir, 'neuronlayer.db');
274
- if (!existsSync(dbPath)) {
275
- dbPath = join(project.dataDir, 'memorylayer.db');
276
- }
277
-
278
- if (existsSync(dbPath)) {
279
- try {
280
- const db = new Database(dbPath, { readonly: true });
281
- result.push({ project, db });
282
- } catch {
283
- // Skip databases we can't open
284
- }
285
- }
286
- }
287
-
288
- return result;
289
- }
290
-
291
- // Close all database connections
292
- closeAllDatabases(dbs: Array<{ db: Database.Database }>): void {
293
- for (const { db } of dbs) {
294
- try {
295
- db.close();
296
- } catch {
297
- // Ignore close errors
298
- }
299
- }
300
- }
301
- }