@karmaniverous/jeeves-watcher 0.18.7 → 0.18.9
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/config.schema.json +228 -180
- package/dist/cli/jeeves-watcher/{index-CS1wrtes.js → index-xP2XrV4W.js} +347 -305
- package/dist/cli/jeeves-watcher/index.js +148 -23
- package/dist/index.d.ts +42 -27
- package/dist/index.js +487 -322
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, createConfigApplyHandler, DEFAULT_PORTS, getBindAddress } from '@karmaniverous/jeeves';
|
|
3
|
-
import {
|
|
4
|
-
export {
|
|
3
|
+
import { d as normalizeSlashes, n as normalizeError, e as execFileAsync, r as retry, b as gitAddViaStdin, c as normalizePathCase, f as findRootForPath, G as GitignoreFilter, a as executeReindex, V as VALID_REINDEX_SCOPES, m as mergeInferenceRules, i as isPathWatched, g as getWatchRootBases, C as CONFIG_WATCH_VALID_SCOPES, s as sleep, l as loadConfig } from './index.js';
|
|
4
|
+
export { h as startFromConfig } from './index.js';
|
|
5
5
|
import { mkdirSync, existsSync, readFileSync, writeFileSync, rmSync, renameSync, readdirSync, statSync } from 'node:fs';
|
|
6
6
|
import { join, dirname, resolve, extname, basename, relative, sep, isAbsolute } from 'node:path';
|
|
7
7
|
import Database from 'better-sqlite3';
|
|
@@ -10,11 +10,9 @@ import Ajv from 'ajv';
|
|
|
10
10
|
import addFormats from 'ajv-formats';
|
|
11
11
|
import picomatch from 'picomatch';
|
|
12
12
|
import { deserialize, serialize } from 'node:v8';
|
|
13
|
-
import { normalizeWatchPaths, extractWatchPathStrings, vcsAuthorConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema } from '@karmaniverous/jeeves-watcher-core';
|
|
14
|
-
import { execFile } from 'node:child_process';
|
|
15
|
-
import { promisify } from 'node:util';
|
|
13
|
+
import { normalizeWatchPaths, vcsRetentionConfigSchema, extractWatchPathStrings, vcsAuthorConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema } from '@karmaniverous/jeeves-watcher-core';
|
|
16
14
|
import { homedir } from 'node:os';
|
|
17
|
-
import { access, rm, readFile, writeFile, appendFile, mkdir
|
|
15
|
+
import { access, rm, stat, readFile, writeFile, appendFile, mkdir } from 'node:fs/promises';
|
|
18
16
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
19
17
|
import { packageDirectorySync } from 'package-directory';
|
|
20
18
|
import { JsonMap } from '@karmaniverous/jsonmap';
|
|
@@ -27,6 +25,7 @@ import { capitalize, title, camel, snake, dash, isEqual, get, omit } from 'radas
|
|
|
27
25
|
import rehypeParse from 'rehype-parse';
|
|
28
26
|
import { unified } from 'unified';
|
|
29
27
|
import yaml from 'js-yaml';
|
|
28
|
+
import 'ignore';
|
|
30
29
|
import chokidar from 'chokidar';
|
|
31
30
|
import Fastify from 'fastify';
|
|
32
31
|
import crypto from 'crypto';
|
|
@@ -39,7 +38,8 @@ import mammoth from 'mammoth';
|
|
|
39
38
|
import { MarkdownTextSplitter, RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
|
|
40
39
|
import { QdrantClient } from '@qdrant/js-client-rest';
|
|
41
40
|
import 'cosmiconfig';
|
|
42
|
-
import '
|
|
41
|
+
import 'node:child_process';
|
|
42
|
+
import 'node:util';
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* @module api/InitialScanTracker
|
|
@@ -928,77 +928,6 @@ class CommitMessageGenerator {
|
|
|
928
928
|
}
|
|
929
929
|
}
|
|
930
930
|
|
|
931
|
-
/**
|
|
932
|
-
* @module vcs/gitExec
|
|
933
|
-
* Shared git execution utilities.
|
|
934
|
-
*/
|
|
935
|
-
/** Promisified execFile for git commands. */
|
|
936
|
-
const execFileAsync = promisify(execFile);
|
|
937
|
-
/**
|
|
938
|
-
* Stage files via stdin to avoid ENAMETOOLONG on Windows.
|
|
939
|
-
*
|
|
940
|
-
* Uses `git add --pathspec-from-file=- --pathspec-file-nul` so the file list is piped
|
|
941
|
-
* through stdin (NUL-delimited) instead of passed as command-line
|
|
942
|
-
* arguments. This sidesteps the Windows CreateProcessW 32 767-char
|
|
943
|
-
* limit that triggers ENAMETOOLONG when batches contain many files
|
|
944
|
-
* with long absolute paths.
|
|
945
|
-
*
|
|
946
|
-
* @param files - Absolute paths of files to stage.
|
|
947
|
-
* @param cwd - Repository root (working directory for git).
|
|
948
|
-
*/
|
|
949
|
-
function gitAddViaStdin(files, cwd) {
|
|
950
|
-
return new Promise((resolve, reject) => {
|
|
951
|
-
const child = execFile('git', ['add', '--pathspec-from-file=-', '--pathspec-file-nul'], { cwd }, (error) => {
|
|
952
|
-
if (error)
|
|
953
|
-
reject(error);
|
|
954
|
-
else
|
|
955
|
-
resolve();
|
|
956
|
-
});
|
|
957
|
-
if (!child.stdin) {
|
|
958
|
-
reject(new Error('Failed to open stdin for git add'));
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
// Suppress EPIPE — expected if git exits before we finish writing
|
|
962
|
-
child.stdin.on('error', (err) => {
|
|
963
|
-
if (err.code !== 'EPIPE') {
|
|
964
|
-
reject(err);
|
|
965
|
-
}
|
|
966
|
-
});
|
|
967
|
-
child.stdin.end(files.join('\0'));
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
/**
|
|
971
|
-
* Normalize a path for case-insensitive comparison on Windows.
|
|
972
|
-
* On Windows, lowercases the entire path; on other platforms, returns as-is.
|
|
973
|
-
*
|
|
974
|
-
* @param p - The path string to normalize.
|
|
975
|
-
* @param platform - The platform to check against (default: `process.platform`).
|
|
976
|
-
* @returns The normalized path.
|
|
977
|
-
*/
|
|
978
|
-
function normalizePathCase(p, platform = process.platform) {
|
|
979
|
-
return platform === 'win32' ? p.toLowerCase() : p;
|
|
980
|
-
}
|
|
981
|
-
/**
|
|
982
|
-
* Find the longest-prefix-match root for a normalized path.
|
|
983
|
-
* Roots must be sorted longest-first for correct nested matching.
|
|
984
|
-
*
|
|
985
|
-
* @param roots - Root paths sorted longest-first.
|
|
986
|
-
* @param normalizedPath - Normalized absolute path (forward slashes).
|
|
987
|
-
* @param platform - The platform to check against (default: `process.platform`).
|
|
988
|
-
* @returns The matching root, or undefined.
|
|
989
|
-
*/
|
|
990
|
-
function findRootForPath(roots, normalizedPath, platform = process.platform) {
|
|
991
|
-
const comparePath = normalizePathCase(normalizedPath, platform);
|
|
992
|
-
for (const root of roots) {
|
|
993
|
-
const compareRoot = normalizePathCase(root, platform);
|
|
994
|
-
if (comparePath === compareRoot ||
|
|
995
|
-
comparePath.startsWith(compareRoot + '/')) {
|
|
996
|
-
return root;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
return undefined;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
931
|
/**
|
|
1003
932
|
* @module vcs/resolveCommitMessageApiKey
|
|
1004
933
|
* Resolves the API key for AI commit message generation.
|
|
@@ -1029,6 +958,91 @@ function resolveCommitMessageApiKey(provider, configApiKey, logger) {
|
|
|
1029
958
|
return undefined;
|
|
1030
959
|
}
|
|
1031
960
|
|
|
961
|
+
/**
|
|
962
|
+
* @module vcs/CommitMessageBuilder
|
|
963
|
+
* Builds commit messages — both template-based and AI-generated.
|
|
964
|
+
*/
|
|
965
|
+
/**
|
|
966
|
+
* Builds commit messages for VCS commits.
|
|
967
|
+
* Supports template-based messages and AI-generated messages with fallback.
|
|
968
|
+
*/
|
|
969
|
+
class CommitMessageBuilder {
|
|
970
|
+
rootPath;
|
|
971
|
+
logger;
|
|
972
|
+
generator;
|
|
973
|
+
constructor(rootPath, logger, generator) {
|
|
974
|
+
this.rootPath = rootPath;
|
|
975
|
+
this.logger = logger;
|
|
976
|
+
this.generator = generator;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Build the template commit message (no AI).
|
|
980
|
+
*/
|
|
981
|
+
buildTemplateMessage(fileCount, isBaseline, pendingReversions) {
|
|
982
|
+
if (pendingReversions.length === 0) {
|
|
983
|
+
if (isBaseline) {
|
|
984
|
+
return `baseline: batch (${String(fileCount)} files)`;
|
|
985
|
+
}
|
|
986
|
+
const timestamp = new Date().toISOString();
|
|
987
|
+
return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
|
|
988
|
+
}
|
|
989
|
+
// Count total reverted files across all pending reversions
|
|
990
|
+
const revertedPaths = new Set(pendingReversions.flatMap((r) => r.paths));
|
|
991
|
+
const revertedCount = revertedPaths.size;
|
|
992
|
+
const otherCount = fileCount - revertedCount;
|
|
993
|
+
// Use the first reversion's glob and commit for the message
|
|
994
|
+
const { glob, commit } = pendingReversions[0];
|
|
995
|
+
const shortCommit = commit.slice(0, 7);
|
|
996
|
+
let message = `revert: ${glob} to ${shortCommit} — restored ${String(revertedCount)} files`;
|
|
997
|
+
if (otherCount > 0) {
|
|
998
|
+
message += ` (+ ${String(otherCount)} other changes)`;
|
|
999
|
+
}
|
|
1000
|
+
return message;
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Build the commit message, using AI when available with template fallback.
|
|
1004
|
+
*/
|
|
1005
|
+
async buildCommitMessage(files, isBaseline, pendingReversions) {
|
|
1006
|
+
const fileCount = files.length;
|
|
1007
|
+
const templateMessage = this.buildTemplateMessage(fileCount, isBaseline, pendingReversions);
|
|
1008
|
+
if (!this.generator) {
|
|
1009
|
+
return templateMessage;
|
|
1010
|
+
}
|
|
1011
|
+
try {
|
|
1012
|
+
const diffSummary = await this.getStagedDiff();
|
|
1013
|
+
const aiMessage = await this.generator.generate(files, diffSummary);
|
|
1014
|
+
if (!aiMessage) {
|
|
1015
|
+
this.logger.warn('AI commit message generation returned null; using template');
|
|
1016
|
+
return templateMessage;
|
|
1017
|
+
}
|
|
1018
|
+
// For reversions: prefix with revert info + AI description
|
|
1019
|
+
if (pendingReversions.length > 0) {
|
|
1020
|
+
const { glob, commit } = pendingReversions[0];
|
|
1021
|
+
const shortCommit = commit.slice(0, 7);
|
|
1022
|
+
return `revert: ${glob} to ${shortCommit} — ${aiMessage}`;
|
|
1023
|
+
}
|
|
1024
|
+
return aiMessage;
|
|
1025
|
+
}
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
this.logger.warn({ err: normalizeError(error) }, 'AI commit message generation failed; using template');
|
|
1028
|
+
return templateMessage;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Get the staged diff for AI context.
|
|
1033
|
+
*/
|
|
1034
|
+
async getStagedDiff() {
|
|
1035
|
+
try {
|
|
1036
|
+
const { stdout: stat } = await execFileAsync('git', ['diff', '--cached', '--stat'], { cwd: this.rootPath });
|
|
1037
|
+
const { stdout: patch } = await execFileAsync('git', ['diff', '--cached'], { cwd: this.rootPath });
|
|
1038
|
+
return `${stat}\n${patch}`;
|
|
1039
|
+
}
|
|
1040
|
+
catch {
|
|
1041
|
+
return '';
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1032
1046
|
/**
|
|
1033
1047
|
* @module vcs/SquashManager
|
|
1034
1048
|
* Handles scheduled squash retention for git-backed VCS roots.
|
|
@@ -1323,6 +1337,50 @@ class SquashManager {
|
|
|
1323
1337
|
}
|
|
1324
1338
|
}
|
|
1325
1339
|
|
|
1340
|
+
/**
|
|
1341
|
+
* @module vcs/vcsPush
|
|
1342
|
+
* Push logic for VCS commits.
|
|
1343
|
+
*/
|
|
1344
|
+
/**
|
|
1345
|
+
* Push to the configured remote if remoteUrl is set.
|
|
1346
|
+
*
|
|
1347
|
+
* On failure: logs the error, appends to pushErrors, and returns undefined.
|
|
1348
|
+
* Never throws.
|
|
1349
|
+
*
|
|
1350
|
+
* @param rootPath - Absolute path of the git repository.
|
|
1351
|
+
* @param remoteUrl - Remote URL to push to, or undefined to skip.
|
|
1352
|
+
* @param accessToken - Optional token for HTTPS authentication.
|
|
1353
|
+
* @param pushErrors - Mutable array where push errors are recorded.
|
|
1354
|
+
* @param logger - Logger instance.
|
|
1355
|
+
* @returns ISO timestamp of a successful push, or undefined if skipped/failed.
|
|
1356
|
+
*/
|
|
1357
|
+
async function pushToRemote(rootPath, remoteUrl, accessToken, pushErrors, logger) {
|
|
1358
|
+
if (!remoteUrl)
|
|
1359
|
+
return undefined;
|
|
1360
|
+
try {
|
|
1361
|
+
// Build the authenticated URL if a token is available
|
|
1362
|
+
const pushUrl = accessToken
|
|
1363
|
+
? remoteUrl.replace(/^https:\/\//, `https://${encodeURIComponent(accessToken)}@`)
|
|
1364
|
+
: remoteUrl;
|
|
1365
|
+
await execFileAsync('git', ['push', pushUrl, 'HEAD'], {
|
|
1366
|
+
cwd: rootPath,
|
|
1367
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
1368
|
+
});
|
|
1369
|
+
const timestamp = new Date().toISOString();
|
|
1370
|
+
logger.info({ root: rootPath, remote: remoteUrl }, 'VCS push succeeded');
|
|
1371
|
+
return timestamp;
|
|
1372
|
+
}
|
|
1373
|
+
catch (error) {
|
|
1374
|
+
const pushError = {
|
|
1375
|
+
timestamp: new Date().toISOString(),
|
|
1376
|
+
message: normalizeError(error).message,
|
|
1377
|
+
};
|
|
1378
|
+
pushErrors.push(pushError);
|
|
1379
|
+
logger.error({ root: rootPath, err: normalizeError(error) }, 'VCS push failed');
|
|
1380
|
+
return undefined;
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1326
1384
|
/**
|
|
1327
1385
|
* @module vcs/VcsManager
|
|
1328
1386
|
* Per-root VCS manager for git-backed content versioning.
|
|
@@ -1343,23 +1401,24 @@ class VcsManager {
|
|
|
1343
1401
|
remoteUrl;
|
|
1344
1402
|
accessToken;
|
|
1345
1403
|
logger;
|
|
1346
|
-
|
|
1404
|
+
commitMessageBuilder;
|
|
1347
1405
|
pending = new Set();
|
|
1348
1406
|
pendingReversions = [];
|
|
1349
1407
|
_pushErrors = [];
|
|
1350
1408
|
squashManager;
|
|
1351
1409
|
debounceTimer;
|
|
1352
1410
|
commitInFlight = Promise.resolve();
|
|
1411
|
+
consecutiveCommitFailures = 0;
|
|
1353
1412
|
started = false;
|
|
1354
|
-
isBaseline =
|
|
1413
|
+
isBaseline = true;
|
|
1355
1414
|
_lastPushTime = null;
|
|
1356
1415
|
constructor(rootPath, config, logger, commitMessageGenerator, remoteUrl, accessToken) {
|
|
1357
1416
|
this.rootPath = rootPath;
|
|
1358
1417
|
this.config = config;
|
|
1359
1418
|
this.logger = logger;
|
|
1360
|
-
this.commitMessageGenerator = commitMessageGenerator;
|
|
1361
1419
|
this.remoteUrl = remoteUrl;
|
|
1362
1420
|
this.accessToken = accessToken;
|
|
1421
|
+
this.commitMessageBuilder = new CommitMessageBuilder(rootPath, logger, commitMessageGenerator);
|
|
1363
1422
|
if (config.retention) {
|
|
1364
1423
|
this.squashManager = new SquashManager(rootPath, config.retention, logger, remoteUrl, accessToken);
|
|
1365
1424
|
}
|
|
@@ -1371,48 +1430,23 @@ class VcsManager {
|
|
|
1371
1430
|
return this._pushErrors;
|
|
1372
1431
|
}
|
|
1373
1432
|
/**
|
|
1374
|
-
* Begin accepting file changes. Sets
|
|
1375
|
-
*
|
|
1433
|
+
* Begin accepting file changes. Sets the manager to started state and
|
|
1434
|
+
* starts the squash manager if configured.
|
|
1435
|
+
*
|
|
1436
|
+
* Baseline mode (commit prefix `"baseline:"`) remains active until
|
|
1437
|
+
* {@link endBaseline} is called by the coordinator after the initial scan.
|
|
1376
1438
|
*/
|
|
1377
|
-
|
|
1439
|
+
start() {
|
|
1378
1440
|
this.started = true;
|
|
1379
1441
|
this.squashManager?.start();
|
|
1380
1442
|
this.logger.info({ root: this.rootPath }, 'VcsManager started');
|
|
1381
|
-
await this.enqueueBaselineIfNeeded();
|
|
1382
1443
|
}
|
|
1383
1444
|
/**
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1445
|
+
* Signal that the initial filesystem scan is complete.
|
|
1446
|
+
* Clears the baseline flag so subsequent commits use normal "watcher: batch" messages.
|
|
1386
1447
|
*/
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
1390
|
-
cwd: this.rootPath,
|
|
1391
|
-
});
|
|
1392
|
-
// HEAD exists — repo already has commits
|
|
1393
|
-
return;
|
|
1394
|
-
}
|
|
1395
|
-
catch {
|
|
1396
|
-
// No commits yet — proceed with baseline
|
|
1397
|
-
}
|
|
1398
|
-
try {
|
|
1399
|
-
const { stdout } = await execFileAsync('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd: this.rootPath });
|
|
1400
|
-
const files = stdout
|
|
1401
|
-
.split('\0')
|
|
1402
|
-
.map((line) => line.trim())
|
|
1403
|
-
.filter((line) => line.length > 0);
|
|
1404
|
-
if (files.length === 0)
|
|
1405
|
-
return;
|
|
1406
|
-
this.logger.info({ root: this.rootPath, fileCount: files.length }, 'Enqueuing baseline commit for pre-existing files');
|
|
1407
|
-
this.isBaseline = true;
|
|
1408
|
-
const absolutePaths = files.map((f) => this.rootPath + '/' + f);
|
|
1409
|
-
for (const filePath of absolutePaths) {
|
|
1410
|
-
this.fileChanged(filePath);
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
catch (error) {
|
|
1414
|
-
this.logger.warn({ root: this.rootPath, err: normalizeError(error) }, 'Failed to enumerate untracked files for baseline commit');
|
|
1415
|
-
}
|
|
1448
|
+
endBaseline() {
|
|
1449
|
+
this.isBaseline = false;
|
|
1416
1450
|
}
|
|
1417
1451
|
/**
|
|
1418
1452
|
* Add a file to the pending set and reset the debounce timer.
|
|
@@ -1423,6 +1457,11 @@ class VcsManager {
|
|
|
1423
1457
|
fileChanged(filePath) {
|
|
1424
1458
|
if (!this.started)
|
|
1425
1459
|
return;
|
|
1460
|
+
// Reset circuit breaker on new file change so the system can recover
|
|
1461
|
+
if (this.consecutiveCommitFailures >= this.config.maxConsecutiveFailures) {
|
|
1462
|
+
this.logger.info({ root: this.rootPath }, 'VCS circuit breaker reset — new file change received, retrying commits');
|
|
1463
|
+
this.consecutiveCommitFailures = 0;
|
|
1464
|
+
}
|
|
1426
1465
|
this.pending.add(filePath);
|
|
1427
1466
|
if (this.pending.size >= this.config.maxBatchSize) {
|
|
1428
1467
|
this.clearDebounce();
|
|
@@ -1510,97 +1549,61 @@ class VcsManager {
|
|
|
1510
1549
|
}, this.config.commitDebounceMs);
|
|
1511
1550
|
}
|
|
1512
1551
|
/**
|
|
1513
|
-
*
|
|
1552
|
+
* Check for and remove stale index.lock files before retrying.
|
|
1514
1553
|
*/
|
|
1515
|
-
async
|
|
1554
|
+
async removeStaleLock() {
|
|
1555
|
+
const lockPath = join(this.rootPath, '.git', 'index.lock');
|
|
1516
1556
|
try {
|
|
1517
|
-
const
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
return '';
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
/**
|
|
1526
|
-
* Build the template commit message (no AI).
|
|
1527
|
-
*/
|
|
1528
|
-
buildTemplateMessage(fileCount) {
|
|
1529
|
-
if (this.pendingReversions.length === 0) {
|
|
1530
|
-
if (this.isBaseline) {
|
|
1531
|
-
return `baseline: batch (${String(fileCount)} files)`;
|
|
1557
|
+
const lockStat = await stat(lockPath);
|
|
1558
|
+
const ageMs = Date.now() - lockStat.mtimeMs;
|
|
1559
|
+
if (ageMs > this.config.staleLockThresholdMs) {
|
|
1560
|
+
await rm(lockPath, { force: true });
|
|
1561
|
+
this.logger.warn({ root: this.rootPath, ageMs }, 'Removed stale index.lock');
|
|
1532
1562
|
}
|
|
1533
|
-
const timestamp = new Date().toISOString();
|
|
1534
|
-
return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
|
|
1535
|
-
}
|
|
1536
|
-
// Count total reverted files across all pending reversions
|
|
1537
|
-
const revertedPaths = new Set(this.pendingReversions.flatMap((r) => r.paths));
|
|
1538
|
-
const revertedCount = revertedPaths.size;
|
|
1539
|
-
const otherCount = fileCount - revertedCount;
|
|
1540
|
-
// Use the first reversion's glob and commit for the message
|
|
1541
|
-
const { glob, commit } = this.pendingReversions[0];
|
|
1542
|
-
const shortCommit = commit.slice(0, 7);
|
|
1543
|
-
let message = `revert: ${glob} to ${shortCommit} — restored ${String(revertedCount)} files`;
|
|
1544
|
-
if (otherCount > 0) {
|
|
1545
|
-
message += ` (+ ${String(otherCount)} other changes)`;
|
|
1546
|
-
}
|
|
1547
|
-
return message;
|
|
1548
|
-
}
|
|
1549
|
-
/**
|
|
1550
|
-
* Build the commit message, using AI when available with template fallback.
|
|
1551
|
-
*/
|
|
1552
|
-
async buildCommitMessage(files) {
|
|
1553
|
-
const fileCount = files.length;
|
|
1554
|
-
const templateMessage = this.buildTemplateMessage(fileCount);
|
|
1555
|
-
if (!this.commitMessageGenerator) {
|
|
1556
|
-
return templateMessage;
|
|
1557
|
-
}
|
|
1558
|
-
try {
|
|
1559
|
-
const diffSummary = await this.getStagedDiff();
|
|
1560
|
-
const aiMessage = await this.commitMessageGenerator.generate(files, diffSummary);
|
|
1561
|
-
if (!aiMessage) {
|
|
1562
|
-
this.logger.warn('AI commit message generation returned null; using template');
|
|
1563
|
-
return templateMessage;
|
|
1564
|
-
}
|
|
1565
|
-
// For reversions: prefix with revert info + AI description
|
|
1566
|
-
if (this.pendingReversions.length > 0) {
|
|
1567
|
-
const { glob, commit } = this.pendingReversions[0];
|
|
1568
|
-
const shortCommit = commit.slice(0, 7);
|
|
1569
|
-
return `revert: ${glob} to ${shortCommit} — ${aiMessage}`;
|
|
1570
|
-
}
|
|
1571
|
-
return aiMessage;
|
|
1572
1563
|
}
|
|
1573
1564
|
catch (error) {
|
|
1574
|
-
|
|
1575
|
-
|
|
1565
|
+
const code = error.code;
|
|
1566
|
+
if (code !== 'ENOENT') {
|
|
1567
|
+
this.logger.warn({ root: this.rootPath, err: normalizeError(error) }, 'Unable to remove stale index.lock');
|
|
1568
|
+
}
|
|
1576
1569
|
}
|
|
1577
1570
|
}
|
|
1578
1571
|
/**
|
|
1579
|
-
* Commit a batch of files to the git repo with index.lock retry
|
|
1572
|
+
* Commit a batch of files to the git repo with index.lock retry,
|
|
1573
|
+
* stale lock detection, circuit breaker, and re-queue cap.
|
|
1580
1574
|
*/
|
|
1581
1575
|
async commitBatch(files) {
|
|
1582
1576
|
if (files.length === 0)
|
|
1583
1577
|
return;
|
|
1578
|
+
// Circuit breaker: skip if too many consecutive failures
|
|
1579
|
+
if (this.consecutiveCommitFailures >= this.config.maxConsecutiveFailures) {
|
|
1580
|
+
this.logger.error({
|
|
1581
|
+
root: this.rootPath,
|
|
1582
|
+
consecutiveFailures: this.consecutiveCommitFailures,
|
|
1583
|
+
discardedFiles: files.length,
|
|
1584
|
+
}, 'VCS circuit breaker tripped — pending files discarded until next successful commit');
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1584
1587
|
try {
|
|
1588
|
+
// Check for stale lock before first attempt
|
|
1589
|
+
await this.removeStaleLock();
|
|
1585
1590
|
await retry(async (attempt) => {
|
|
1586
1591
|
await gitAddViaStdin(files, this.rootPath);
|
|
1587
|
-
// Build message after staging so getStagedDiff can see the changes
|
|
1588
|
-
// Skip AI for baselines and retries — use template directly
|
|
1592
|
+
// Build message after staging so getStagedDiff can see the changes.
|
|
1593
|
+
// Skip AI for baselines and retries — use template directly.
|
|
1589
1594
|
const message = this.isBaseline || attempt > 1
|
|
1590
|
-
? this.buildTemplateMessage(files.length)
|
|
1591
|
-
: await this.buildCommitMessage(files);
|
|
1595
|
+
? this.commitMessageBuilder.buildTemplateMessage(files.length, this.isBaseline, this.pendingReversions)
|
|
1596
|
+
: await this.commitMessageBuilder.buildCommitMessage(files, this.isBaseline, this.pendingReversions);
|
|
1592
1597
|
this.pendingReversions.length = 0;
|
|
1593
|
-
// Only clear baseline flag when all baseline files have been committed
|
|
1594
|
-
if (this.pending.size === 0) {
|
|
1595
|
-
this.isBaseline = false;
|
|
1596
|
-
}
|
|
1597
1598
|
await execFileAsync('git', ['commit', '-m', message], {
|
|
1598
1599
|
cwd: this.rootPath,
|
|
1599
1600
|
});
|
|
1600
1601
|
const { stdout: hashOut } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], { cwd: this.rootPath });
|
|
1601
1602
|
const hash = hashOut.trim();
|
|
1602
1603
|
this.logger.info({ root: this.rootPath, hash, fileCount: files.length }, 'VCS commit created');
|
|
1603
|
-
await this.
|
|
1604
|
+
const pushTime = await pushToRemote(this.rootPath, this.remoteUrl, this.accessToken, this._pushErrors, this.logger);
|
|
1605
|
+
if (pushTime)
|
|
1606
|
+
this._lastPushTime = pushTime;
|
|
1604
1607
|
}, {
|
|
1605
1608
|
attempts: 4,
|
|
1606
1609
|
baseDelayMs: 500,
|
|
@@ -1609,43 +1612,25 @@ class VcsManager {
|
|
|
1609
1612
|
this.logger.warn({ root: this.rootPath, attempt, delay: delayMs }, 'index.lock contention, retrying');
|
|
1610
1613
|
},
|
|
1611
1614
|
});
|
|
1615
|
+
// Success — reset circuit breaker
|
|
1616
|
+
this.consecutiveCommitFailures = 0;
|
|
1612
1617
|
}
|
|
1613
1618
|
catch (error) {
|
|
1614
|
-
|
|
1619
|
+
this.consecutiveCommitFailures++;
|
|
1620
|
+
// Re-queue cap: only re-queue up to maxBatchSize files
|
|
1621
|
+
const cap = this.config.maxBatchSize;
|
|
1622
|
+
const toRequeue = files.slice(0, cap);
|
|
1623
|
+
const overflow = files.length - toRequeue.length;
|
|
1624
|
+
for (const f of toRequeue) {
|
|
1615
1625
|
this.pending.add(f);
|
|
1616
1626
|
}
|
|
1617
|
-
|
|
1627
|
+
if (overflow > 0) {
|
|
1628
|
+
this.logger.warn({ root: this.rootPath, overflow, cap }, 'Re-queue cap exceeded — discarded overflow files');
|
|
1629
|
+
}
|
|
1630
|
+
this.logger.warn({ root: this.rootPath, fileCount: toRequeue.length }, 'Re-queued files after commit failure');
|
|
1618
1631
|
this.logger.error({ root: this.rootPath, err: normalizeError(error) }, 'VCS commit failed');
|
|
1619
1632
|
}
|
|
1620
1633
|
}
|
|
1621
|
-
/**
|
|
1622
|
-
* Push to the configured remote if remoteUrl is set.
|
|
1623
|
-
* On failure: logs the error and records it in pushErrors; never throws.
|
|
1624
|
-
*/
|
|
1625
|
-
async pushIfConfigured() {
|
|
1626
|
-
if (!this.remoteUrl)
|
|
1627
|
-
return;
|
|
1628
|
-
try {
|
|
1629
|
-
// Build the authenticated URL if a token is available
|
|
1630
|
-
const pushUrl = this.accessToken
|
|
1631
|
-
? this.remoteUrl.replace(/^https:\/\//, `https://${encodeURIComponent(this.accessToken)}@`)
|
|
1632
|
-
: this.remoteUrl;
|
|
1633
|
-
await execFileAsync('git', ['push', pushUrl, 'HEAD'], {
|
|
1634
|
-
cwd: this.rootPath,
|
|
1635
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
1636
|
-
});
|
|
1637
|
-
this._lastPushTime = new Date().toISOString();
|
|
1638
|
-
this.logger.info({ root: this.rootPath, remote: this.remoteUrl }, 'VCS push succeeded');
|
|
1639
|
-
}
|
|
1640
|
-
catch (error) {
|
|
1641
|
-
const pushError = {
|
|
1642
|
-
timestamp: new Date().toISOString(),
|
|
1643
|
-
message: normalizeError(error).message,
|
|
1644
|
-
};
|
|
1645
|
-
this._pushErrors.push(pushError);
|
|
1646
|
-
this.logger.error({ root: this.rootPath, err: normalizeError(error) }, 'VCS push failed');
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
1634
|
}
|
|
1650
1635
|
|
|
1651
1636
|
/**
|
|
@@ -1674,10 +1659,14 @@ class VcsCoordinator {
|
|
|
1674
1659
|
// Deduplicate: skip if another glob already resolved to this root.
|
|
1675
1660
|
if (this.managers.has(rootKey))
|
|
1676
1661
|
continue;
|
|
1662
|
+
const mergedRetention = entry.vcs?.retention ??
|
|
1663
|
+
config.vcs.retention ??
|
|
1664
|
+
vcsRetentionConfigSchema.parse({});
|
|
1677
1665
|
const mergedConfig = {
|
|
1678
1666
|
...config.vcs,
|
|
1679
1667
|
...entry.vcs,
|
|
1680
1668
|
enabled: true,
|
|
1669
|
+
retention: mergedRetention,
|
|
1681
1670
|
};
|
|
1682
1671
|
// Create CommitMessageGenerator if AI commit messages are configured
|
|
1683
1672
|
const cmConfig = mergedConfig.commitMessage;
|
|
@@ -1701,12 +1690,10 @@ class VcsCoordinator {
|
|
|
1701
1690
|
/**
|
|
1702
1691
|
* Start all VcsManager instances.
|
|
1703
1692
|
*/
|
|
1704
|
-
|
|
1705
|
-
const startPromises = [];
|
|
1693
|
+
start() {
|
|
1706
1694
|
this.managers.forEach((manager) => {
|
|
1707
|
-
|
|
1695
|
+
manager.start();
|
|
1708
1696
|
});
|
|
1709
|
-
await Promise.all(startPromises);
|
|
1710
1697
|
this.logger.info({ rootCount: this.managers.size }, 'VcsCoordinator started');
|
|
1711
1698
|
}
|
|
1712
1699
|
/**
|
|
@@ -1727,6 +1714,21 @@ class VcsCoordinator {
|
|
|
1727
1714
|
manager.fileChanged(normalizedPath);
|
|
1728
1715
|
}
|
|
1729
1716
|
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Signal that the initial filesystem scan is complete.
|
|
1719
|
+
*
|
|
1720
|
+
* Flushes each manager's debounce buffer before ending baseline mode, ensuring
|
|
1721
|
+
* any files still pending in the VCS debounce are committed with a
|
|
1722
|
+
* `"baseline:"` prefix rather than `"watcher:"`. AI generation is not enabled
|
|
1723
|
+
* until all managers have completed their flush.
|
|
1724
|
+
*/
|
|
1725
|
+
async onInitialScanComplete() {
|
|
1726
|
+
for (const manager of this.managers.values()) {
|
|
1727
|
+
await manager.flush();
|
|
1728
|
+
manager.endBaseline();
|
|
1729
|
+
}
|
|
1730
|
+
this.logger.debug('VcsCoordinator: initial scan complete, baseline ended');
|
|
1731
|
+
}
|
|
1730
1732
|
/**
|
|
1731
1733
|
* Flush all managers and clean up.
|
|
1732
1734
|
*/
|
|
@@ -3129,6 +3131,68 @@ async function ensureGitignore(rootPath, alwaysOnEntries = []) {
|
|
|
3129
3131
|
}
|
|
3130
3132
|
}
|
|
3131
3133
|
|
|
3134
|
+
/**
|
|
3135
|
+
* @module app/watcherFactory
|
|
3136
|
+
* Factory helpers for creating and rebuilding the filesystem watcher.
|
|
3137
|
+
* Extracted from initialization.ts to follow SRP.
|
|
3138
|
+
*/
|
|
3139
|
+
/**
|
|
3140
|
+
* Create file system watcher with gitignore filtering.
|
|
3141
|
+
*/
|
|
3142
|
+
function createWatcher(config, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
|
|
3143
|
+
const respectGitignore = config.watch.respectGitignore ?? true;
|
|
3144
|
+
const gitignoreFilter = respectGitignore
|
|
3145
|
+
? new GitignoreFilter(extractWatchPathStrings(config.watch.paths))
|
|
3146
|
+
: undefined;
|
|
3147
|
+
const watcher = factories.createFileSystemWatcher(config.watch, queue, processor, logger, {
|
|
3148
|
+
maxRetries: config.maxRetries ?? runtimeOptions.maxRetries,
|
|
3149
|
+
maxBackoffMs: config.maxBackoffMs ?? runtimeOptions.maxBackoffMs,
|
|
3150
|
+
onFatalError: runtimeOptions.onFatalError,
|
|
3151
|
+
gitignoreFilter,
|
|
3152
|
+
initialScanTracker,
|
|
3153
|
+
contentHashCache,
|
|
3154
|
+
onVcsFileChange,
|
|
3155
|
+
onInitialScanComplete,
|
|
3156
|
+
});
|
|
3157
|
+
return { watcher, gitignoreFilter };
|
|
3158
|
+
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Tear down and rebuild the filesystem watcher with new config.
|
|
3161
|
+
* Falls back to the old watcher if the new one fails to start.
|
|
3162
|
+
*/
|
|
3163
|
+
async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
|
|
3164
|
+
try {
|
|
3165
|
+
await oldState.watcher.stop();
|
|
3166
|
+
const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete);
|
|
3167
|
+
newWatcher.start();
|
|
3168
|
+
logger.info('Filesystem watcher rebuilt successfully');
|
|
3169
|
+
return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
|
|
3170
|
+
}
|
|
3171
|
+
catch (error) {
|
|
3172
|
+
logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
|
|
3173
|
+
try {
|
|
3174
|
+
oldState.watcher.start();
|
|
3175
|
+
}
|
|
3176
|
+
catch (restartError) {
|
|
3177
|
+
logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
|
|
3178
|
+
}
|
|
3179
|
+
return oldState;
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
/**
|
|
3183
|
+
* Check whether watch-relevant config fields changed between old and new config.
|
|
3184
|
+
*/
|
|
3185
|
+
function watchConfigChanged(oldConfig, newConfig) {
|
|
3186
|
+
return (JSON.stringify(oldConfig.watch.paths) !==
|
|
3187
|
+
JSON.stringify(newConfig.watch.paths) ||
|
|
3188
|
+
JSON.stringify(oldConfig.watch.ignored) !==
|
|
3189
|
+
JSON.stringify(newConfig.watch.ignored) ||
|
|
3190
|
+
JSON.stringify(oldConfig.watch.moveDetection) !==
|
|
3191
|
+
JSON.stringify(newConfig.watch.moveDetection) ||
|
|
3192
|
+
(oldConfig.watch.respectGitignore ?? true) !==
|
|
3193
|
+
(newConfig.watch.respectGitignore ?? true));
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3132
3196
|
/**
|
|
3133
3197
|
* @module app/initialization
|
|
3134
3198
|
* Initialization helpers for JeevesWatcher. Extracted to follow SRP.
|
|
@@ -3193,25 +3257,6 @@ function createProcessorConfig(config, configDir, customMapLib) {
|
|
|
3193
3257
|
globalSchemas: config.schemas,
|
|
3194
3258
|
};
|
|
3195
3259
|
}
|
|
3196
|
-
/**
|
|
3197
|
-
* Create file system watcher with gitignore filtering.
|
|
3198
|
-
*/
|
|
3199
|
-
function createWatcher(config, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange) {
|
|
3200
|
-
const respectGitignore = config.watch.respectGitignore ?? true;
|
|
3201
|
-
const gitignoreFilter = respectGitignore
|
|
3202
|
-
? new GitignoreFilter(extractWatchPathStrings(config.watch.paths))
|
|
3203
|
-
: undefined;
|
|
3204
|
-
const watcher = factories.createFileSystemWatcher(config.watch, queue, processor, logger, {
|
|
3205
|
-
maxRetries: config.maxRetries ?? runtimeOptions.maxRetries,
|
|
3206
|
-
maxBackoffMs: config.maxBackoffMs ?? runtimeOptions.maxBackoffMs,
|
|
3207
|
-
onFatalError: runtimeOptions.onFatalError,
|
|
3208
|
-
gitignoreFilter,
|
|
3209
|
-
initialScanTracker,
|
|
3210
|
-
contentHashCache,
|
|
3211
|
-
onVcsFileChange,
|
|
3212
|
-
});
|
|
3213
|
-
return { watcher, gitignoreFilter };
|
|
3214
|
-
}
|
|
3215
3260
|
/**
|
|
3216
3261
|
* Introspect all helpers for merged document.
|
|
3217
3262
|
*/
|
|
@@ -3227,42 +3272,6 @@ async function introspectHelpers(config, configDir) {
|
|
|
3227
3272
|
function getConfigDir(configPath) {
|
|
3228
3273
|
return configPath ? dirname(configPath) : '.';
|
|
3229
3274
|
}
|
|
3230
|
-
/**
|
|
3231
|
-
* Tear down and rebuild the filesystem watcher with new config.
|
|
3232
|
-
* Falls back to the old watcher if the new one fails to start.
|
|
3233
|
-
*/
|
|
3234
|
-
async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache) {
|
|
3235
|
-
try {
|
|
3236
|
-
await oldState.watcher.stop();
|
|
3237
|
-
const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache);
|
|
3238
|
-
newWatcher.start();
|
|
3239
|
-
logger.info('Filesystem watcher rebuilt successfully');
|
|
3240
|
-
return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
|
|
3241
|
-
}
|
|
3242
|
-
catch (error) {
|
|
3243
|
-
logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
|
|
3244
|
-
try {
|
|
3245
|
-
oldState.watcher.start();
|
|
3246
|
-
}
|
|
3247
|
-
catch (restartError) {
|
|
3248
|
-
logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
|
|
3249
|
-
}
|
|
3250
|
-
return oldState;
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
/**
|
|
3254
|
-
* Check whether watch-relevant config fields changed between old and new config.
|
|
3255
|
-
*/
|
|
3256
|
-
function watchConfigChanged(oldConfig, newConfig) {
|
|
3257
|
-
return (JSON.stringify(oldConfig.watch.paths) !==
|
|
3258
|
-
JSON.stringify(newConfig.watch.paths) ||
|
|
3259
|
-
JSON.stringify(oldConfig.watch.ignored) !==
|
|
3260
|
-
JSON.stringify(newConfig.watch.ignored) ||
|
|
3261
|
-
JSON.stringify(oldConfig.watch.moveDetection) !==
|
|
3262
|
-
JSON.stringify(newConfig.watch.moveDetection) ||
|
|
3263
|
-
(oldConfig.watch.respectGitignore ?? true) !==
|
|
3264
|
-
(newConfig.watch.respectGitignore ?? true));
|
|
3265
|
-
}
|
|
3266
3275
|
/**
|
|
3267
3276
|
* Resolve package version from nearest package.json.
|
|
3268
3277
|
*/
|
|
@@ -3349,7 +3358,7 @@ async function reloadConfig(state, deps) {
|
|
|
3349
3358
|
// Rebuild filesystem watcher if watch config changed
|
|
3350
3359
|
if (watchConfigChanged(oldConfig, newConfig)) {
|
|
3351
3360
|
logger.info('Watch config changed, rebuilding filesystem watcher...');
|
|
3352
|
-
const newState = await rebuildWatcher(newConfig, deps.factories, deps.queue, processor, logger, deps.runtimeOptions, { watcher: state.watcher, gitignoreFilter: state.gitignoreFilter }, deps.initialScanTracker, deps.contentHashCache);
|
|
3361
|
+
const newState = await rebuildWatcher(newConfig, deps.factories, deps.queue, processor, logger, deps.runtimeOptions, { watcher: state.watcher, gitignoreFilter: state.gitignoreFilter }, deps.initialScanTracker, deps.contentHashCache, deps.onVcsFileChange, deps.onInitialScanComplete);
|
|
3353
3362
|
state.watcher = newState.watcher;
|
|
3354
3363
|
state.gitignoreFilter = newState.gitignoreFilter;
|
|
3355
3364
|
}
|
|
@@ -7431,6 +7440,37 @@ class MoveCorrelator {
|
|
|
7431
7440
|
}
|
|
7432
7441
|
}
|
|
7433
7442
|
|
|
7443
|
+
/**
|
|
7444
|
+
* @module watcher/ScanStats
|
|
7445
|
+
* Tracks initial filesystem scan statistics for diagnostics.
|
|
7446
|
+
*/
|
|
7447
|
+
/**
|
|
7448
|
+
* Mutable counter bag updated during the chokidar initial scan.
|
|
7449
|
+
* Extracted from the watcher start() method for clarity.
|
|
7450
|
+
*/
|
|
7451
|
+
class ScanStats {
|
|
7452
|
+
total = 0;
|
|
7453
|
+
matched = 0;
|
|
7454
|
+
globRejected = 0;
|
|
7455
|
+
gitignored = 0;
|
|
7456
|
+
byRoot;
|
|
7457
|
+
constructor(roots) {
|
|
7458
|
+
this.byRoot = Object.fromEntries(roots.map((r) => [r, 0]));
|
|
7459
|
+
}
|
|
7460
|
+
/**
|
|
7461
|
+
* Attribute a newly discovered file to its root for per-root diagnostics.
|
|
7462
|
+
*/
|
|
7463
|
+
classifyByRoot(path) {
|
|
7464
|
+
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
7465
|
+
for (const root of Object.keys(this.byRoot)) {
|
|
7466
|
+
if (normalized.startsWith(`${root}/`) || normalized === root) {
|
|
7467
|
+
this.byRoot[root] = (this.byRoot[root] ?? 0) + 1;
|
|
7468
|
+
break;
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
}
|
|
7472
|
+
}
|
|
7473
|
+
|
|
7434
7474
|
/**
|
|
7435
7475
|
* @module watcher
|
|
7436
7476
|
* Filesystem watcher wrapping chokidar. I/O: watches files/directories for add/change/unlink events, enqueues to processing queue.
|
|
@@ -7448,6 +7488,7 @@ class FileSystemWatcher {
|
|
|
7448
7488
|
initialScanTracker;
|
|
7449
7489
|
contentHashCache;
|
|
7450
7490
|
onVcsFileChange;
|
|
7491
|
+
onInitialScanComplete;
|
|
7451
7492
|
moveCorrelator;
|
|
7452
7493
|
globMatches;
|
|
7453
7494
|
watcher;
|
|
@@ -7469,6 +7510,7 @@ class FileSystemWatcher {
|
|
|
7469
7510
|
this.initialScanTracker = options.initialScanTracker;
|
|
7470
7511
|
this.contentHashCache = options.contentHashCache;
|
|
7471
7512
|
this.onVcsFileChange = options.onVcsFileChange;
|
|
7513
|
+
this.onInitialScanComplete = options.onInitialScanComplete;
|
|
7472
7514
|
this.globMatches = () => true;
|
|
7473
7515
|
const healthOptions = {
|
|
7474
7516
|
maxRetries: options.maxRetries,
|
|
@@ -7496,24 +7538,9 @@ class FileSystemWatcher {
|
|
|
7496
7538
|
? resolveIgnored(this.config.ignored)
|
|
7497
7539
|
: undefined;
|
|
7498
7540
|
// Track initial scan statistics per root for diagnostics.
|
|
7499
|
-
const scanStats =
|
|
7500
|
-
total: 0,
|
|
7501
|
-
matched: 0,
|
|
7502
|
-
globRejected: 0,
|
|
7503
|
-
gitignored: 0,
|
|
7504
|
-
byRoot: Object.fromEntries(roots.map((r) => [r, 0])),
|
|
7505
|
-
};
|
|
7541
|
+
const scanStats = new ScanStats(roots);
|
|
7506
7542
|
let initialScanComplete = false;
|
|
7507
7543
|
this.initialScanTracker?.start();
|
|
7508
|
-
const classifyByRoot = (path) => {
|
|
7509
|
-
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
7510
|
-
for (const root of roots) {
|
|
7511
|
-
if (normalized.startsWith(root + '/') || normalized === root) {
|
|
7512
|
-
scanStats.byRoot[root] = (scanStats.byRoot[root] ?? 0) + 1;
|
|
7513
|
-
break;
|
|
7514
|
-
}
|
|
7515
|
-
}
|
|
7516
|
-
};
|
|
7517
7544
|
// Create move correlator if move detection is configured and cache is available.
|
|
7518
7545
|
const moveConfig = this.config.moveDetection;
|
|
7519
7546
|
if (moveConfig?.enabled && this.contentHashCache) {
|
|
@@ -7546,7 +7573,7 @@ class FileSystemWatcher {
|
|
|
7546
7573
|
this.watcher.on('add', (path) => {
|
|
7547
7574
|
if (!initialScanComplete) {
|
|
7548
7575
|
scanStats.total++;
|
|
7549
|
-
classifyByRoot(path);
|
|
7576
|
+
scanStats.classifyByRoot(path);
|
|
7550
7577
|
}
|
|
7551
7578
|
this.handleGitignoreChange(path);
|
|
7552
7579
|
if (!this.globMatches(path)) {
|
|
@@ -7567,8 +7594,7 @@ class FileSystemWatcher {
|
|
|
7567
7594
|
this.initialScanTracker?.incrementEnqueued();
|
|
7568
7595
|
}
|
|
7569
7596
|
this.logger.debug({ path }, 'File added');
|
|
7570
|
-
|
|
7571
|
-
this.onVcsFileChange?.(path, 'add');
|
|
7597
|
+
this.onVcsFileChange?.(path, 'add');
|
|
7572
7598
|
if (correlator && initialScanComplete) {
|
|
7573
7599
|
void correlator.handleAdd(path);
|
|
7574
7600
|
}
|
|
@@ -7607,6 +7633,7 @@ class FileSystemWatcher {
|
|
|
7607
7633
|
initialScanComplete = true;
|
|
7608
7634
|
this.initialScanTracker?.complete();
|
|
7609
7635
|
this.logger.info({ scanStats }, 'Initial scan complete');
|
|
7636
|
+
void this.onInitialScanComplete?.();
|
|
7610
7637
|
});
|
|
7611
7638
|
this.watcher.on('error', (error) => {
|
|
7612
7639
|
this.logger.error({ err: normalizeError(error) }, 'Watcher error');
|
|
@@ -7768,6 +7795,12 @@ class JeevesWatcher {
|
|
|
7768
7795
|
* Start the watcher, API server, and all components.
|
|
7769
7796
|
*/
|
|
7770
7797
|
async start() {
|
|
7798
|
+
await this.initInfrastructure();
|
|
7799
|
+
await this.initPipeline();
|
|
7800
|
+
await this.startServicesAndWatcher();
|
|
7801
|
+
}
|
|
7802
|
+
/** Set up logger, VCS, embedding provider, and vector store. */
|
|
7803
|
+
async initInfrastructure() {
|
|
7771
7804
|
const logger = this.factories.createLogger(this.config.logging);
|
|
7772
7805
|
this.logger = logger;
|
|
7773
7806
|
await initVcs(this.config, logger);
|
|
@@ -7777,6 +7810,10 @@ class JeevesWatcher {
|
|
|
7777
7810
|
if (this.config.search?.hybrid?.enabled) {
|
|
7778
7811
|
await vectorStore.ensureTextIndex('chunk_text');
|
|
7779
7812
|
}
|
|
7813
|
+
}
|
|
7814
|
+
/** Build the processing pipeline: rules, templates, processor, queue, VCS. */
|
|
7815
|
+
async initPipeline() {
|
|
7816
|
+
const logger = this.logger;
|
|
7780
7817
|
const compiledRules = this.factories.compileRules(this.config.inferenceRules ?? []);
|
|
7781
7818
|
const configDir = getConfigDir(this.configPath);
|
|
7782
7819
|
const { templateEngine, customMapLib } = await buildTemplateEngineAndCustomMapLib(this.config, configDir);
|
|
@@ -7786,36 +7823,39 @@ class JeevesWatcher {
|
|
|
7786
7823
|
this.issuesManager = new IssuesManager(stateDir, logger);
|
|
7787
7824
|
this.valuesManager = new ValuesManager(stateDir, logger);
|
|
7788
7825
|
this.enrichmentStore = new EnrichmentStore(stateDir, logger);
|
|
7789
|
-
const enrichmentStore = this.enrichmentStore;
|
|
7790
7826
|
this.contentHashCache = new ContentHashCache();
|
|
7791
|
-
|
|
7792
|
-
const processor = this.factories.createDocumentProcessor({
|
|
7827
|
+
this.processor = this.factories.createDocumentProcessor({
|
|
7793
7828
|
config: processorConfig,
|
|
7794
|
-
embeddingProvider,
|
|
7795
|
-
vectorStore,
|
|
7829
|
+
embeddingProvider: this.embeddingProvider,
|
|
7830
|
+
vectorStore: this.vectorStore,
|
|
7796
7831
|
compiledRules,
|
|
7797
7832
|
logger,
|
|
7798
7833
|
templateEngine,
|
|
7799
|
-
enrichmentStore,
|
|
7834
|
+
enrichmentStore: this.enrichmentStore,
|
|
7800
7835
|
issuesManager: this.issuesManager,
|
|
7801
7836
|
valuesManager: this.valuesManager,
|
|
7802
|
-
contentHashCache,
|
|
7837
|
+
contentHashCache: this.contentHashCache,
|
|
7803
7838
|
});
|
|
7804
|
-
this.processor = processor;
|
|
7805
7839
|
this.queue = this.factories.createEventQueue({
|
|
7806
7840
|
debounceMs: this.config.watch.debounceMs ?? 2000,
|
|
7807
7841
|
concurrency: this.config.embedding.concurrency ?? 5,
|
|
7808
7842
|
rateLimitPerMinute: this.config.embedding.rateLimitPerMinute,
|
|
7809
7843
|
});
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7844
|
+
this.vcsCoordinator = new VcsCoordinator(this.config, logger);
|
|
7845
|
+
}
|
|
7846
|
+
/** Wire up watcher and VCS coordinator, then start all services. */
|
|
7847
|
+
async startServicesAndWatcher() {
|
|
7848
|
+
const logger = this.logger;
|
|
7849
|
+
const vcsCoordinator = this.vcsCoordinator;
|
|
7850
|
+
const { watcher, gitignoreFilter } = createWatcher(this.config, this.factories, this.queue, this.processor, logger, this.runtimeOptions, this.initialScanTracker, this.contentHashCache, (filePath, event) => {
|
|
7813
7851
|
vcsCoordinator.onFileChange(filePath, event);
|
|
7852
|
+
}, () => {
|
|
7853
|
+
void vcsCoordinator.onInitialScanComplete();
|
|
7814
7854
|
});
|
|
7815
7855
|
this.watcher = watcher;
|
|
7816
7856
|
this.gitignoreFilter = gitignoreFilter;
|
|
7817
7857
|
this.server = await this.startApiServer();
|
|
7818
|
-
|
|
7858
|
+
vcsCoordinator.start();
|
|
7819
7859
|
this.watcher.start();
|
|
7820
7860
|
this.startConfigWatch();
|
|
7821
7861
|
logger.info('jeeves-watcher started');
|
|
@@ -7930,6 +7970,8 @@ class JeevesWatcher {
|
|
|
7930
7970
|
contentHashCache: this.contentHashCache,
|
|
7931
7971
|
valuesManager: this.valuesManager,
|
|
7932
7972
|
issuesManager: this.issuesManager,
|
|
7973
|
+
onVcsFileChange: (filePath, event) => this.vcsCoordinator?.onFileChange(filePath, event),
|
|
7974
|
+
onInitialScanComplete: () => this.vcsCoordinator?.onInitialScanComplete(),
|
|
7933
7975
|
});
|
|
7934
7976
|
this.config = state.config;
|
|
7935
7977
|
this.watcher = state.watcher;
|