@karmaniverous/jeeves-watcher 0.2.3 → 0.2.5

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.
@@ -3,7 +3,7 @@
3
3
  import { Command } from '@commander-js/extra-typings';
4
4
  import Fastify from 'fastify';
5
5
  import { readdir, stat, rm, readFile, mkdir, writeFile } from 'node:fs/promises';
6
- import { resolve, dirname, join, extname, basename } from 'node:path';
6
+ import { resolve, dirname, join, relative, extname, basename } from 'node:path';
7
7
  import picomatch from 'picomatch';
8
8
  import { omit, get } from 'radash';
9
9
  import { createHash } from 'node:crypto';
@@ -11,6 +11,8 @@ import { cosmiconfig } from 'cosmiconfig';
11
11
  import { z, ZodError } from 'zod';
12
12
  import { jsonMapMapSchema, JsonMap } from '@karmaniverous/jsonmap';
13
13
  import { GoogleGenerativeAIEmbeddings } from '@langchain/google-genai';
14
+ import { existsSync, statSync, readdirSync, readFileSync } from 'node:fs';
15
+ import ignore from 'ignore';
14
16
  import pino from 'pino';
15
17
  import { v5 } from 'uuid';
16
18
  import * as cheerio from 'cheerio';
@@ -436,6 +438,7 @@ const WATCH_DEFAULTS = {
436
438
  stabilityThresholdMs: 500,
437
439
  usePolling: false,
438
440
  pollIntervalMs: 1000,
441
+ respectGitignore: true,
439
442
  };
440
443
  /** Default embedding configuration. */
441
444
  const EMBEDDING_DEFAULTS = {
@@ -501,6 +504,11 @@ const watchConfigSchema = z.object({
501
504
  .number()
502
505
  .optional()
503
506
  .describe('Time in milliseconds a file must remain unchanged before processing.'),
507
+ /** Whether to respect .gitignore files when processing. */
508
+ respectGitignore: z
509
+ .boolean()
510
+ .optional()
511
+ .describe('Skip files ignored by .gitignore in git repositories. Only applies to repos with a .git directory. Default: true.'),
504
512
  });
505
513
  /**
506
514
  * Configuration watch settings.
@@ -668,6 +676,16 @@ const jeevesWatcherConfigSchema = z.object({
668
676
  .number()
669
677
  .optional()
670
678
  .describe('Timeout in milliseconds for graceful shutdown.'),
679
+ /** Maximum consecutive system-level failures before triggering fatal error. Default: Infinity. */
680
+ maxRetries: z
681
+ .number()
682
+ .optional()
683
+ .describe('Maximum consecutive system-level failures before triggering fatal error. Default: Infinity.'),
684
+ /** Maximum backoff delay in milliseconds for system errors. Default: 60000. */
685
+ maxBackoffMs: z
686
+ .number()
687
+ .optional()
688
+ .describe('Maximum backoff delay in milliseconds for system errors. Default: 60000.'),
671
689
  });
672
690
 
673
691
  /**
@@ -956,6 +974,212 @@ function createEmbeddingProvider(config, logger) {
956
974
  return factory(config, logger);
957
975
  }
958
976
 
977
+ /**
978
+ * @module gitignore
979
+ * Processor-level gitignore filtering. Scans watched paths for `.gitignore` files in git repos, caches parsed patterns, and exposes `isIgnored()` for path checking.
980
+ */
981
+ /**
982
+ * Find the git repo root by walking up from `startDir` looking for `.git/`.
983
+ * Returns `undefined` if no repo is found.
984
+ */
985
+ function findRepoRoot(startDir) {
986
+ let dir = resolve(startDir);
987
+ const root = resolve('/');
988
+ while (dir !== root) {
989
+ if (existsSync(join(dir, '.git')) &&
990
+ statSync(join(dir, '.git')).isDirectory()) {
991
+ return dir;
992
+ }
993
+ const parent = dirname(dir);
994
+ if (parent === dir)
995
+ break;
996
+ dir = parent;
997
+ }
998
+ return undefined;
999
+ }
1000
+ /**
1001
+ * Convert a watch path (directory, file path, or glob) to a concrete directory
1002
+ * that can be scanned for a repo root.
1003
+ */
1004
+ function watchPathToScanDir(watchPath) {
1005
+ const absPath = resolve(watchPath);
1006
+ try {
1007
+ return statSync(absPath).isDirectory() ? absPath : dirname(absPath);
1008
+ }
1009
+ catch {
1010
+ // ignore
1011
+ }
1012
+ // If this is a glob, fall back to the non-glob prefix.
1013
+ const globMatch = /[*?[{]/.exec(watchPath);
1014
+ if (!globMatch)
1015
+ return undefined;
1016
+ const prefix = watchPath.slice(0, globMatch.index);
1017
+ const trimmed = prefix.trim();
1018
+ const baseDir = trimmed.length === 0
1019
+ ? '.'
1020
+ : trimmed.endsWith('/') || trimmed.endsWith('\\')
1021
+ ? trimmed
1022
+ : dirname(trimmed);
1023
+ const resolved = resolve(baseDir);
1024
+ if (!existsSync(resolved))
1025
+ return undefined;
1026
+ return resolved;
1027
+ }
1028
+ /**
1029
+ * Recursively find all `.gitignore` files under `dir`.
1030
+ * Skips `.git` and `node_modules` directories for performance.
1031
+ */
1032
+ function findGitignoreFiles(dir) {
1033
+ const results = [];
1034
+ const gitignorePath = join(dir, '.gitignore');
1035
+ if (existsSync(gitignorePath)) {
1036
+ results.push(gitignorePath);
1037
+ }
1038
+ let entries;
1039
+ try {
1040
+ entries = readdirSync(dir);
1041
+ }
1042
+ catch {
1043
+ return results;
1044
+ }
1045
+ for (const entry of entries) {
1046
+ if (entry === '.git' || entry === 'node_modules')
1047
+ continue;
1048
+ const fullPath = join(dir, entry);
1049
+ try {
1050
+ if (statSync(fullPath).isDirectory()) {
1051
+ results.push(...findGitignoreFiles(fullPath));
1052
+ }
1053
+ }
1054
+ catch {
1055
+ // Skip inaccessible entries
1056
+ }
1057
+ }
1058
+ return results;
1059
+ }
1060
+ /**
1061
+ * Parse a `.gitignore` file into an `ignore` instance.
1062
+ */
1063
+ function parseGitignore(gitignorePath) {
1064
+ const content = readFileSync(gitignorePath, 'utf8');
1065
+ return ignore().add(content);
1066
+ }
1067
+ /**
1068
+ * Normalize a path to use forward slashes (required by `ignore` package).
1069
+ */
1070
+ function toForwardSlash(p) {
1071
+ return p.replace(/\\/g, '/');
1072
+ }
1073
+ /**
1074
+ * Processor-level gitignore filter. Checks file paths against the nearest
1075
+ * `.gitignore` chain in git repositories.
1076
+ */
1077
+ class GitignoreFilter {
1078
+ repos = new Map();
1079
+ /**
1080
+ * Create a GitignoreFilter by scanning watched paths for `.gitignore` files.
1081
+ *
1082
+ * @param watchPaths - Absolute paths being watched (directories or globs resolved to roots).
1083
+ */
1084
+ constructor(watchPaths) {
1085
+ this.scan(watchPaths);
1086
+ }
1087
+ /**
1088
+ * Scan paths for git repos and their `.gitignore` files.
1089
+ */
1090
+ scan(watchPaths) {
1091
+ this.repos.clear();
1092
+ const scannedDirs = new Set();
1093
+ for (const watchPath of watchPaths) {
1094
+ const scanDir = watchPathToScanDir(watchPath);
1095
+ if (!scanDir)
1096
+ continue;
1097
+ if (scannedDirs.has(scanDir))
1098
+ continue;
1099
+ scannedDirs.add(scanDir);
1100
+ const repoRoot = findRepoRoot(scanDir);
1101
+ if (!repoRoot)
1102
+ continue;
1103
+ if (this.repos.has(repoRoot))
1104
+ continue;
1105
+ const gitignoreFiles = findGitignoreFiles(repoRoot);
1106
+ const entries = gitignoreFiles.map((gf) => ({
1107
+ dir: dirname(gf),
1108
+ ig: parseGitignore(gf),
1109
+ }));
1110
+ // Sort deepest-first so nested `.gitignore` files are checked first
1111
+ entries.sort((a, b) => b.dir.length - a.dir.length);
1112
+ this.repos.set(repoRoot, { root: repoRoot, entries });
1113
+ }
1114
+ }
1115
+ /**
1116
+ * Check whether a file path is ignored by any applicable `.gitignore`.
1117
+ *
1118
+ * @param filePath - Absolute file path to check.
1119
+ * @returns `true` if the file should be ignored.
1120
+ */
1121
+ isIgnored(filePath) {
1122
+ const absPath = resolve(filePath);
1123
+ for (const [, repo] of this.repos) {
1124
+ // Check if file is within this repo
1125
+ const relToRepo = relative(repo.root, absPath);
1126
+ if (relToRepo.startsWith('..') || relToRepo.startsWith(resolve('/'))) {
1127
+ continue;
1128
+ }
1129
+ // Check each `.gitignore` entry (deepest-first)
1130
+ for (const entry of repo.entries) {
1131
+ const relToEntry = relative(entry.dir, absPath);
1132
+ if (relToEntry.startsWith('..'))
1133
+ continue;
1134
+ const normalized = toForwardSlash(relToEntry);
1135
+ if (entry.ig.ignores(normalized)) {
1136
+ return true;
1137
+ }
1138
+ }
1139
+ }
1140
+ return false;
1141
+ }
1142
+ /**
1143
+ * Invalidate and re-parse a specific `.gitignore` file.
1144
+ * Call when a `.gitignore` file is added, changed, or removed.
1145
+ *
1146
+ * @param gitignorePath - Absolute path to the `.gitignore` file that changed.
1147
+ */
1148
+ invalidate(gitignorePath) {
1149
+ const absPath = resolve(gitignorePath);
1150
+ const gitignoreDir = dirname(absPath);
1151
+ for (const [, repo] of this.repos) {
1152
+ const relToRepo = relative(repo.root, gitignoreDir);
1153
+ if (relToRepo.startsWith('..'))
1154
+ continue;
1155
+ // Remove old entry for this directory
1156
+ repo.entries = repo.entries.filter((e) => e.dir !== gitignoreDir);
1157
+ // Re-parse if file still exists
1158
+ if (existsSync(absPath)) {
1159
+ repo.entries.push({ dir: gitignoreDir, ig: parseGitignore(absPath) });
1160
+ // Re-sort deepest-first
1161
+ repo.entries.sort((a, b) => b.dir.length - a.dir.length);
1162
+ }
1163
+ return;
1164
+ }
1165
+ // If not in any known repo, check if it's in a repo we haven't scanned
1166
+ const repoRoot = findRepoRoot(gitignoreDir);
1167
+ if (repoRoot && existsSync(absPath)) {
1168
+ const entries = [
1169
+ { dir: gitignoreDir, ig: parseGitignore(absPath) },
1170
+ ];
1171
+ if (this.repos.has(repoRoot)) {
1172
+ const repo = this.repos.get(repoRoot);
1173
+ repo.entries.push(entries[0]);
1174
+ repo.entries.sort((a, b) => b.dir.length - a.dir.length);
1175
+ }
1176
+ else {
1177
+ this.repos.set(repoRoot, { root: repoRoot, entries });
1178
+ }
1179
+ }
1180
+ }
1181
+ }
1182
+
959
1183
  /**
960
1184
  * @module logger
961
1185
  * Creates pino logger instances. I/O: optionally writes logs to file via pino/file transport. Defaults to stdout at info level.
@@ -1072,11 +1296,11 @@ async function extractMarkdown(filePath) {
1072
1296
  }
1073
1297
  async function extractPlaintext(filePath) {
1074
1298
  const raw = await readFile(filePath, 'utf8');
1075
- return { text: raw };
1299
+ return { text: raw.replace(/^\uFEFF/, '') };
1076
1300
  }
1077
1301
  async function extractJson(filePath) {
1078
1302
  const raw = await readFile(filePath, 'utf8');
1079
- const parsed = JSON.parse(raw);
1303
+ const parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
1080
1304
  const json = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
1081
1305
  ? parsed
1082
1306
  : undefined;
@@ -1098,7 +1322,7 @@ async function extractDocx(filePath) {
1098
1322
  }
1099
1323
  async function extractHtml(filePath) {
1100
1324
  const raw = await readFile(filePath, 'utf8');
1101
- const $ = cheerio.load(raw);
1325
+ const $ = cheerio.load(raw.replace(/^\uFEFF/, ''));
1102
1326
  $('script, style').remove();
1103
1327
  const text = $('body').text().trim() || $.text().trim();
1104
1328
  return { text };
@@ -1928,6 +2152,112 @@ class VectorStoreClient {
1928
2152
  }
1929
2153
  }
1930
2154
 
2155
+ /**
2156
+ * @module health
2157
+ * Tracks consecutive system-level failures and applies exponential backoff.
2158
+ * Triggers fatal error callback when maxRetries is exceeded.
2159
+ */
2160
+ /**
2161
+ * Tracks system health via consecutive failure count and exponential backoff.
2162
+ */
2163
+ class SystemHealth {
2164
+ consecutiveFailures = 0;
2165
+ maxRetries;
2166
+ maxBackoffMs;
2167
+ baseDelayMs;
2168
+ onFatalError;
2169
+ logger;
2170
+ constructor(options) {
2171
+ this.maxRetries = options.maxRetries ?? Number.POSITIVE_INFINITY;
2172
+ this.maxBackoffMs = options.maxBackoffMs ?? 60_000;
2173
+ this.baseDelayMs = options.baseDelayMs ?? 1000;
2174
+ this.onFatalError = options.onFatalError;
2175
+ this.logger = options.logger;
2176
+ }
2177
+ /**
2178
+ * Record a successful system operation. Resets the failure counter.
2179
+ */
2180
+ recordSuccess() {
2181
+ if (this.consecutiveFailures > 0) {
2182
+ this.logger.info({ previousFailures: this.consecutiveFailures }, 'System health recovered');
2183
+ }
2184
+ this.consecutiveFailures = 0;
2185
+ }
2186
+ /**
2187
+ * Record a system-level failure. If maxRetries is exceeded, triggers fatal error.
2188
+ *
2189
+ * @param error - The error that occurred.
2190
+ * @returns Whether the watcher should continue (false = fatal).
2191
+ */
2192
+ recordFailure(error) {
2193
+ this.consecutiveFailures += 1;
2194
+ this.logger.error({
2195
+ consecutiveFailures: this.consecutiveFailures,
2196
+ maxRetries: this.maxRetries,
2197
+ err: normalizeError(error),
2198
+ }, 'System-level failure recorded');
2199
+ if (this.consecutiveFailures >= this.maxRetries) {
2200
+ this.logger.fatal({ consecutiveFailures: this.consecutiveFailures }, 'Maximum retries exceeded, triggering fatal error');
2201
+ if (this.onFatalError) {
2202
+ this.onFatalError(error);
2203
+ return false;
2204
+ }
2205
+ throw error instanceof Error
2206
+ ? error
2207
+ : new Error(`Fatal system error: ${String(error)}`);
2208
+ }
2209
+ return true;
2210
+ }
2211
+ /**
2212
+ * Compute the current backoff delay based on consecutive failures.
2213
+ *
2214
+ * @returns Delay in milliseconds.
2215
+ */
2216
+ get currentBackoffMs() {
2217
+ if (this.consecutiveFailures === 0)
2218
+ return 0;
2219
+ const exp = Math.max(0, this.consecutiveFailures - 1);
2220
+ return Math.min(this.maxBackoffMs, this.baseDelayMs * 2 ** exp);
2221
+ }
2222
+ /**
2223
+ * Sleep for the current backoff duration.
2224
+ *
2225
+ * @param signal - Optional abort signal.
2226
+ */
2227
+ async backoff(signal) {
2228
+ const delay = this.currentBackoffMs;
2229
+ if (delay <= 0)
2230
+ return;
2231
+ this.logger.warn({ delayMs: delay, consecutiveFailures: this.consecutiveFailures }, 'Backing off before next attempt');
2232
+ await new Promise((resolve, reject) => {
2233
+ const timer = setTimeout(() => {
2234
+ cleanup();
2235
+ resolve();
2236
+ }, delay);
2237
+ const onAbort = () => {
2238
+ cleanup();
2239
+ reject(new Error('Backoff aborted'));
2240
+ };
2241
+ const cleanup = () => {
2242
+ clearTimeout(timer);
2243
+ if (signal)
2244
+ signal.removeEventListener('abort', onAbort);
2245
+ };
2246
+ if (signal) {
2247
+ if (signal.aborted) {
2248
+ onAbort();
2249
+ return;
2250
+ }
2251
+ signal.addEventListener('abort', onAbort, { once: true });
2252
+ }
2253
+ });
2254
+ }
2255
+ /** Current consecutive failure count. */
2256
+ get failures() {
2257
+ return this.consecutiveFailures;
2258
+ }
2259
+ }
2260
+
1931
2261
  /**
1932
2262
  * @module watcher
1933
2263
  * Filesystem watcher wrapping chokidar. I/O: watches files/directories for add/change/unlink events, enqueues to processing queue.
@@ -1940,6 +2270,8 @@ class FileSystemWatcher {
1940
2270
  queue;
1941
2271
  processor;
1942
2272
  logger;
2273
+ health;
2274
+ gitignoreFilter;
1943
2275
  watcher;
1944
2276
  /**
1945
2277
  * Create a new FileSystemWatcher.
@@ -1948,12 +2280,21 @@ class FileSystemWatcher {
1948
2280
  * @param queue - The event queue.
1949
2281
  * @param processor - The document processor.
1950
2282
  * @param logger - The logger instance.
2283
+ * @param options - Optional health/fatal error options.
1951
2284
  */
1952
- constructor(config, queue, processor, logger) {
2285
+ constructor(config, queue, processor, logger, options = {}) {
1953
2286
  this.config = config;
1954
2287
  this.queue = queue;
1955
2288
  this.processor = processor;
1956
2289
  this.logger = logger;
2290
+ this.gitignoreFilter = options.gitignoreFilter;
2291
+ const healthOptions = {
2292
+ maxRetries: options.maxRetries,
2293
+ maxBackoffMs: options.maxBackoffMs,
2294
+ onFatalError: options.onFatalError,
2295
+ logger,
2296
+ };
2297
+ this.health = new SystemHealth(healthOptions);
1957
2298
  }
1958
2299
  /**
1959
2300
  * Start watching the filesystem and processing events.
@@ -1969,19 +2310,29 @@ class FileSystemWatcher {
1969
2310
  ignoreInitial: false,
1970
2311
  });
1971
2312
  this.watcher.on('add', (path) => {
2313
+ this.handleGitignoreChange(path);
2314
+ if (this.isGitignored(path))
2315
+ return;
1972
2316
  this.logger.debug({ path }, 'File added');
1973
- this.queue.enqueue({ type: 'create', path, priority: 'normal' }, () => this.processor.processFile(path));
2317
+ this.queue.enqueue({ type: 'create', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.processFile(path)));
1974
2318
  });
1975
2319
  this.watcher.on('change', (path) => {
2320
+ this.handleGitignoreChange(path);
2321
+ if (this.isGitignored(path))
2322
+ return;
1976
2323
  this.logger.debug({ path }, 'File changed');
1977
- this.queue.enqueue({ type: 'modify', path, priority: 'normal' }, () => this.processor.processFile(path));
2324
+ this.queue.enqueue({ type: 'modify', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.processFile(path)));
1978
2325
  });
1979
2326
  this.watcher.on('unlink', (path) => {
2327
+ this.handleGitignoreChange(path);
2328
+ if (this.isGitignored(path))
2329
+ return;
1980
2330
  this.logger.debug({ path }, 'File removed');
1981
- this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.processor.deleteFile(path));
2331
+ this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.deleteFile(path)));
1982
2332
  });
1983
2333
  this.watcher.on('error', (error) => {
1984
2334
  this.logger.error({ err: normalizeError(error) }, 'Watcher error');
2335
+ this.health.recordFailure(error);
1985
2336
  });
1986
2337
  this.queue.process();
1987
2338
  this.logger.info({ paths: this.config.paths }, 'Filesystem watcher started');
@@ -1996,6 +2347,53 @@ class FileSystemWatcher {
1996
2347
  this.logger.info('Filesystem watcher stopped');
1997
2348
  }
1998
2349
  }
2350
+ /**
2351
+ * Get the system health tracker.
2352
+ */
2353
+ get systemHealth() {
2354
+ return this.health;
2355
+ }
2356
+ /**
2357
+ * Check if a path is gitignored and should be skipped.
2358
+ */
2359
+ isGitignored(path) {
2360
+ if (!this.gitignoreFilter)
2361
+ return false;
2362
+ const ignored = this.gitignoreFilter.isIgnored(path);
2363
+ if (ignored) {
2364
+ this.logger.debug({ path }, 'Skipping gitignored file');
2365
+ }
2366
+ return ignored;
2367
+ }
2368
+ /**
2369
+ * If the changed file is a `.gitignore`, invalidate the filter cache.
2370
+ */
2371
+ handleGitignoreChange(path) {
2372
+ if (!this.gitignoreFilter)
2373
+ return;
2374
+ if (path.endsWith('.gitignore')) {
2375
+ this.logger.info({ path }, 'Gitignore file changed, refreshing filter');
2376
+ this.gitignoreFilter.invalidate(path);
2377
+ }
2378
+ }
2379
+ /**
2380
+ * Wrap a processing operation with health tracking.
2381
+ * On success, resets the failure counter.
2382
+ * On failure, records the failure and applies backoff.
2383
+ */
2384
+ async wrapProcessing(fn) {
2385
+ try {
2386
+ await this.health.backoff();
2387
+ await fn();
2388
+ this.health.recordSuccess();
2389
+ }
2390
+ catch (error) {
2391
+ const shouldContinue = this.health.recordFailure(error);
2392
+ if (!shouldContinue) {
2393
+ await this.stop();
2394
+ }
2395
+ }
2396
+ }
1999
2397
  }
2000
2398
 
2001
2399
  /**
@@ -2071,7 +2469,7 @@ const defaultFactories = {
2071
2469
  compileRules,
2072
2470
  createDocumentProcessor: (config, embeddingProvider, vectorStore, compiledRules, logger) => new DocumentProcessor(config, embeddingProvider, vectorStore, compiledRules, logger),
2073
2471
  createEventQueue: (options) => new EventQueue(options),
2074
- createFileSystemWatcher: (config, queue, processor, logger) => new FileSystemWatcher(config, queue, processor, logger),
2472
+ createFileSystemWatcher: (config, queue, processor, logger, options) => new FileSystemWatcher(config, queue, processor, logger, options),
2075
2473
  createApiServer,
2076
2474
  };
2077
2475
  /**
@@ -2081,6 +2479,7 @@ class JeevesWatcher {
2081
2479
  config;
2082
2480
  configPath;
2083
2481
  factories;
2482
+ runtimeOptions;
2084
2483
  logger;
2085
2484
  watcher;
2086
2485
  queue;
@@ -2093,11 +2492,13 @@ class JeevesWatcher {
2093
2492
  * @param config - The application configuration.
2094
2493
  * @param configPath - Optional config file path to watch for changes.
2095
2494
  * @param factories - Optional component factories (for dependency injection).
2495
+ * @param runtimeOptions - Optional runtime-only options (e.g., onFatalError).
2096
2496
  */
2097
- constructor(config, configPath, factories = {}) {
2497
+ constructor(config, configPath, factories = {}, runtimeOptions = {}) {
2098
2498
  this.config = config;
2099
2499
  this.configPath = configPath;
2100
2500
  this.factories = { ...defaultFactories, ...factories };
2501
+ this.runtimeOptions = runtimeOptions;
2101
2502
  }
2102
2503
  /**
2103
2504
  * Start the watcher, API server, and all components.
@@ -2130,7 +2531,16 @@ class JeevesWatcher {
2130
2531
  rateLimitPerMinute: this.config.embedding.rateLimitPerMinute,
2131
2532
  });
2132
2533
  this.queue = queue;
2133
- const watcher = this.factories.createFileSystemWatcher(this.config.watch, queue, processor, logger);
2534
+ const respectGitignore = this.config.watch.respectGitignore ?? true;
2535
+ const gitignoreFilter = respectGitignore
2536
+ ? new GitignoreFilter(this.config.watch.paths)
2537
+ : undefined;
2538
+ const watcher = this.factories.createFileSystemWatcher(this.config.watch, queue, processor, logger, {
2539
+ maxRetries: this.config.maxRetries,
2540
+ maxBackoffMs: this.config.maxBackoffMs,
2541
+ onFatalError: this.runtimeOptions.onFatalError,
2542
+ gitignoreFilter,
2543
+ });
2134
2544
  this.watcher = watcher;
2135
2545
  const server = this.factories.createApiServer({
2136
2546
  processor,