@comfanion/workflow 4.35.0 → 4.36.2
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.
- package/package.json +1 -1
- package/src/build-info.json +1 -1
- package/src/opencode/plugins/file-indexer.ts +246 -49
- package/src/vectorizer/index.js +84 -3
package/package.json
CHANGED
package/src/build-info.json
CHANGED
|
@@ -1,25 +1,46 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
2
|
import path from "path"
|
|
3
3
|
import fs from "fs/promises"
|
|
4
|
+
import fsSync from "fs"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* File Indexer Plugin
|
|
7
8
|
*
|
|
8
9
|
* Automatically manages semantic search indexes:
|
|
9
|
-
* - On
|
|
10
|
+
* - On plugin load (opencode startup): freshen existing indexes
|
|
10
11
|
* - On file edit: queue file for reindexing (debounced)
|
|
11
12
|
*
|
|
12
13
|
* Configuration in .opencode/config.yaml:
|
|
13
14
|
* vectorizer:
|
|
14
15
|
* enabled: true # Master switch
|
|
15
16
|
* auto_index: true # Enable this plugin
|
|
16
|
-
* debounce_ms:
|
|
17
|
+
* debounce_ms: 1000 # Wait time before indexing
|
|
17
18
|
*
|
|
18
19
|
* Debug mode: set DEBUG=file-indexer or DEBUG=* to see logs
|
|
19
20
|
*/
|
|
20
21
|
|
|
21
22
|
const DEBUG = process.env.DEBUG?.includes('file-indexer') || process.env.DEBUG === '*'
|
|
22
23
|
|
|
24
|
+
let logFilePath: string | null = null
|
|
25
|
+
|
|
26
|
+
// Log to file only
|
|
27
|
+
function logFile(msg: string): void {
|
|
28
|
+
if (logFilePath) {
|
|
29
|
+
const timestamp = new Date().toISOString().slice(11, 19)
|
|
30
|
+
fsSync.appendFileSync(logFilePath, `${timestamp} ${msg}\n`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Log to console AND file
|
|
35
|
+
function log(msg: string): void {
|
|
36
|
+
console.log(`[file-indexer] ${msg}`)
|
|
37
|
+
logFile(msg)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function debug(msg: string): void {
|
|
41
|
+
if (DEBUG) log(msg)
|
|
42
|
+
}
|
|
43
|
+
|
|
23
44
|
// Default config (used if config.yaml is missing or invalid)
|
|
24
45
|
const DEFAULT_CONFIG = {
|
|
25
46
|
enabled: true,
|
|
@@ -41,12 +62,91 @@ interface VectorizerConfig {
|
|
|
41
62
|
exclude: string[]
|
|
42
63
|
}
|
|
43
64
|
|
|
44
|
-
|
|
65
|
+
// Fun messages based on file count and language
|
|
66
|
+
const FUN_MESSAGES = {
|
|
67
|
+
en: {
|
|
68
|
+
indexing: (files: number) => `Indexing ${files} files...`,
|
|
69
|
+
fun: (files: number, mins: number) => {
|
|
70
|
+
if (files < 20) return `Quick coffee? ☕`
|
|
71
|
+
if (files < 100) return `~${mins}min. Stretch break? 🧘`
|
|
72
|
+
if (files < 500) return `~${mins}min. Make coffee ☕ and relax 🛋️`
|
|
73
|
+
return `~${mins}min. Go touch grass 🌿 or take a nap 😴`
|
|
74
|
+
},
|
|
75
|
+
done: (files: number, duration: string) => {
|
|
76
|
+
if (files < 20) return `Done! ${files} files in ${duration}. Fast! 🚀`
|
|
77
|
+
if (files < 100) return `Indexed ${files} files in ${duration}. Let's go! 🎸`
|
|
78
|
+
return `${files} files in ${duration}. Worth the wait! 🎉`
|
|
79
|
+
},
|
|
80
|
+
fresh: () => `Everything's fresh! Nothing to do 😎`,
|
|
81
|
+
error: (msg: string) => `Oops! ${msg} 😬`
|
|
82
|
+
},
|
|
83
|
+
uk: {
|
|
84
|
+
indexing: (files: number) => `Індексую ${files} файлів...`,
|
|
85
|
+
fun: (files: number, mins: number) => {
|
|
86
|
+
if (files < 20) return `Швидка кава? ☕`
|
|
87
|
+
if (files < 100) return `~${mins}хв. Розімнись! 🧘`
|
|
88
|
+
if (files < 500) return `~${mins}хв. Зроби каву ☕ і відпочинь 🛋️`
|
|
89
|
+
return `~${mins}хв. Йди погуляй 🌿 або поспи 😴`
|
|
90
|
+
},
|
|
91
|
+
done: (files: number, duration: string) => {
|
|
92
|
+
if (files < 20) return `Готово! ${files} файлів за ${duration}. Швидко! 🚀`
|
|
93
|
+
if (files < 100) return `${files} файлів за ${duration}. Поїхали! 🎸`
|
|
94
|
+
return `${files} файлів за ${duration}. Варто було чекати! 🎉`
|
|
95
|
+
},
|
|
96
|
+
fresh: () => `Все свіже! Нічого робити 😎`,
|
|
97
|
+
error: (msg: string) => `Ой! ${msg} 😬`
|
|
98
|
+
},
|
|
99
|
+
ru: {
|
|
100
|
+
indexing: (files: number) => `Индексирую ${files} файлов...`,
|
|
101
|
+
fun: (files: number, mins: number) => {
|
|
102
|
+
if (files < 20) return `Кофе? ☕`
|
|
103
|
+
if (files < 100) return `~${mins}мин. Разомнись! 🧘`
|
|
104
|
+
if (files < 500) return `~${mins}мин. Сделай кофе ☕ и отдохни 🛋️`
|
|
105
|
+
return `~${mins}мин. Иди погуляй 🌿 или поспи 😴`
|
|
106
|
+
},
|
|
107
|
+
done: (files: number, duration: string) => {
|
|
108
|
+
if (files < 20) return `Готово! ${files} файлов за ${duration}. Быстро! 🚀`
|
|
109
|
+
if (files < 100) return `${files} файлов за ${duration}. Поехали! 🎸`
|
|
110
|
+
return `${files} файлов за ${duration}. Стоило подождать! 🎉`
|
|
111
|
+
},
|
|
112
|
+
fresh: () => `Всё свежее! Делать нечего 😎`,
|
|
113
|
+
error: (msg: string) => `Ой! ${msg} 😬`
|
|
114
|
+
}
|
|
115
|
+
}
|
|
45
116
|
|
|
46
|
-
|
|
47
|
-
|
|
117
|
+
type Lang = keyof typeof FUN_MESSAGES
|
|
118
|
+
|
|
119
|
+
async function getLanguage(projectRoot: string): Promise<Lang> {
|
|
120
|
+
try {
|
|
121
|
+
const configPath = path.join(projectRoot, ".opencode", "config.yaml")
|
|
122
|
+
const content = await fs.readFile(configPath, 'utf8')
|
|
123
|
+
const match = content.match(/communication_language:\s*["']?(\w+)["']?/i)
|
|
124
|
+
const lang = match?.[1]?.toLowerCase()
|
|
125
|
+
if (lang === 'ukrainian' || lang === 'uk') return 'uk'
|
|
126
|
+
if (lang === 'russian' || lang === 'ru') return 'ru'
|
|
127
|
+
return 'en'
|
|
128
|
+
} catch {
|
|
129
|
+
return 'en'
|
|
130
|
+
}
|
|
48
131
|
}
|
|
49
132
|
|
|
133
|
+
function estimateTime(fileCount: number): number {
|
|
134
|
+
// Model loading: ~30 sec, then ~0.5 sec per file
|
|
135
|
+
const modelLoadTime = 30 // seconds
|
|
136
|
+
const perFileTime = 0.5 // seconds
|
|
137
|
+
const totalSeconds = modelLoadTime + (fileCount * perFileTime)
|
|
138
|
+
return Math.ceil(totalSeconds / 60) // minutes
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatDuration(seconds: number): string {
|
|
142
|
+
if (seconds < 60) return `${Math.round(seconds)}s`
|
|
143
|
+
const mins = Math.floor(seconds / 60)
|
|
144
|
+
const secs = Math.round(seconds % 60)
|
|
145
|
+
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const pendingFiles: Map<string, { indexName: string; timestamp: number }> = new Map()
|
|
149
|
+
|
|
50
150
|
async function loadConfig(projectRoot: string): Promise<VectorizerConfig> {
|
|
51
151
|
try {
|
|
52
152
|
const configPath = path.join(projectRoot, ".opencode", "config.yaml")
|
|
@@ -119,46 +219,120 @@ async function hasIndex(projectRoot: string, indexName: string): Promise<boolean
|
|
|
119
219
|
}
|
|
120
220
|
}
|
|
121
221
|
|
|
222
|
+
interface IndexResult {
|
|
223
|
+
totalFiles: number
|
|
224
|
+
elapsedSeconds: number
|
|
225
|
+
action: 'created' | 'rebuilt' | 'freshened' | 'skipped'
|
|
226
|
+
}
|
|
227
|
+
|
|
122
228
|
/**
|
|
123
229
|
* Ensure index exists and is fresh on session start
|
|
230
|
+
* Creates index if missing, freshens if exists
|
|
124
231
|
*/
|
|
125
|
-
async function ensureIndexOnSessionStart(
|
|
232
|
+
async function ensureIndexOnSessionStart(
|
|
233
|
+
projectRoot: string,
|
|
234
|
+
config: VectorizerConfig,
|
|
235
|
+
onStart?: (totalFiles: number, estimatedMins: number) => void
|
|
236
|
+
): Promise<IndexResult> {
|
|
237
|
+
let totalFiles = 0
|
|
238
|
+
let elapsedSeconds = 0
|
|
239
|
+
let action: IndexResult['action'] = 'skipped'
|
|
240
|
+
|
|
126
241
|
if (!await isVectorizerInstalled(projectRoot)) {
|
|
127
|
-
|
|
128
|
-
return
|
|
242
|
+
log(`Vectorizer not installed - run: npx @comfanion/workflow vectorizer install`)
|
|
243
|
+
return { totalFiles: 0, elapsedSeconds: 0, action: 'skipped' }
|
|
129
244
|
}
|
|
130
245
|
|
|
131
246
|
try {
|
|
132
247
|
const vectorizerModule = path.join(projectRoot, ".opencode", "vectorizer", "index.js")
|
|
133
248
|
const { CodebaseIndexer } = await import(`file://${vectorizerModule}`)
|
|
249
|
+
const overallStart = Date.now()
|
|
250
|
+
|
|
251
|
+
// First pass - count files and check health
|
|
252
|
+
let needsWork = false
|
|
253
|
+
let totalExpectedFiles = 0
|
|
134
254
|
|
|
135
|
-
// Check each enabled index
|
|
136
255
|
for (const [indexName, indexConfig] of Object.entries(config.indexes)) {
|
|
137
256
|
if (!indexConfig.enabled) continue
|
|
138
|
-
|
|
257
|
+
const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
|
|
139
258
|
const indexExists = await hasIndex(projectRoot, indexName)
|
|
140
259
|
|
|
141
260
|
if (!indexExists) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
261
|
+
const health = await indexer.checkHealth(config.exclude)
|
|
262
|
+
totalExpectedFiles += health.expectedCount
|
|
263
|
+
needsWork = true
|
|
264
|
+
} else {
|
|
265
|
+
const health = await indexer.checkHealth(config.exclude)
|
|
266
|
+
if (health.needsReindex) {
|
|
267
|
+
totalExpectedFiles += health.expectedCount
|
|
268
|
+
needsWork = true
|
|
269
|
+
}
|
|
145
270
|
}
|
|
271
|
+
await indexer.unloadModel()
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Notify about work to do
|
|
275
|
+
if (needsWork && onStart) {
|
|
276
|
+
onStart(totalExpectedFiles, estimateTime(totalExpectedFiles))
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Second pass - do the actual work
|
|
280
|
+
for (const [indexName, indexConfig] of Object.entries(config.indexes)) {
|
|
281
|
+
if (!indexConfig.enabled) continue
|
|
282
|
+
|
|
283
|
+
const indexExists = await hasIndex(projectRoot, indexName)
|
|
284
|
+
const startTime = Date.now()
|
|
146
285
|
|
|
147
|
-
// Index exists - freshen it (update stale files)
|
|
148
|
-
debug(`Session start: freshening index "${indexName}"...`)
|
|
149
286
|
const indexer = await new CodebaseIndexer(projectRoot, indexName).init()
|
|
150
|
-
const stats = await indexer.freshen()
|
|
151
287
|
|
|
152
|
-
if (
|
|
153
|
-
|
|
288
|
+
if (!indexExists) {
|
|
289
|
+
log(`Creating "${indexName}" index...`)
|
|
290
|
+
const stats = await indexer.indexAll((indexed: number, total: number, file: string) => {
|
|
291
|
+
if (indexed % 10 === 0 || indexed === total) {
|
|
292
|
+
logFile(`"${indexName}": ${indexed}/${total} - ${file}`)
|
|
293
|
+
}
|
|
294
|
+
}, config.exclude)
|
|
295
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
|
|
296
|
+
log(`"${indexName}": done ${stats.indexed} files (${elapsed}s)`)
|
|
297
|
+
totalFiles += stats.indexed
|
|
298
|
+
action = 'created'
|
|
154
299
|
} else {
|
|
155
|
-
|
|
300
|
+
const health = await indexer.checkHealth(config.exclude)
|
|
301
|
+
|
|
302
|
+
if (health.needsReindex) {
|
|
303
|
+
log(`Rebuilding "${indexName}" (${health.reason}: ${health.currentCount} vs ${health.expectedCount} files)...`)
|
|
304
|
+
const stats = await indexer.indexAll((indexed: number, total: number, file: string) => {
|
|
305
|
+
if (indexed % 10 === 0 || indexed === total) {
|
|
306
|
+
logFile(`"${indexName}": ${indexed}/${total} - ${file}`)
|
|
307
|
+
}
|
|
308
|
+
}, config.exclude)
|
|
309
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
|
|
310
|
+
log(`"${indexName}": rebuilt ${stats.indexed} files (${elapsed}s)`)
|
|
311
|
+
totalFiles += stats.indexed
|
|
312
|
+
action = 'rebuilt'
|
|
313
|
+
} else {
|
|
314
|
+
log(`Freshening "${indexName}"...`)
|
|
315
|
+
const stats = await indexer.freshen()
|
|
316
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1)
|
|
317
|
+
|
|
318
|
+
if (stats.updated > 0 || stats.deleted > 0) {
|
|
319
|
+
log(`"${indexName}": +${stats.updated} -${stats.deleted} (${elapsed}s)`)
|
|
320
|
+
action = 'freshened'
|
|
321
|
+
} else {
|
|
322
|
+
log(`"${indexName}": fresh (${elapsed}s)`)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
156
325
|
}
|
|
157
326
|
|
|
158
327
|
await indexer.unloadModel()
|
|
159
328
|
}
|
|
329
|
+
|
|
330
|
+
elapsedSeconds = (Date.now() - overallStart) / 1000
|
|
331
|
+
log(`Indexes ready!`)
|
|
332
|
+
return { totalFiles, elapsedSeconds, action }
|
|
160
333
|
} catch (e) {
|
|
161
|
-
|
|
334
|
+
log(`Index error: ${(e as Error).message}`)
|
|
335
|
+
throw e
|
|
162
336
|
}
|
|
163
337
|
}
|
|
164
338
|
|
|
@@ -209,20 +383,63 @@ async function processPendingFiles(projectRoot: string, config: VectorizerConfig
|
|
|
209
383
|
}
|
|
210
384
|
}
|
|
211
385
|
|
|
212
|
-
export const FileIndexerPlugin: Plugin = async ({ directory }) => {
|
|
386
|
+
export const FileIndexerPlugin: Plugin = async ({ directory, client }) => {
|
|
213
387
|
let processingTimeout: NodeJS.Timeout | null = null
|
|
214
388
|
let config = await loadConfig(directory)
|
|
215
389
|
|
|
390
|
+
// Toast helper
|
|
391
|
+
const toast = async (message: string, variant: 'info' | 'success' | 'error' = 'info') => {
|
|
392
|
+
try {
|
|
393
|
+
await client?.tui?.showToast?.({ body: { message, variant } })
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Always log plugin load
|
|
398
|
+
log(`Plugin loaded for: ${path.basename(directory)}`)
|
|
399
|
+
|
|
216
400
|
// Check if plugin should be active
|
|
217
401
|
if (!config.enabled || !config.auto_index) {
|
|
218
|
-
|
|
402
|
+
log(`Plugin DISABLED (enabled: ${config.enabled}, auto_index: ${config.auto_index})`)
|
|
219
403
|
return {
|
|
220
404
|
event: async () => {}, // No-op
|
|
221
405
|
}
|
|
222
406
|
}
|
|
223
407
|
|
|
224
|
-
|
|
225
|
-
|
|
408
|
+
// Setup log file
|
|
409
|
+
logFilePath = path.join(directory, '.opencode', 'indexer.log')
|
|
410
|
+
fsSync.writeFileSync(logFilePath, '') // Clear old log
|
|
411
|
+
|
|
412
|
+
log(`Plugin ACTIVE`)
|
|
413
|
+
|
|
414
|
+
// Get language for fun messages
|
|
415
|
+
const lang = await getLanguage(directory)
|
|
416
|
+
const messages = FUN_MESSAGES[lang]
|
|
417
|
+
|
|
418
|
+
// Run indexing async (non-blocking) with toast notifications
|
|
419
|
+
// Small delay to let TUI initialize
|
|
420
|
+
setTimeout(async () => {
|
|
421
|
+
try {
|
|
422
|
+
const result = await ensureIndexOnSessionStart(
|
|
423
|
+
directory,
|
|
424
|
+
config,
|
|
425
|
+
// onStart callback - show 2 toasts
|
|
426
|
+
async (totalFiles, estimatedMins) => {
|
|
427
|
+
await toast(messages.indexing(totalFiles), 'info')
|
|
428
|
+
setTimeout(() => toast(messages.fun(totalFiles, estimatedMins), 'info'), 1500)
|
|
429
|
+
}
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
// Show result
|
|
433
|
+
if (result.action === 'skipped') {
|
|
434
|
+
toast(messages.fresh(), 'success')
|
|
435
|
+
} else {
|
|
436
|
+
const duration = formatDuration(result.elapsedSeconds)
|
|
437
|
+
toast(messages.done(result.totalFiles, duration), 'success')
|
|
438
|
+
}
|
|
439
|
+
} catch (e: any) {
|
|
440
|
+
toast(messages.error(e.message), 'error')
|
|
441
|
+
}
|
|
442
|
+
}, 1000)
|
|
226
443
|
|
|
227
444
|
function queueFileForIndexing(filePath: string): void {
|
|
228
445
|
const relativePath = path.relative(directory, filePath)
|
|
@@ -250,35 +467,15 @@ export const FileIndexerPlugin: Plugin = async ({ directory }) => {
|
|
|
250
467
|
}, config.debounce_ms + 100)
|
|
251
468
|
}
|
|
252
469
|
|
|
253
|
-
//
|
|
254
|
-
let sessionFreshened = false
|
|
255
|
-
|
|
470
|
+
// Event handler for file changes (if events start working in future)
|
|
256
471
|
return {
|
|
257
|
-
event: async (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// Freshen index on session start/resume
|
|
261
|
-
if ((event.type === "session.started" || event.type === "session.resumed") && !sessionFreshened) {
|
|
262
|
-
sessionFreshened = true
|
|
263
|
-
debug(`${event.type}: checking indexes...`)
|
|
264
|
-
// Run async, don't block
|
|
265
|
-
ensureIndexOnSessionStart(directory, config).catch(e => debug(`Error: ${e.message}`))
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (event.type === "file.edited") {
|
|
269
|
-
const props = (event as any).properties || {}
|
|
270
|
-
const filePath = props.file || props.path || props.filePath
|
|
271
|
-
if (filePath) {
|
|
272
|
-
debug(`file.edited: ${filePath}`)
|
|
273
|
-
queueFileForIndexing(filePath)
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (event.type === "file.watcher.updated") {
|
|
472
|
+
event: async ({ event }) => {
|
|
473
|
+
// File edit events - queue for reindexing
|
|
474
|
+
if (event.type === "file.edited" || event.type === "file.watcher.updated") {
|
|
278
475
|
const props = (event as any).properties || {}
|
|
279
476
|
const filePath = props.file || props.path || props.filePath
|
|
280
477
|
if (filePath) {
|
|
281
|
-
debug(
|
|
478
|
+
debug(`${event.type}: ${filePath}`)
|
|
282
479
|
queueFileForIndexing(filePath)
|
|
283
480
|
}
|
|
284
481
|
}
|
package/src/vectorizer/index.js
CHANGED
|
@@ -22,18 +22,22 @@ if (!DEBUG) {
|
|
|
22
22
|
const INDEX_PRESETS = {
|
|
23
23
|
code: {
|
|
24
24
|
pattern: '**/*.{js,ts,jsx,tsx,mjs,cjs,py,go,rs,java,kt,swift,c,cpp,h,hpp,cs,rb,php,scala,clj}',
|
|
25
|
-
|
|
25
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/.opencode/**', '**/docs/**', '**/vendor/**', '**/__pycache__/**'],
|
|
26
|
+
description: 'Source code files (excludes docs, vendor, node_modules)'
|
|
26
27
|
},
|
|
27
28
|
docs: {
|
|
28
|
-
pattern: '
|
|
29
|
-
|
|
29
|
+
pattern: 'docs/**/*.{md,mdx,txt,rst,adoc}',
|
|
30
|
+
ignore: [],
|
|
31
|
+
description: 'Documentation in docs/ folder'
|
|
30
32
|
},
|
|
31
33
|
config: {
|
|
32
34
|
pattern: '**/*.{yaml,yml,json,toml,ini,env,xml}',
|
|
35
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/.opencode/**'],
|
|
33
36
|
description: 'Configuration files'
|
|
34
37
|
},
|
|
35
38
|
all: {
|
|
36
39
|
pattern: '**/*.{js,ts,jsx,tsx,mjs,cjs,py,go,rs,java,kt,swift,c,cpp,h,hpp,cs,rb,php,scala,clj,md,mdx,txt,rst,adoc,yaml,yml,json,toml}',
|
|
40
|
+
ignore: ['**/node_modules/**', '**/.git/**', '**/.opencode/**'],
|
|
37
41
|
description: 'All supported files'
|
|
38
42
|
}
|
|
39
43
|
};
|
|
@@ -208,6 +212,43 @@ class CodebaseIndexer {
|
|
|
208
212
|
return results;
|
|
209
213
|
}
|
|
210
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Check if index needs full reindex (files don't match current patterns)
|
|
217
|
+
* @param {string[]} extraIgnore - Additional patterns to ignore
|
|
218
|
+
* Returns { needsReindex, reason, currentCount, expectedCount }
|
|
219
|
+
*/
|
|
220
|
+
async checkHealth(extraIgnore = []) {
|
|
221
|
+
const { glob } = await import('glob');
|
|
222
|
+
const preset = INDEX_PRESETS[this.indexName] || INDEX_PRESETS.code;
|
|
223
|
+
|
|
224
|
+
// Combine preset ignore with extra ignore patterns
|
|
225
|
+
const ignore = [...(preset.ignore || []), ...extraIgnore.map(p => `**/${p}/**`)];
|
|
226
|
+
|
|
227
|
+
const expectedFiles = await glob(preset.pattern, {
|
|
228
|
+
cwd: this.root,
|
|
229
|
+
nodir: true,
|
|
230
|
+
ignore
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const indexedFiles = Object.keys(this.hashes);
|
|
234
|
+
const currentCount = indexedFiles.length;
|
|
235
|
+
const expectedCount = expectedFiles.length;
|
|
236
|
+
|
|
237
|
+
// Check if counts differ significantly (>20% difference or index is empty)
|
|
238
|
+
const diff = Math.abs(currentCount - expectedCount);
|
|
239
|
+
const threshold = Math.max(5, expectedCount * 0.2); // 20% or at least 5 files
|
|
240
|
+
|
|
241
|
+
if (currentCount === 0 && expectedCount > 0) {
|
|
242
|
+
return { needsReindex: true, reason: 'empty', currentCount, expectedCount };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (diff > threshold) {
|
|
246
|
+
return { needsReindex: true, reason: 'mismatch', currentCount, expectedCount };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { needsReindex: false, reason: 'ok', currentCount, expectedCount };
|
|
250
|
+
}
|
|
251
|
+
|
|
211
252
|
/**
|
|
212
253
|
* Freshen index - check for stale files and reindex only changed ones
|
|
213
254
|
* Returns { checked, updated, deleted } counts
|
|
@@ -246,6 +287,46 @@ class CodebaseIndexer {
|
|
|
246
287
|
return { checked, updated, deleted };
|
|
247
288
|
}
|
|
248
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Index all files matching the preset pattern
|
|
292
|
+
* @param {function} onProgress - Optional callback(indexed, total, currentFile)
|
|
293
|
+
* @param {string[]} extraIgnore - Additional patterns to ignore
|
|
294
|
+
* Returns { indexed, skipped } counts
|
|
295
|
+
*/
|
|
296
|
+
async indexAll(onProgress = null, extraIgnore = []) {
|
|
297
|
+
const { glob } = await import('glob');
|
|
298
|
+
const preset = INDEX_PRESETS[this.indexName] || INDEX_PRESETS.code;
|
|
299
|
+
|
|
300
|
+
// Combine preset ignore with extra ignore patterns
|
|
301
|
+
const ignore = [...(preset.ignore || []), ...extraIgnore.map(p => `**/${p}/**`)];
|
|
302
|
+
|
|
303
|
+
const files = await glob(preset.pattern, {
|
|
304
|
+
cwd: this.root,
|
|
305
|
+
nodir: true,
|
|
306
|
+
ignore
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
let indexed = 0;
|
|
310
|
+
let skipped = 0;
|
|
311
|
+
|
|
312
|
+
for (const relPath of files) {
|
|
313
|
+
const filePath = path.join(this.root, relPath);
|
|
314
|
+
try {
|
|
315
|
+
const wasIndexed = await this.indexFile(filePath);
|
|
316
|
+
if (wasIndexed) {
|
|
317
|
+
indexed++;
|
|
318
|
+
if (onProgress) onProgress(indexed, files.length, relPath);
|
|
319
|
+
} else {
|
|
320
|
+
skipped++;
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
skipped++;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return { indexed, skipped, total: files.length };
|
|
328
|
+
}
|
|
329
|
+
|
|
249
330
|
/**
|
|
250
331
|
* Index a single file by path (convenience method)
|
|
251
332
|
*/
|