@karmaniverous/jeeves-watcher 0.18.6 → 0.18.8
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/dist/cli/jeeves-watcher/{index-CS1wrtes.js → index-BjghvUnc.js} +291 -232
- package/dist/cli/jeeves-watcher/index.js +1 -1
- package/dist/index.d.ts +37 -28
- package/dist/index.js +290 -231
- package/package.json +1 -1
|
@@ -27,6 +27,7 @@ import { capitalize, title, camel, snake, dash, isEqual, get, omit } from 'radas
|
|
|
27
27
|
import rehypeParse from 'rehype-parse';
|
|
28
28
|
import { unified } from 'unified';
|
|
29
29
|
import yaml from 'js-yaml';
|
|
30
|
+
import 'ignore';
|
|
30
31
|
import chokidar from 'chokidar';
|
|
31
32
|
import Fastify from 'fastify';
|
|
32
33
|
import crypto from 'crypto';
|
|
@@ -39,7 +40,6 @@ import mammoth from 'mammoth';
|
|
|
39
40
|
import { MarkdownTextSplitter, RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
|
|
40
41
|
import { QdrantClient } from '@qdrant/js-client-rest';
|
|
41
42
|
import 'cosmiconfig';
|
|
42
|
-
import 'ignore';
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* @module api/InitialScanTracker
|
|
@@ -1029,6 +1029,91 @@ function resolveCommitMessageApiKey(provider, configApiKey, logger) {
|
|
|
1029
1029
|
return undefined;
|
|
1030
1030
|
}
|
|
1031
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
* @module vcs/CommitMessageBuilder
|
|
1034
|
+
* Builds commit messages — both template-based and AI-generated.
|
|
1035
|
+
*/
|
|
1036
|
+
/**
|
|
1037
|
+
* Builds commit messages for VCS commits.
|
|
1038
|
+
* Supports template-based messages and AI-generated messages with fallback.
|
|
1039
|
+
*/
|
|
1040
|
+
class CommitMessageBuilder {
|
|
1041
|
+
rootPath;
|
|
1042
|
+
logger;
|
|
1043
|
+
generator;
|
|
1044
|
+
constructor(rootPath, logger, generator) {
|
|
1045
|
+
this.rootPath = rootPath;
|
|
1046
|
+
this.logger = logger;
|
|
1047
|
+
this.generator = generator;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Build the template commit message (no AI).
|
|
1051
|
+
*/
|
|
1052
|
+
buildTemplateMessage(fileCount, isBaseline, pendingReversions) {
|
|
1053
|
+
if (pendingReversions.length === 0) {
|
|
1054
|
+
if (isBaseline) {
|
|
1055
|
+
return `baseline: batch (${String(fileCount)} files)`;
|
|
1056
|
+
}
|
|
1057
|
+
const timestamp = new Date().toISOString();
|
|
1058
|
+
return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
|
|
1059
|
+
}
|
|
1060
|
+
// Count total reverted files across all pending reversions
|
|
1061
|
+
const revertedPaths = new Set(pendingReversions.flatMap((r) => r.paths));
|
|
1062
|
+
const revertedCount = revertedPaths.size;
|
|
1063
|
+
const otherCount = fileCount - revertedCount;
|
|
1064
|
+
// Use the first reversion's glob and commit for the message
|
|
1065
|
+
const { glob, commit } = pendingReversions[0];
|
|
1066
|
+
const shortCommit = commit.slice(0, 7);
|
|
1067
|
+
let message = `revert: ${glob} to ${shortCommit} — restored ${String(revertedCount)} files`;
|
|
1068
|
+
if (otherCount > 0) {
|
|
1069
|
+
message += ` (+ ${String(otherCount)} other changes)`;
|
|
1070
|
+
}
|
|
1071
|
+
return message;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Build the commit message, using AI when available with template fallback.
|
|
1075
|
+
*/
|
|
1076
|
+
async buildCommitMessage(files, isBaseline, pendingReversions) {
|
|
1077
|
+
const fileCount = files.length;
|
|
1078
|
+
const templateMessage = this.buildTemplateMessage(fileCount, isBaseline, pendingReversions);
|
|
1079
|
+
if (!this.generator) {
|
|
1080
|
+
return templateMessage;
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
const diffSummary = await this.getStagedDiff();
|
|
1084
|
+
const aiMessage = await this.generator.generate(files, diffSummary);
|
|
1085
|
+
if (!aiMessage) {
|
|
1086
|
+
this.logger.warn('AI commit message generation returned null; using template');
|
|
1087
|
+
return templateMessage;
|
|
1088
|
+
}
|
|
1089
|
+
// For reversions: prefix with revert info + AI description
|
|
1090
|
+
if (pendingReversions.length > 0) {
|
|
1091
|
+
const { glob, commit } = pendingReversions[0];
|
|
1092
|
+
const shortCommit = commit.slice(0, 7);
|
|
1093
|
+
return `revert: ${glob} to ${shortCommit} — ${aiMessage}`;
|
|
1094
|
+
}
|
|
1095
|
+
return aiMessage;
|
|
1096
|
+
}
|
|
1097
|
+
catch (error) {
|
|
1098
|
+
this.logger.warn({ err: normalizeError(error) }, 'AI commit message generation failed; using template');
|
|
1099
|
+
return templateMessage;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Get the staged diff for AI context.
|
|
1104
|
+
*/
|
|
1105
|
+
async getStagedDiff() {
|
|
1106
|
+
try {
|
|
1107
|
+
const { stdout: stat } = await execFileAsync('git', ['diff', '--cached', '--stat'], { cwd: this.rootPath });
|
|
1108
|
+
const { stdout: patch } = await execFileAsync('git', ['diff', '--cached'], { cwd: this.rootPath });
|
|
1109
|
+
return `${stat}\n${patch}`;
|
|
1110
|
+
}
|
|
1111
|
+
catch {
|
|
1112
|
+
return '';
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1032
1117
|
/**
|
|
1033
1118
|
* @module vcs/SquashManager
|
|
1034
1119
|
* Handles scheduled squash retention for git-backed VCS roots.
|
|
@@ -1323,6 +1408,50 @@ class SquashManager {
|
|
|
1323
1408
|
}
|
|
1324
1409
|
}
|
|
1325
1410
|
|
|
1411
|
+
/**
|
|
1412
|
+
* @module vcs/vcsPush
|
|
1413
|
+
* Push logic for VCS commits.
|
|
1414
|
+
*/
|
|
1415
|
+
/**
|
|
1416
|
+
* Push to the configured remote if remoteUrl is set.
|
|
1417
|
+
*
|
|
1418
|
+
* On failure: logs the error, appends to pushErrors, and returns undefined.
|
|
1419
|
+
* Never throws.
|
|
1420
|
+
*
|
|
1421
|
+
* @param rootPath - Absolute path of the git repository.
|
|
1422
|
+
* @param remoteUrl - Remote URL to push to, or undefined to skip.
|
|
1423
|
+
* @param accessToken - Optional token for HTTPS authentication.
|
|
1424
|
+
* @param pushErrors - Mutable array where push errors are recorded.
|
|
1425
|
+
* @param logger - Logger instance.
|
|
1426
|
+
* @returns ISO timestamp of a successful push, or undefined if skipped/failed.
|
|
1427
|
+
*/
|
|
1428
|
+
async function pushToRemote(rootPath, remoteUrl, accessToken, pushErrors, logger) {
|
|
1429
|
+
if (!remoteUrl)
|
|
1430
|
+
return undefined;
|
|
1431
|
+
try {
|
|
1432
|
+
// Build the authenticated URL if a token is available
|
|
1433
|
+
const pushUrl = accessToken
|
|
1434
|
+
? remoteUrl.replace(/^https:\/\//, `https://${encodeURIComponent(accessToken)}@`)
|
|
1435
|
+
: remoteUrl;
|
|
1436
|
+
await execFileAsync('git', ['push', pushUrl, 'HEAD'], {
|
|
1437
|
+
cwd: rootPath,
|
|
1438
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
1439
|
+
});
|
|
1440
|
+
const timestamp = new Date().toISOString();
|
|
1441
|
+
logger.info({ root: rootPath, remote: remoteUrl }, 'VCS push succeeded');
|
|
1442
|
+
return timestamp;
|
|
1443
|
+
}
|
|
1444
|
+
catch (error) {
|
|
1445
|
+
const pushError = {
|
|
1446
|
+
timestamp: new Date().toISOString(),
|
|
1447
|
+
message: normalizeError(error).message,
|
|
1448
|
+
};
|
|
1449
|
+
pushErrors.push(pushError);
|
|
1450
|
+
logger.error({ root: rootPath, err: normalizeError(error) }, 'VCS push failed');
|
|
1451
|
+
return undefined;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1326
1455
|
/**
|
|
1327
1456
|
* @module vcs/VcsManager
|
|
1328
1457
|
* Per-root VCS manager for git-backed content versioning.
|
|
@@ -1343,7 +1472,7 @@ class VcsManager {
|
|
|
1343
1472
|
remoteUrl;
|
|
1344
1473
|
accessToken;
|
|
1345
1474
|
logger;
|
|
1346
|
-
|
|
1475
|
+
commitMessageBuilder;
|
|
1347
1476
|
pending = new Set();
|
|
1348
1477
|
pendingReversions = [];
|
|
1349
1478
|
_pushErrors = [];
|
|
@@ -1351,15 +1480,15 @@ class VcsManager {
|
|
|
1351
1480
|
debounceTimer;
|
|
1352
1481
|
commitInFlight = Promise.resolve();
|
|
1353
1482
|
started = false;
|
|
1354
|
-
isBaseline =
|
|
1483
|
+
isBaseline = true;
|
|
1355
1484
|
_lastPushTime = null;
|
|
1356
1485
|
constructor(rootPath, config, logger, commitMessageGenerator, remoteUrl, accessToken) {
|
|
1357
1486
|
this.rootPath = rootPath;
|
|
1358
1487
|
this.config = config;
|
|
1359
1488
|
this.logger = logger;
|
|
1360
|
-
this.commitMessageGenerator = commitMessageGenerator;
|
|
1361
1489
|
this.remoteUrl = remoteUrl;
|
|
1362
1490
|
this.accessToken = accessToken;
|
|
1491
|
+
this.commitMessageBuilder = new CommitMessageBuilder(rootPath, logger, commitMessageGenerator);
|
|
1363
1492
|
if (config.retention) {
|
|
1364
1493
|
this.squashManager = new SquashManager(rootPath, config.retention, logger, remoteUrl, accessToken);
|
|
1365
1494
|
}
|
|
@@ -1371,48 +1500,23 @@ class VcsManager {
|
|
|
1371
1500
|
return this._pushErrors;
|
|
1372
1501
|
}
|
|
1373
1502
|
/**
|
|
1374
|
-
* Begin accepting file changes. Sets
|
|
1375
|
-
*
|
|
1503
|
+
* Begin accepting file changes. Sets the manager to started state and
|
|
1504
|
+
* starts the squash manager if configured.
|
|
1505
|
+
*
|
|
1506
|
+
* Baseline mode (commit prefix `"baseline:"`) remains active until
|
|
1507
|
+
* {@link endBaseline} is called by the coordinator after the initial scan.
|
|
1376
1508
|
*/
|
|
1377
|
-
|
|
1509
|
+
start() {
|
|
1378
1510
|
this.started = true;
|
|
1379
1511
|
this.squashManager?.start();
|
|
1380
1512
|
this.logger.info({ root: this.rootPath }, 'VcsManager started');
|
|
1381
|
-
await this.enqueueBaselineIfNeeded();
|
|
1382
1513
|
}
|
|
1383
1514
|
/**
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1515
|
+
* Signal that the initial filesystem scan is complete.
|
|
1516
|
+
* Clears the baseline flag so subsequent commits use normal "watcher: batch" messages.
|
|
1386
1517
|
*/
|
|
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
|
-
}
|
|
1518
|
+
endBaseline() {
|
|
1519
|
+
this.isBaseline = false;
|
|
1416
1520
|
}
|
|
1417
1521
|
/**
|
|
1418
1522
|
* Add a file to the pending set and reset the debounce timer.
|
|
@@ -1509,72 +1613,6 @@ class VcsManager {
|
|
|
1509
1613
|
void this.flush();
|
|
1510
1614
|
}, this.config.commitDebounceMs);
|
|
1511
1615
|
}
|
|
1512
|
-
/**
|
|
1513
|
-
* Get the cached diff for staged files, for AI context.
|
|
1514
|
-
*/
|
|
1515
|
-
async getStagedDiff() {
|
|
1516
|
-
try {
|
|
1517
|
-
const { stdout: stat } = await execFileAsync('git', ['diff', '--cached', '--stat'], { cwd: this.rootPath });
|
|
1518
|
-
const { stdout: patch } = await execFileAsync('git', ['diff', '--cached'], { cwd: this.rootPath });
|
|
1519
|
-
return `${stat}\n${patch}`;
|
|
1520
|
-
}
|
|
1521
|
-
catch {
|
|
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)`;
|
|
1532
|
-
}
|
|
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
|
-
}
|
|
1573
|
-
catch (error) {
|
|
1574
|
-
this.logger.warn({ err: normalizeError(error) }, 'AI commit message generation failed; using template');
|
|
1575
|
-
return templateMessage;
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
1616
|
/**
|
|
1579
1617
|
* Commit a batch of files to the git repo with index.lock retry.
|
|
1580
1618
|
*/
|
|
@@ -1584,23 +1622,21 @@ class VcsManager {
|
|
|
1584
1622
|
try {
|
|
1585
1623
|
await retry(async (attempt) => {
|
|
1586
1624
|
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
|
|
1625
|
+
// Build message after staging so getStagedDiff can see the changes.
|
|
1626
|
+
// Skip AI for baselines and retries — use template directly.
|
|
1589
1627
|
const message = this.isBaseline || attempt > 1
|
|
1590
|
-
? this.buildTemplateMessage(files.length)
|
|
1591
|
-
: await this.buildCommitMessage(files);
|
|
1628
|
+
? this.commitMessageBuilder.buildTemplateMessage(files.length, this.isBaseline, this.pendingReversions)
|
|
1629
|
+
: await this.commitMessageBuilder.buildCommitMessage(files, this.isBaseline, this.pendingReversions);
|
|
1592
1630
|
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
1631
|
await execFileAsync('git', ['commit', '-m', message], {
|
|
1598
1632
|
cwd: this.rootPath,
|
|
1599
1633
|
});
|
|
1600
1634
|
const { stdout: hashOut } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], { cwd: this.rootPath });
|
|
1601
1635
|
const hash = hashOut.trim();
|
|
1602
1636
|
this.logger.info({ root: this.rootPath, hash, fileCount: files.length }, 'VCS commit created');
|
|
1603
|
-
await this.
|
|
1637
|
+
const pushTime = await pushToRemote(this.rootPath, this.remoteUrl, this.accessToken, this._pushErrors, this.logger);
|
|
1638
|
+
if (pushTime)
|
|
1639
|
+
this._lastPushTime = pushTime;
|
|
1604
1640
|
}, {
|
|
1605
1641
|
attempts: 4,
|
|
1606
1642
|
baseDelayMs: 500,
|
|
@@ -1618,34 +1654,6 @@ class VcsManager {
|
|
|
1618
1654
|
this.logger.error({ root: this.rootPath, err: normalizeError(error) }, 'VCS commit failed');
|
|
1619
1655
|
}
|
|
1620
1656
|
}
|
|
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
1657
|
}
|
|
1650
1658
|
|
|
1651
1659
|
/**
|
|
@@ -1701,12 +1709,10 @@ class VcsCoordinator {
|
|
|
1701
1709
|
/**
|
|
1702
1710
|
* Start all VcsManager instances.
|
|
1703
1711
|
*/
|
|
1704
|
-
|
|
1705
|
-
const startPromises = [];
|
|
1712
|
+
start() {
|
|
1706
1713
|
this.managers.forEach((manager) => {
|
|
1707
|
-
|
|
1714
|
+
manager.start();
|
|
1708
1715
|
});
|
|
1709
|
-
await Promise.all(startPromises);
|
|
1710
1716
|
this.logger.info({ rootCount: this.managers.size }, 'VcsCoordinator started');
|
|
1711
1717
|
}
|
|
1712
1718
|
/**
|
|
@@ -1727,6 +1733,21 @@ class VcsCoordinator {
|
|
|
1727
1733
|
manager.fileChanged(normalizedPath);
|
|
1728
1734
|
}
|
|
1729
1735
|
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Signal that the initial filesystem scan is complete.
|
|
1738
|
+
*
|
|
1739
|
+
* Flushes each manager's debounce buffer before ending baseline mode, ensuring
|
|
1740
|
+
* any files still pending in the VCS debounce are committed with a
|
|
1741
|
+
* `"baseline:"` prefix rather than `"watcher:"`. AI generation is not enabled
|
|
1742
|
+
* until all managers have completed their flush.
|
|
1743
|
+
*/
|
|
1744
|
+
async onInitialScanComplete() {
|
|
1745
|
+
for (const manager of this.managers.values()) {
|
|
1746
|
+
await manager.flush();
|
|
1747
|
+
manager.endBaseline();
|
|
1748
|
+
}
|
|
1749
|
+
this.logger.debug('VcsCoordinator: initial scan complete, baseline ended');
|
|
1750
|
+
}
|
|
1730
1751
|
/**
|
|
1731
1752
|
* Flush all managers and clean up.
|
|
1732
1753
|
*/
|
|
@@ -3129,6 +3150,68 @@ async function ensureGitignore(rootPath, alwaysOnEntries = []) {
|
|
|
3129
3150
|
}
|
|
3130
3151
|
}
|
|
3131
3152
|
|
|
3153
|
+
/**
|
|
3154
|
+
* @module app/watcherFactory
|
|
3155
|
+
* Factory helpers for creating and rebuilding the filesystem watcher.
|
|
3156
|
+
* Extracted from initialization.ts to follow SRP.
|
|
3157
|
+
*/
|
|
3158
|
+
/**
|
|
3159
|
+
* Create file system watcher with gitignore filtering.
|
|
3160
|
+
*/
|
|
3161
|
+
function createWatcher(config, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
|
|
3162
|
+
const respectGitignore = config.watch.respectGitignore ?? true;
|
|
3163
|
+
const gitignoreFilter = respectGitignore
|
|
3164
|
+
? new GitignoreFilter(extractWatchPathStrings(config.watch.paths))
|
|
3165
|
+
: undefined;
|
|
3166
|
+
const watcher = factories.createFileSystemWatcher(config.watch, queue, processor, logger, {
|
|
3167
|
+
maxRetries: config.maxRetries ?? runtimeOptions.maxRetries,
|
|
3168
|
+
maxBackoffMs: config.maxBackoffMs ?? runtimeOptions.maxBackoffMs,
|
|
3169
|
+
onFatalError: runtimeOptions.onFatalError,
|
|
3170
|
+
gitignoreFilter,
|
|
3171
|
+
initialScanTracker,
|
|
3172
|
+
contentHashCache,
|
|
3173
|
+
onVcsFileChange,
|
|
3174
|
+
onInitialScanComplete,
|
|
3175
|
+
});
|
|
3176
|
+
return { watcher, gitignoreFilter };
|
|
3177
|
+
}
|
|
3178
|
+
/**
|
|
3179
|
+
* Tear down and rebuild the filesystem watcher with new config.
|
|
3180
|
+
* Falls back to the old watcher if the new one fails to start.
|
|
3181
|
+
*/
|
|
3182
|
+
async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache) {
|
|
3183
|
+
try {
|
|
3184
|
+
await oldState.watcher.stop();
|
|
3185
|
+
const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache);
|
|
3186
|
+
newWatcher.start();
|
|
3187
|
+
logger.info('Filesystem watcher rebuilt successfully');
|
|
3188
|
+
return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
|
|
3189
|
+
}
|
|
3190
|
+
catch (error) {
|
|
3191
|
+
logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
|
|
3192
|
+
try {
|
|
3193
|
+
oldState.watcher.start();
|
|
3194
|
+
}
|
|
3195
|
+
catch (restartError) {
|
|
3196
|
+
logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
|
|
3197
|
+
}
|
|
3198
|
+
return oldState;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
/**
|
|
3202
|
+
* Check whether watch-relevant config fields changed between old and new config.
|
|
3203
|
+
*/
|
|
3204
|
+
function watchConfigChanged(oldConfig, newConfig) {
|
|
3205
|
+
return (JSON.stringify(oldConfig.watch.paths) !==
|
|
3206
|
+
JSON.stringify(newConfig.watch.paths) ||
|
|
3207
|
+
JSON.stringify(oldConfig.watch.ignored) !==
|
|
3208
|
+
JSON.stringify(newConfig.watch.ignored) ||
|
|
3209
|
+
JSON.stringify(oldConfig.watch.moveDetection) !==
|
|
3210
|
+
JSON.stringify(newConfig.watch.moveDetection) ||
|
|
3211
|
+
(oldConfig.watch.respectGitignore ?? true) !==
|
|
3212
|
+
(newConfig.watch.respectGitignore ?? true));
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3132
3215
|
/**
|
|
3133
3216
|
* @module app/initialization
|
|
3134
3217
|
* Initialization helpers for JeevesWatcher. Extracted to follow SRP.
|
|
@@ -3193,25 +3276,6 @@ function createProcessorConfig(config, configDir, customMapLib) {
|
|
|
3193
3276
|
globalSchemas: config.schemas,
|
|
3194
3277
|
};
|
|
3195
3278
|
}
|
|
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
3279
|
/**
|
|
3216
3280
|
* Introspect all helpers for merged document.
|
|
3217
3281
|
*/
|
|
@@ -3227,42 +3291,6 @@ async function introspectHelpers(config, configDir) {
|
|
|
3227
3291
|
function getConfigDir(configPath) {
|
|
3228
3292
|
return configPath ? dirname(configPath) : '.';
|
|
3229
3293
|
}
|
|
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
3294
|
/**
|
|
3267
3295
|
* Resolve package version from nearest package.json.
|
|
3268
3296
|
*/
|
|
@@ -7431,6 +7459,37 @@ class MoveCorrelator {
|
|
|
7431
7459
|
}
|
|
7432
7460
|
}
|
|
7433
7461
|
|
|
7462
|
+
/**
|
|
7463
|
+
* @module watcher/ScanStats
|
|
7464
|
+
* Tracks initial filesystem scan statistics for diagnostics.
|
|
7465
|
+
*/
|
|
7466
|
+
/**
|
|
7467
|
+
* Mutable counter bag updated during the chokidar initial scan.
|
|
7468
|
+
* Extracted from the watcher start() method for clarity.
|
|
7469
|
+
*/
|
|
7470
|
+
class ScanStats {
|
|
7471
|
+
total = 0;
|
|
7472
|
+
matched = 0;
|
|
7473
|
+
globRejected = 0;
|
|
7474
|
+
gitignored = 0;
|
|
7475
|
+
byRoot;
|
|
7476
|
+
constructor(roots) {
|
|
7477
|
+
this.byRoot = Object.fromEntries(roots.map((r) => [r, 0]));
|
|
7478
|
+
}
|
|
7479
|
+
/**
|
|
7480
|
+
* Attribute a newly discovered file to its root for per-root diagnostics.
|
|
7481
|
+
*/
|
|
7482
|
+
classifyByRoot(path) {
|
|
7483
|
+
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
7484
|
+
for (const root of Object.keys(this.byRoot)) {
|
|
7485
|
+
if (normalized.startsWith(`${root}/`) || normalized === root) {
|
|
7486
|
+
this.byRoot[root] = (this.byRoot[root] ?? 0) + 1;
|
|
7487
|
+
break;
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
}
|
|
7492
|
+
|
|
7434
7493
|
/**
|
|
7435
7494
|
* @module watcher
|
|
7436
7495
|
* Filesystem watcher wrapping chokidar. I/O: watches files/directories for add/change/unlink events, enqueues to processing queue.
|
|
@@ -7448,6 +7507,7 @@ class FileSystemWatcher {
|
|
|
7448
7507
|
initialScanTracker;
|
|
7449
7508
|
contentHashCache;
|
|
7450
7509
|
onVcsFileChange;
|
|
7510
|
+
onInitialScanComplete;
|
|
7451
7511
|
moveCorrelator;
|
|
7452
7512
|
globMatches;
|
|
7453
7513
|
watcher;
|
|
@@ -7469,6 +7529,7 @@ class FileSystemWatcher {
|
|
|
7469
7529
|
this.initialScanTracker = options.initialScanTracker;
|
|
7470
7530
|
this.contentHashCache = options.contentHashCache;
|
|
7471
7531
|
this.onVcsFileChange = options.onVcsFileChange;
|
|
7532
|
+
this.onInitialScanComplete = options.onInitialScanComplete;
|
|
7472
7533
|
this.globMatches = () => true;
|
|
7473
7534
|
const healthOptions = {
|
|
7474
7535
|
maxRetries: options.maxRetries,
|
|
@@ -7496,24 +7557,9 @@ class FileSystemWatcher {
|
|
|
7496
7557
|
? resolveIgnored(this.config.ignored)
|
|
7497
7558
|
: undefined;
|
|
7498
7559
|
// 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
|
-
};
|
|
7560
|
+
const scanStats = new ScanStats(roots);
|
|
7506
7561
|
let initialScanComplete = false;
|
|
7507
7562
|
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
7563
|
// Create move correlator if move detection is configured and cache is available.
|
|
7518
7564
|
const moveConfig = this.config.moveDetection;
|
|
7519
7565
|
if (moveConfig?.enabled && this.contentHashCache) {
|
|
@@ -7546,7 +7592,7 @@ class FileSystemWatcher {
|
|
|
7546
7592
|
this.watcher.on('add', (path) => {
|
|
7547
7593
|
if (!initialScanComplete) {
|
|
7548
7594
|
scanStats.total++;
|
|
7549
|
-
classifyByRoot(path);
|
|
7595
|
+
scanStats.classifyByRoot(path);
|
|
7550
7596
|
}
|
|
7551
7597
|
this.handleGitignoreChange(path);
|
|
7552
7598
|
if (!this.globMatches(path)) {
|
|
@@ -7567,8 +7613,7 @@ class FileSystemWatcher {
|
|
|
7567
7613
|
this.initialScanTracker?.incrementEnqueued();
|
|
7568
7614
|
}
|
|
7569
7615
|
this.logger.debug({ path }, 'File added');
|
|
7570
|
-
|
|
7571
|
-
this.onVcsFileChange?.(path, 'add');
|
|
7616
|
+
this.onVcsFileChange?.(path, 'add');
|
|
7572
7617
|
if (correlator && initialScanComplete) {
|
|
7573
7618
|
void correlator.handleAdd(path);
|
|
7574
7619
|
}
|
|
@@ -7607,6 +7652,7 @@ class FileSystemWatcher {
|
|
|
7607
7652
|
initialScanComplete = true;
|
|
7608
7653
|
this.initialScanTracker?.complete();
|
|
7609
7654
|
this.logger.info({ scanStats }, 'Initial scan complete');
|
|
7655
|
+
void this.onInitialScanComplete?.();
|
|
7610
7656
|
});
|
|
7611
7657
|
this.watcher.on('error', (error) => {
|
|
7612
7658
|
this.logger.error({ err: normalizeError(error) }, 'Watcher error');
|
|
@@ -7768,6 +7814,12 @@ class JeevesWatcher {
|
|
|
7768
7814
|
* Start the watcher, API server, and all components.
|
|
7769
7815
|
*/
|
|
7770
7816
|
async start() {
|
|
7817
|
+
await this.initInfrastructure();
|
|
7818
|
+
await this.initPipeline();
|
|
7819
|
+
await this.startServicesAndWatcher();
|
|
7820
|
+
}
|
|
7821
|
+
/** Set up logger, VCS, embedding provider, and vector store. */
|
|
7822
|
+
async initInfrastructure() {
|
|
7771
7823
|
const logger = this.factories.createLogger(this.config.logging);
|
|
7772
7824
|
this.logger = logger;
|
|
7773
7825
|
await initVcs(this.config, logger);
|
|
@@ -7777,6 +7829,10 @@ class JeevesWatcher {
|
|
|
7777
7829
|
if (this.config.search?.hybrid?.enabled) {
|
|
7778
7830
|
await vectorStore.ensureTextIndex('chunk_text');
|
|
7779
7831
|
}
|
|
7832
|
+
}
|
|
7833
|
+
/** Build the processing pipeline: rules, templates, processor, queue, VCS. */
|
|
7834
|
+
async initPipeline() {
|
|
7835
|
+
const logger = this.logger;
|
|
7780
7836
|
const compiledRules = this.factories.compileRules(this.config.inferenceRules ?? []);
|
|
7781
7837
|
const configDir = getConfigDir(this.configPath);
|
|
7782
7838
|
const { templateEngine, customMapLib } = await buildTemplateEngineAndCustomMapLib(this.config, configDir);
|
|
@@ -7786,36 +7842,39 @@ class JeevesWatcher {
|
|
|
7786
7842
|
this.issuesManager = new IssuesManager(stateDir, logger);
|
|
7787
7843
|
this.valuesManager = new ValuesManager(stateDir, logger);
|
|
7788
7844
|
this.enrichmentStore = new EnrichmentStore(stateDir, logger);
|
|
7789
|
-
const enrichmentStore = this.enrichmentStore;
|
|
7790
7845
|
this.contentHashCache = new ContentHashCache();
|
|
7791
|
-
|
|
7792
|
-
const processor = this.factories.createDocumentProcessor({
|
|
7846
|
+
this.processor = this.factories.createDocumentProcessor({
|
|
7793
7847
|
config: processorConfig,
|
|
7794
|
-
embeddingProvider,
|
|
7795
|
-
vectorStore,
|
|
7848
|
+
embeddingProvider: this.embeddingProvider,
|
|
7849
|
+
vectorStore: this.vectorStore,
|
|
7796
7850
|
compiledRules,
|
|
7797
7851
|
logger,
|
|
7798
7852
|
templateEngine,
|
|
7799
|
-
enrichmentStore,
|
|
7853
|
+
enrichmentStore: this.enrichmentStore,
|
|
7800
7854
|
issuesManager: this.issuesManager,
|
|
7801
7855
|
valuesManager: this.valuesManager,
|
|
7802
|
-
contentHashCache,
|
|
7856
|
+
contentHashCache: this.contentHashCache,
|
|
7803
7857
|
});
|
|
7804
|
-
this.processor = processor;
|
|
7805
7858
|
this.queue = this.factories.createEventQueue({
|
|
7806
7859
|
debounceMs: this.config.watch.debounceMs ?? 2000,
|
|
7807
7860
|
concurrency: this.config.embedding.concurrency ?? 5,
|
|
7808
7861
|
rateLimitPerMinute: this.config.embedding.rateLimitPerMinute,
|
|
7809
7862
|
});
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7863
|
+
this.vcsCoordinator = new VcsCoordinator(this.config, logger);
|
|
7864
|
+
}
|
|
7865
|
+
/** Wire up watcher and VCS coordinator, then start all services. */
|
|
7866
|
+
async startServicesAndWatcher() {
|
|
7867
|
+
const logger = this.logger;
|
|
7868
|
+
const vcsCoordinator = this.vcsCoordinator;
|
|
7869
|
+
const { watcher, gitignoreFilter } = createWatcher(this.config, this.factories, this.queue, this.processor, logger, this.runtimeOptions, this.initialScanTracker, this.contentHashCache, (filePath, event) => {
|
|
7813
7870
|
vcsCoordinator.onFileChange(filePath, event);
|
|
7871
|
+
}, () => {
|
|
7872
|
+
void vcsCoordinator.onInitialScanComplete();
|
|
7814
7873
|
});
|
|
7815
7874
|
this.watcher = watcher;
|
|
7816
7875
|
this.gitignoreFilter = gitignoreFilter;
|
|
7817
7876
|
this.server = await this.startApiServer();
|
|
7818
|
-
|
|
7877
|
+
vcsCoordinator.start();
|
|
7819
7878
|
this.watcher.start();
|
|
7820
7879
|
this.startConfigWatch();
|
|
7821
7880
|
logger.info('jeeves-watcher started');
|