@mnemonik/scanner 1.0.0 → 2.0.1
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/README.md +90 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +3 -0
- package/dist/client.js.map +1 -1
- package/dist/daemon.d.ts +22 -10
- package/dist/daemon.js +172 -73
- package/dist/daemon.js.map +1 -1
- package/dist/discovery.d.ts +21 -0
- package/dist/discovery.js +107 -0
- package/dist/discovery.js.map +1 -0
- package/dist/index.js +248 -56
- package/dist/index.js.map +1 -1
- package/dist/watcher.d.ts +3 -1
- package/dist/watcher.js +9 -1
- package/dist/watcher.js.map +1 -1
- package/package.json +3 -3
- package/src/client.ts +4 -0
- package/src/daemon.ts +214 -80
- package/src/discovery.ts +124 -0
- package/src/index.ts +273 -58
- package/src/watcher.ts +13 -2
package/src/index.ts
CHANGED
|
@@ -1,74 +1,103 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { readFile, unlink, mkdir } from 'fs/promises';
|
|
3
|
+
import { readFile, writeFile, unlink, mkdir, stat, chmod } from 'fs/promises';
|
|
4
4
|
import { join } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { ScannerDaemon } from './daemon.js';
|
|
7
7
|
|
|
8
8
|
const DEFAULT_SERVER = 'https://api.mnemonik.dev';
|
|
9
9
|
const MNEMONIK_DIR = join(homedir(), '.mnemonik');
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const PID_FILE = join(MNEMONIK_DIR, 'daemon.pid');
|
|
11
|
+
const LOG_FILE = join(MNEMONIK_DIR, 'scanner.log');
|
|
12
|
+
const CONFIG_FILE = join(MNEMONIK_DIR, 'scanner.json');
|
|
12
13
|
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
interface ScannerConfig {
|
|
16
|
+
roots: string[];
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
server?: string;
|
|
19
|
+
refreshIntervalMs?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CliArgs {
|
|
23
|
+
command: 'start' | 'stop' | 'status' | 'log' | 'help';
|
|
24
|
+
key?: string;
|
|
25
|
+
server?: string;
|
|
26
|
+
roots?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseCliArgs(): CliArgs {
|
|
15
30
|
const args = process.argv.slice(2);
|
|
16
|
-
|
|
31
|
+
const command = (args[0] ?? 'help') as CliArgs['command'];
|
|
32
|
+
|
|
33
|
+
if (!['start', 'stop', 'status', 'log', 'help'].includes(command)) {
|
|
34
|
+
return { command: 'help' };
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
let key: string | undefined;
|
|
38
|
+
let server: string | undefined;
|
|
39
|
+
let roots: string[] | undefined;
|
|
18
40
|
|
|
19
|
-
for (let i =
|
|
20
|
-
if (args[i] === '--
|
|
21
|
-
else if (args[i] === '--
|
|
41
|
+
for (let i = 1; i < args.length; i++) {
|
|
42
|
+
if (args[i] === '--key' && args[i + 1]) key = args[++i]!;
|
|
43
|
+
else if (args[i] === '--server' && args[i + 1]) server = args[++i]!;
|
|
44
|
+
else if (args[i] === '--roots' && args[i + 1]) {
|
|
45
|
+
roots = args[++i]!.split(',').map((r) => r.trim());
|
|
46
|
+
}
|
|
22
47
|
}
|
|
23
48
|
|
|
24
|
-
return {
|
|
49
|
+
return { command, key, server, roots };
|
|
25
50
|
}
|
|
26
51
|
|
|
27
|
-
async function
|
|
28
|
-
const configPath = join(process.cwd(), '.mnemonik.json');
|
|
52
|
+
async function readConfig(): Promise<ScannerConfig | null> {
|
|
29
53
|
try {
|
|
30
|
-
const raw = await readFile(
|
|
31
|
-
|
|
32
|
-
if (typeof parsed.projectId === 'string' && parsed.projectId.length > 0) {
|
|
33
|
-
return parsed.projectId;
|
|
34
|
-
}
|
|
35
|
-
console.error('[mnemonik] .mnemonik.json missing "projectId" field.');
|
|
36
|
-
process.exit(1);
|
|
54
|
+
const raw = await readFile(CONFIG_FILE, 'utf-8');
|
|
55
|
+
return JSON.parse(raw) as ScannerConfig;
|
|
37
56
|
} catch {
|
|
38
|
-
|
|
39
|
-
'[mnemonik] No .mnemonik.json found in current directory.\n' +
|
|
40
|
-
' Run session_bootstrap from your IDE to create one.'
|
|
41
|
-
);
|
|
42
|
-
process.exit(1);
|
|
57
|
+
return null;
|
|
43
58
|
}
|
|
44
59
|
}
|
|
45
60
|
|
|
46
|
-
function
|
|
47
|
-
|
|
61
|
+
async function writeConfig(config: ScannerConfig): Promise<void> {
|
|
62
|
+
await mkdir(MNEMONIK_DIR, { recursive: true });
|
|
63
|
+
await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
function
|
|
51
|
-
|
|
66
|
+
async function checkConfigPermissions(): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
const s = await stat(CONFIG_FILE);
|
|
69
|
+
// Check if group or other have read/write permissions
|
|
70
|
+
const mode = s.mode & 0o077;
|
|
71
|
+
if (mode !== 0) {
|
|
72
|
+
console.warn(
|
|
73
|
+
`[mnemonik] WARNING: ${CONFIG_FILE} has overly permissive permissions (${(s.mode & 0o777).toString(8)}).`
|
|
74
|
+
);
|
|
75
|
+
console.warn(`[mnemonik] Fixing to 0600 (owner read/write only).`);
|
|
76
|
+
await chmod(CONFIG_FILE, 0o600);
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Config file doesn't exist yet
|
|
80
|
+
}
|
|
52
81
|
}
|
|
53
82
|
|
|
54
|
-
async function rotateLogIfNeeded(
|
|
83
|
+
async function rotateLogIfNeeded(): Promise<void> {
|
|
55
84
|
try {
|
|
56
|
-
const {
|
|
57
|
-
const s = await
|
|
85
|
+
const { rename } = await import('fs/promises');
|
|
86
|
+
const s = await stat(LOG_FILE);
|
|
58
87
|
if (s.size > MAX_LOG_SIZE) {
|
|
59
|
-
await rename(
|
|
88
|
+
await rename(LOG_FILE, LOG_FILE + '.old');
|
|
60
89
|
}
|
|
61
90
|
} catch {
|
|
62
91
|
// Log file doesn't exist yet
|
|
63
92
|
}
|
|
64
93
|
}
|
|
65
94
|
|
|
66
|
-
async function acquireLock(
|
|
95
|
+
async function acquireLock(retried = false): Promise<boolean> {
|
|
67
96
|
try {
|
|
68
97
|
const { open: fsOpen } = await import('fs/promises');
|
|
69
98
|
const { constants } = await import('fs');
|
|
70
99
|
const fd = await fsOpen(
|
|
71
|
-
|
|
100
|
+
PID_FILE,
|
|
72
101
|
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,
|
|
73
102
|
0o644
|
|
74
103
|
);
|
|
@@ -78,7 +107,7 @@ async function acquireLock(lockPath: string, retried = false): Promise<boolean>
|
|
|
78
107
|
} catch (err: unknown) {
|
|
79
108
|
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') return false;
|
|
80
109
|
|
|
81
|
-
const existing = await readFile(
|
|
110
|
+
const existing = await readFile(PID_FILE, 'utf-8').catch(() => null);
|
|
82
111
|
if (existing) {
|
|
83
112
|
const pid = parseInt(existing.trim(), 10);
|
|
84
113
|
try {
|
|
@@ -89,71 +118,257 @@ async function acquireLock(lockPath: string, retried = false): Promise<boolean>
|
|
|
89
118
|
}
|
|
90
119
|
}
|
|
91
120
|
if (retried) return false;
|
|
92
|
-
await unlink(
|
|
93
|
-
return acquireLock(
|
|
121
|
+
await unlink(PID_FILE).catch(() => {});
|
|
122
|
+
return acquireLock(true);
|
|
94
123
|
}
|
|
95
124
|
}
|
|
96
125
|
|
|
97
|
-
async function releaseLock(
|
|
98
|
-
await unlink(
|
|
126
|
+
async function releaseLock(): Promise<void> {
|
|
127
|
+
await unlink(PID_FILE).catch(() => {});
|
|
99
128
|
}
|
|
100
129
|
|
|
101
|
-
async function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
130
|
+
async function readPid(): Promise<number | null> {
|
|
131
|
+
try {
|
|
132
|
+
const raw = await readFile(PID_FILE, 'utf-8');
|
|
133
|
+
const pid = parseInt(raw.trim(), 10);
|
|
134
|
+
if (isNaN(pid)) return null;
|
|
135
|
+
// Check if process is actually alive
|
|
136
|
+
try {
|
|
137
|
+
process.kill(pid, 0);
|
|
138
|
+
return pid;
|
|
139
|
+
} catch {
|
|
140
|
+
return null; // Stale PID
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function printHelp(): void {
|
|
148
|
+
console.log(`
|
|
149
|
+
mnemonik-scanner - Automatic codebase indexing daemon
|
|
106
150
|
|
|
151
|
+
Usage:
|
|
152
|
+
mnemonik-scanner start [options] Start the scanner daemon
|
|
153
|
+
mnemonik-scanner stop Stop the running daemon
|
|
154
|
+
mnemonik-scanner status Show daemon status
|
|
155
|
+
mnemonik-scanner log Tail the scanner log file
|
|
156
|
+
mnemonik-scanner help Show this help
|
|
157
|
+
|
|
158
|
+
Options (for start):
|
|
159
|
+
--key <api-key> API key (or set MNEMONIK_API_KEY env var)
|
|
160
|
+
--server <url> Server URL (default: https://api.mnemonik.dev)
|
|
161
|
+
--roots <dirs> Comma-separated directories to scan for projects
|
|
162
|
+
|
|
163
|
+
Examples:
|
|
164
|
+
mnemonik-scanner start --key mnk_... --roots ~/Projects,~/work
|
|
165
|
+
mnemonik-scanner status
|
|
166
|
+
mnemonik-scanner stop
|
|
167
|
+
|
|
168
|
+
Configuration is saved to ~/.mnemonik/scanner.json after first run.
|
|
169
|
+
Subsequent starts use saved config (CLI args override).
|
|
170
|
+
|
|
171
|
+
Environment variables:
|
|
172
|
+
MNEMONIK_API_KEY API key (takes precedence over config file)
|
|
173
|
+
`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function handleStart(cli: CliArgs): Promise<void> {
|
|
177
|
+
const config = await readConfig();
|
|
178
|
+
|
|
179
|
+
// Resolve API key: env var > CLI > config
|
|
180
|
+
const apiKey = process.env.MNEMONIK_API_KEY || cli.key || config?.apiKey;
|
|
107
181
|
if (!apiKey) {
|
|
108
182
|
console.error(
|
|
109
183
|
'[mnemonik] Missing API key.\n' +
|
|
110
|
-
'
|
|
111
|
-
'
|
|
184
|
+
' Use --key <api-key>, set MNEMONIK_API_KEY env var,\n' +
|
|
185
|
+
' or run once with --key to save to config.'
|
|
186
|
+
);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const roots = cli.roots || config?.roots;
|
|
191
|
+
if (!roots || roots.length === 0) {
|
|
192
|
+
console.error(
|
|
193
|
+
'[mnemonik] No roots specified.\n' +
|
|
194
|
+
' Use --roots ~/Projects,~/work to specify directories to scan.'
|
|
112
195
|
);
|
|
113
196
|
process.exit(1);
|
|
114
197
|
}
|
|
115
198
|
|
|
199
|
+
const server = cli.server || config?.server || DEFAULT_SERVER;
|
|
200
|
+
|
|
116
201
|
// Ensure directories exist
|
|
117
|
-
await mkdir(
|
|
118
|
-
await mkdir(LOGS_DIR, { recursive: true });
|
|
202
|
+
await mkdir(MNEMONIK_DIR, { recursive: true });
|
|
119
203
|
|
|
120
|
-
|
|
121
|
-
const
|
|
204
|
+
// Save config for future runs (never save env var key to file)
|
|
205
|
+
const configToSave: ScannerConfig = {
|
|
206
|
+
roots,
|
|
207
|
+
server,
|
|
208
|
+
refreshIntervalMs: config?.refreshIntervalMs,
|
|
209
|
+
};
|
|
210
|
+
// Only save API key to config if it came from CLI (not env var)
|
|
211
|
+
if (cli.key) {
|
|
212
|
+
configToSave.apiKey = cli.key;
|
|
213
|
+
} else if (config?.apiKey && !process.env.MNEMONIK_API_KEY) {
|
|
214
|
+
configToSave.apiKey = config.apiKey;
|
|
215
|
+
}
|
|
216
|
+
await writeConfig(configToSave);
|
|
217
|
+
await checkConfigPermissions();
|
|
122
218
|
|
|
123
|
-
await rotateLogIfNeeded(
|
|
219
|
+
await rotateLogIfNeeded();
|
|
124
220
|
|
|
125
|
-
const locked = await acquireLock(
|
|
221
|
+
const locked = await acquireLock();
|
|
126
222
|
if (!locked) {
|
|
127
|
-
|
|
223
|
+
const pid = await readPid();
|
|
224
|
+
console.log(
|
|
225
|
+
`[mnemonik] Scanner daemon already running (PID ${pid}). Use 'mnemonik-scanner stop' first.`
|
|
226
|
+
);
|
|
128
227
|
process.exit(0);
|
|
129
228
|
}
|
|
130
229
|
|
|
131
|
-
const projectRoot = process.cwd();
|
|
132
230
|
const daemon = new ScannerDaemon({
|
|
133
|
-
projectId,
|
|
134
|
-
projectRoot,
|
|
135
231
|
serverUrl: server,
|
|
136
232
|
apiKey,
|
|
233
|
+
roots,
|
|
234
|
+
refreshIntervalMs: config?.refreshIntervalMs,
|
|
137
235
|
});
|
|
138
236
|
|
|
139
237
|
const shutdown = async () => {
|
|
140
238
|
console.log('\n[mnemonik] Shutting down...');
|
|
141
239
|
await daemon.stop();
|
|
142
|
-
await releaseLock(
|
|
240
|
+
await releaseLock();
|
|
143
241
|
process.exit(0);
|
|
144
242
|
};
|
|
145
243
|
|
|
146
244
|
process.on('SIGINT', shutdown);
|
|
147
245
|
process.on('SIGTERM', shutdown);
|
|
246
|
+
process.on('SIGHUP', shutdown); // Graceful restart — external tooling sends SIGHUP then relaunches
|
|
148
247
|
|
|
149
248
|
try {
|
|
150
249
|
await daemon.start();
|
|
151
|
-
console.log('[mnemonik] Watching for changes.');
|
|
152
250
|
} catch (err) {
|
|
153
251
|
console.error(`[mnemonik] ${(err as Error).message}`);
|
|
154
|
-
await releaseLock(
|
|
252
|
+
await releaseLock();
|
|
155
253
|
process.exit(1);
|
|
156
254
|
}
|
|
157
255
|
}
|
|
158
256
|
|
|
257
|
+
async function handleStop(): Promise<void> {
|
|
258
|
+
const pid = await readPid();
|
|
259
|
+
if (!pid) {
|
|
260
|
+
console.log('[mnemonik] Scanner daemon is not running.');
|
|
261
|
+
// Clean up stale PID file if it exists
|
|
262
|
+
await unlink(PID_FILE).catch(() => {});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
process.kill(pid, 'SIGTERM');
|
|
268
|
+
console.log(`[mnemonik] Sent SIGTERM to daemon (PID ${pid}).`);
|
|
269
|
+
|
|
270
|
+
// Wait for process to exit (up to 5 seconds)
|
|
271
|
+
for (let i = 0; i < 50; i++) {
|
|
272
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
273
|
+
try {
|
|
274
|
+
process.kill(pid, 0);
|
|
275
|
+
} catch {
|
|
276
|
+
console.log('[mnemonik] Daemon stopped.');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
console.warn('[mnemonik] Daemon did not exit within 5 seconds.');
|
|
281
|
+
} catch {
|
|
282
|
+
console.log('[mnemonik] Daemon process not found. Cleaning up PID file.');
|
|
283
|
+
await unlink(PID_FILE).catch(() => {});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function handleStatus(): Promise<void> {
|
|
288
|
+
const pid = await readPid();
|
|
289
|
+
const config = await readConfig();
|
|
290
|
+
|
|
291
|
+
if (!pid) {
|
|
292
|
+
console.log('Status: not running');
|
|
293
|
+
if (config?.roots) {
|
|
294
|
+
console.log(`Configured roots: ${config.roots.join(', ')}`);
|
|
295
|
+
}
|
|
296
|
+
console.log(`\nStart with: mnemonik-scanner start`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
console.log(`Status: running (PID ${pid})`);
|
|
301
|
+
if (config?.roots) {
|
|
302
|
+
console.log(`Roots: ${config.roots.join(', ')}`);
|
|
303
|
+
}
|
|
304
|
+
if (config?.server) {
|
|
305
|
+
console.log(`Server: ${config.server}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Check for old per-project daemons
|
|
309
|
+
try {
|
|
310
|
+
const { readdir: readdirAsync } = await import('fs/promises');
|
|
311
|
+
const oldDaemonsDir = join(MNEMONIK_DIR, 'daemons');
|
|
312
|
+
const oldPidFiles = await readdirAsync(oldDaemonsDir).catch(() => []);
|
|
313
|
+
const staleOldDaemons: string[] = [];
|
|
314
|
+
for (const f of oldPidFiles) {
|
|
315
|
+
if (!f.endsWith('.pid')) continue;
|
|
316
|
+
const content = await readFile(join(oldDaemonsDir, f), 'utf-8').catch(() => null);
|
|
317
|
+
if (content) {
|
|
318
|
+
const oldPid = parseInt(content.trim(), 10);
|
|
319
|
+
try {
|
|
320
|
+
process.kill(oldPid, 0);
|
|
321
|
+
staleOldDaemons.push(`${f} (PID ${oldPid})`);
|
|
322
|
+
} catch {
|
|
323
|
+
// Dead process, just a stale file
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (staleOldDaemons.length > 0) {
|
|
328
|
+
console.log(`\nLegacy per-project daemons still running:`);
|
|
329
|
+
for (const d of staleOldDaemons) {
|
|
330
|
+
console.log(` ${d}`);
|
|
331
|
+
}
|
|
332
|
+
console.log(' Consider stopping these — the global daemon handles all projects.');
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
// Old daemons dir doesn't exist
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function handleLog(): Promise<void> {
|
|
340
|
+
try {
|
|
341
|
+
const content = await readFile(LOG_FILE, 'utf-8');
|
|
342
|
+
// Show last 50 lines
|
|
343
|
+
const lines = content.split('\n');
|
|
344
|
+
const tail = lines.slice(-50).join('\n');
|
|
345
|
+
console.log(tail);
|
|
346
|
+
} catch {
|
|
347
|
+
console.log('[mnemonik] No log file found at', LOG_FILE);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function main(): Promise<void> {
|
|
352
|
+
const cli = parseCliArgs();
|
|
353
|
+
|
|
354
|
+
switch (cli.command) {
|
|
355
|
+
case 'start':
|
|
356
|
+
await handleStart(cli);
|
|
357
|
+
break;
|
|
358
|
+
case 'stop':
|
|
359
|
+
await handleStop();
|
|
360
|
+
break;
|
|
361
|
+
case 'status':
|
|
362
|
+
await handleStatus();
|
|
363
|
+
break;
|
|
364
|
+
case 'log':
|
|
365
|
+
await handleLog();
|
|
366
|
+
break;
|
|
367
|
+
case 'help':
|
|
368
|
+
default:
|
|
369
|
+
printHelp();
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
159
374
|
main();
|
package/src/watcher.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { watch, type FSWatcher } from 'fs';
|
|
2
2
|
import { join, relative } from 'path';
|
|
3
|
-
import { readdir
|
|
3
|
+
import { readdir } from 'fs/promises';
|
|
4
4
|
|
|
5
5
|
const SKIP_DIRS = new Set([
|
|
6
6
|
'node_modules',
|
|
@@ -23,19 +23,23 @@ const SKIP_DIRS = new Set([
|
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
25
|
export type ChangeHandler = (changedFiles: string[]) => void;
|
|
26
|
+
export type ErrorHandler = (err: Error) => void;
|
|
26
27
|
|
|
27
28
|
export class FileWatcher {
|
|
28
29
|
private watchers: FSWatcher[] = [];
|
|
29
30
|
private pendingFiles = new Set<string>();
|
|
30
31
|
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
31
32
|
private debounceMs: number;
|
|
33
|
+
private onError: ErrorHandler | undefined;
|
|
32
34
|
|
|
33
35
|
constructor(
|
|
34
36
|
private rootPath: string,
|
|
35
37
|
private onChange: ChangeHandler,
|
|
36
|
-
debounceMs = 500
|
|
38
|
+
debounceMs = 500,
|
|
39
|
+
onError?: ErrorHandler
|
|
37
40
|
) {
|
|
38
41
|
this.debounceMs = debounceMs;
|
|
42
|
+
this.onError = onError;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
async start(): Promise<void> {
|
|
@@ -79,6 +83,13 @@ export class FileWatcher {
|
|
|
79
83
|
this.scheduleFlush();
|
|
80
84
|
});
|
|
81
85
|
|
|
86
|
+
watcher.on('error', (err) => {
|
|
87
|
+
console.warn(`[scanner] Watcher error for ${dir}:`, err.message);
|
|
88
|
+
if (dir === this.rootPath && this.onError) {
|
|
89
|
+
this.onError(err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
82
93
|
this.watchers.push(watcher);
|
|
83
94
|
|
|
84
95
|
const entries = await readdir(dir, { withFileTypes: true });
|