@karmaniverous/jeeves-watcher 0.18.7 → 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
package/dist/index.js
CHANGED
|
@@ -4193,6 +4193,91 @@ async function introspectAllHelpers(config, configDir) {
|
|
|
4193
4193
|
return result;
|
|
4194
4194
|
}
|
|
4195
4195
|
|
|
4196
|
+
/**
|
|
4197
|
+
* @module vcs/CommitMessageBuilder
|
|
4198
|
+
* Builds commit messages — both template-based and AI-generated.
|
|
4199
|
+
*/
|
|
4200
|
+
/**
|
|
4201
|
+
* Builds commit messages for VCS commits.
|
|
4202
|
+
* Supports template-based messages and AI-generated messages with fallback.
|
|
4203
|
+
*/
|
|
4204
|
+
class CommitMessageBuilder {
|
|
4205
|
+
rootPath;
|
|
4206
|
+
logger;
|
|
4207
|
+
generator;
|
|
4208
|
+
constructor(rootPath, logger, generator) {
|
|
4209
|
+
this.rootPath = rootPath;
|
|
4210
|
+
this.logger = logger;
|
|
4211
|
+
this.generator = generator;
|
|
4212
|
+
}
|
|
4213
|
+
/**
|
|
4214
|
+
* Build the template commit message (no AI).
|
|
4215
|
+
*/
|
|
4216
|
+
buildTemplateMessage(fileCount, isBaseline, pendingReversions) {
|
|
4217
|
+
if (pendingReversions.length === 0) {
|
|
4218
|
+
if (isBaseline) {
|
|
4219
|
+
return `baseline: batch (${String(fileCount)} files)`;
|
|
4220
|
+
}
|
|
4221
|
+
const timestamp = new Date().toISOString();
|
|
4222
|
+
return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
|
|
4223
|
+
}
|
|
4224
|
+
// Count total reverted files across all pending reversions
|
|
4225
|
+
const revertedPaths = new Set(pendingReversions.flatMap((r) => r.paths));
|
|
4226
|
+
const revertedCount = revertedPaths.size;
|
|
4227
|
+
const otherCount = fileCount - revertedCount;
|
|
4228
|
+
// Use the first reversion's glob and commit for the message
|
|
4229
|
+
const { glob, commit } = pendingReversions[0];
|
|
4230
|
+
const shortCommit = commit.slice(0, 7);
|
|
4231
|
+
let message = `revert: ${glob} to ${shortCommit} — restored ${String(revertedCount)} files`;
|
|
4232
|
+
if (otherCount > 0) {
|
|
4233
|
+
message += ` (+ ${String(otherCount)} other changes)`;
|
|
4234
|
+
}
|
|
4235
|
+
return message;
|
|
4236
|
+
}
|
|
4237
|
+
/**
|
|
4238
|
+
* Build the commit message, using AI when available with template fallback.
|
|
4239
|
+
*/
|
|
4240
|
+
async buildCommitMessage(files, isBaseline, pendingReversions) {
|
|
4241
|
+
const fileCount = files.length;
|
|
4242
|
+
const templateMessage = this.buildTemplateMessage(fileCount, isBaseline, pendingReversions);
|
|
4243
|
+
if (!this.generator) {
|
|
4244
|
+
return templateMessage;
|
|
4245
|
+
}
|
|
4246
|
+
try {
|
|
4247
|
+
const diffSummary = await this.getStagedDiff();
|
|
4248
|
+
const aiMessage = await this.generator.generate(files, diffSummary);
|
|
4249
|
+
if (!aiMessage) {
|
|
4250
|
+
this.logger.warn('AI commit message generation returned null; using template');
|
|
4251
|
+
return templateMessage;
|
|
4252
|
+
}
|
|
4253
|
+
// For reversions: prefix with revert info + AI description
|
|
4254
|
+
if (pendingReversions.length > 0) {
|
|
4255
|
+
const { glob, commit } = pendingReversions[0];
|
|
4256
|
+
const shortCommit = commit.slice(0, 7);
|
|
4257
|
+
return `revert: ${glob} to ${shortCommit} — ${aiMessage}`;
|
|
4258
|
+
}
|
|
4259
|
+
return aiMessage;
|
|
4260
|
+
}
|
|
4261
|
+
catch (error) {
|
|
4262
|
+
this.logger.warn({ err: normalizeError(error) }, 'AI commit message generation failed; using template');
|
|
4263
|
+
return templateMessage;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
/**
|
|
4267
|
+
* Get the staged diff for AI context.
|
|
4268
|
+
*/
|
|
4269
|
+
async getStagedDiff() {
|
|
4270
|
+
try {
|
|
4271
|
+
const { stdout: stat } = await execFileAsync('git', ['diff', '--cached', '--stat'], { cwd: this.rootPath });
|
|
4272
|
+
const { stdout: patch } = await execFileAsync('git', ['diff', '--cached'], { cwd: this.rootPath });
|
|
4273
|
+
return `${stat}\n${patch}`;
|
|
4274
|
+
}
|
|
4275
|
+
catch {
|
|
4276
|
+
return '';
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4196
4281
|
/**
|
|
4197
4282
|
* @module vcs/CommitMessageGenerator
|
|
4198
4283
|
* Generates AI-powered commit messages via the Anthropic Messages API.
|
|
@@ -4819,6 +4904,50 @@ function resolveIgnored(ignored) {
|
|
|
4819
4904
|
});
|
|
4820
4905
|
}
|
|
4821
4906
|
|
|
4907
|
+
/**
|
|
4908
|
+
* @module vcs/vcsPush
|
|
4909
|
+
* Push logic for VCS commits.
|
|
4910
|
+
*/
|
|
4911
|
+
/**
|
|
4912
|
+
* Push to the configured remote if remoteUrl is set.
|
|
4913
|
+
*
|
|
4914
|
+
* On failure: logs the error, appends to pushErrors, and returns undefined.
|
|
4915
|
+
* Never throws.
|
|
4916
|
+
*
|
|
4917
|
+
* @param rootPath - Absolute path of the git repository.
|
|
4918
|
+
* @param remoteUrl - Remote URL to push to, or undefined to skip.
|
|
4919
|
+
* @param accessToken - Optional token for HTTPS authentication.
|
|
4920
|
+
* @param pushErrors - Mutable array where push errors are recorded.
|
|
4921
|
+
* @param logger - Logger instance.
|
|
4922
|
+
* @returns ISO timestamp of a successful push, or undefined if skipped/failed.
|
|
4923
|
+
*/
|
|
4924
|
+
async function pushToRemote(rootPath, remoteUrl, accessToken, pushErrors, logger) {
|
|
4925
|
+
if (!remoteUrl)
|
|
4926
|
+
return undefined;
|
|
4927
|
+
try {
|
|
4928
|
+
// Build the authenticated URL if a token is available
|
|
4929
|
+
const pushUrl = accessToken
|
|
4930
|
+
? remoteUrl.replace(/^https:\/\//, `https://${encodeURIComponent(accessToken)}@`)
|
|
4931
|
+
: remoteUrl;
|
|
4932
|
+
await execFileAsync('git', ['push', pushUrl, 'HEAD'], {
|
|
4933
|
+
cwd: rootPath,
|
|
4934
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
4935
|
+
});
|
|
4936
|
+
const timestamp = new Date().toISOString();
|
|
4937
|
+
logger.info({ root: rootPath, remote: remoteUrl }, 'VCS push succeeded');
|
|
4938
|
+
return timestamp;
|
|
4939
|
+
}
|
|
4940
|
+
catch (error) {
|
|
4941
|
+
const pushError = {
|
|
4942
|
+
timestamp: new Date().toISOString(),
|
|
4943
|
+
message: normalizeError(error).message,
|
|
4944
|
+
};
|
|
4945
|
+
pushErrors.push(pushError);
|
|
4946
|
+
logger.error({ root: rootPath, err: normalizeError(error) }, 'VCS push failed');
|
|
4947
|
+
return undefined;
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4822
4951
|
/**
|
|
4823
4952
|
* @module vcs/VcsManager
|
|
4824
4953
|
* Per-root VCS manager for git-backed content versioning.
|
|
@@ -4839,7 +4968,7 @@ class VcsManager {
|
|
|
4839
4968
|
remoteUrl;
|
|
4840
4969
|
accessToken;
|
|
4841
4970
|
logger;
|
|
4842
|
-
|
|
4971
|
+
commitMessageBuilder;
|
|
4843
4972
|
pending = new Set();
|
|
4844
4973
|
pendingReversions = [];
|
|
4845
4974
|
_pushErrors = [];
|
|
@@ -4847,15 +4976,15 @@ class VcsManager {
|
|
|
4847
4976
|
debounceTimer;
|
|
4848
4977
|
commitInFlight = Promise.resolve();
|
|
4849
4978
|
started = false;
|
|
4850
|
-
isBaseline =
|
|
4979
|
+
isBaseline = true;
|
|
4851
4980
|
_lastPushTime = null;
|
|
4852
4981
|
constructor(rootPath, config, logger, commitMessageGenerator, remoteUrl, accessToken) {
|
|
4853
4982
|
this.rootPath = rootPath;
|
|
4854
4983
|
this.config = config;
|
|
4855
4984
|
this.logger = logger;
|
|
4856
|
-
this.commitMessageGenerator = commitMessageGenerator;
|
|
4857
4985
|
this.remoteUrl = remoteUrl;
|
|
4858
4986
|
this.accessToken = accessToken;
|
|
4987
|
+
this.commitMessageBuilder = new CommitMessageBuilder(rootPath, logger, commitMessageGenerator);
|
|
4859
4988
|
if (config.retention) {
|
|
4860
4989
|
this.squashManager = new SquashManager(rootPath, config.retention, logger, remoteUrl, accessToken);
|
|
4861
4990
|
}
|
|
@@ -4867,48 +4996,23 @@ class VcsManager {
|
|
|
4867
4996
|
return this._pushErrors;
|
|
4868
4997
|
}
|
|
4869
4998
|
/**
|
|
4870
|
-
* Begin accepting file changes. Sets
|
|
4871
|
-
*
|
|
4999
|
+
* Begin accepting file changes. Sets the manager to started state and
|
|
5000
|
+
* starts the squash manager if configured.
|
|
5001
|
+
*
|
|
5002
|
+
* Baseline mode (commit prefix `"baseline:"`) remains active until
|
|
5003
|
+
* {@link endBaseline} is called by the coordinator after the initial scan.
|
|
4872
5004
|
*/
|
|
4873
|
-
|
|
5005
|
+
start() {
|
|
4874
5006
|
this.started = true;
|
|
4875
5007
|
this.squashManager?.start();
|
|
4876
5008
|
this.logger.info({ root: this.rootPath }, 'VcsManager started');
|
|
4877
|
-
await this.enqueueBaselineIfNeeded();
|
|
4878
5009
|
}
|
|
4879
5010
|
/**
|
|
4880
|
-
*
|
|
4881
|
-
*
|
|
5011
|
+
* Signal that the initial filesystem scan is complete.
|
|
5012
|
+
* Clears the baseline flag so subsequent commits use normal "watcher: batch" messages.
|
|
4882
5013
|
*/
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
4886
|
-
cwd: this.rootPath,
|
|
4887
|
-
});
|
|
4888
|
-
// HEAD exists — repo already has commits
|
|
4889
|
-
return;
|
|
4890
|
-
}
|
|
4891
|
-
catch {
|
|
4892
|
-
// No commits yet — proceed with baseline
|
|
4893
|
-
}
|
|
4894
|
-
try {
|
|
4895
|
-
const { stdout } = await execFileAsync('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd: this.rootPath });
|
|
4896
|
-
const files = stdout
|
|
4897
|
-
.split('\0')
|
|
4898
|
-
.map((line) => line.trim())
|
|
4899
|
-
.filter((line) => line.length > 0);
|
|
4900
|
-
if (files.length === 0)
|
|
4901
|
-
return;
|
|
4902
|
-
this.logger.info({ root: this.rootPath, fileCount: files.length }, 'Enqueuing baseline commit for pre-existing files');
|
|
4903
|
-
this.isBaseline = true;
|
|
4904
|
-
const absolutePaths = files.map((f) => this.rootPath + '/' + f);
|
|
4905
|
-
for (const filePath of absolutePaths) {
|
|
4906
|
-
this.fileChanged(filePath);
|
|
4907
|
-
}
|
|
4908
|
-
}
|
|
4909
|
-
catch (error) {
|
|
4910
|
-
this.logger.warn({ root: this.rootPath, err: normalizeError(error) }, 'Failed to enumerate untracked files for baseline commit');
|
|
4911
|
-
}
|
|
5014
|
+
endBaseline() {
|
|
5015
|
+
this.isBaseline = false;
|
|
4912
5016
|
}
|
|
4913
5017
|
/**
|
|
4914
5018
|
* Add a file to the pending set and reset the debounce timer.
|
|
@@ -5005,72 +5109,6 @@ class VcsManager {
|
|
|
5005
5109
|
void this.flush();
|
|
5006
5110
|
}, this.config.commitDebounceMs);
|
|
5007
5111
|
}
|
|
5008
|
-
/**
|
|
5009
|
-
* Get the cached diff for staged files, for AI context.
|
|
5010
|
-
*/
|
|
5011
|
-
async getStagedDiff() {
|
|
5012
|
-
try {
|
|
5013
|
-
const { stdout: stat } = await execFileAsync('git', ['diff', '--cached', '--stat'], { cwd: this.rootPath });
|
|
5014
|
-
const { stdout: patch } = await execFileAsync('git', ['diff', '--cached'], { cwd: this.rootPath });
|
|
5015
|
-
return `${stat}\n${patch}`;
|
|
5016
|
-
}
|
|
5017
|
-
catch {
|
|
5018
|
-
return '';
|
|
5019
|
-
}
|
|
5020
|
-
}
|
|
5021
|
-
/**
|
|
5022
|
-
* Build the template commit message (no AI).
|
|
5023
|
-
*/
|
|
5024
|
-
buildTemplateMessage(fileCount) {
|
|
5025
|
-
if (this.pendingReversions.length === 0) {
|
|
5026
|
-
if (this.isBaseline) {
|
|
5027
|
-
return `baseline: batch (${String(fileCount)} files)`;
|
|
5028
|
-
}
|
|
5029
|
-
const timestamp = new Date().toISOString();
|
|
5030
|
-
return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
|
|
5031
|
-
}
|
|
5032
|
-
// Count total reverted files across all pending reversions
|
|
5033
|
-
const revertedPaths = new Set(this.pendingReversions.flatMap((r) => r.paths));
|
|
5034
|
-
const revertedCount = revertedPaths.size;
|
|
5035
|
-
const otherCount = fileCount - revertedCount;
|
|
5036
|
-
// Use the first reversion's glob and commit for the message
|
|
5037
|
-
const { glob, commit } = this.pendingReversions[0];
|
|
5038
|
-
const shortCommit = commit.slice(0, 7);
|
|
5039
|
-
let message = `revert: ${glob} to ${shortCommit} — restored ${String(revertedCount)} files`;
|
|
5040
|
-
if (otherCount > 0) {
|
|
5041
|
-
message += ` (+ ${String(otherCount)} other changes)`;
|
|
5042
|
-
}
|
|
5043
|
-
return message;
|
|
5044
|
-
}
|
|
5045
|
-
/**
|
|
5046
|
-
* Build the commit message, using AI when available with template fallback.
|
|
5047
|
-
*/
|
|
5048
|
-
async buildCommitMessage(files) {
|
|
5049
|
-
const fileCount = files.length;
|
|
5050
|
-
const templateMessage = this.buildTemplateMessage(fileCount);
|
|
5051
|
-
if (!this.commitMessageGenerator) {
|
|
5052
|
-
return templateMessage;
|
|
5053
|
-
}
|
|
5054
|
-
try {
|
|
5055
|
-
const diffSummary = await this.getStagedDiff();
|
|
5056
|
-
const aiMessage = await this.commitMessageGenerator.generate(files, diffSummary);
|
|
5057
|
-
if (!aiMessage) {
|
|
5058
|
-
this.logger.warn('AI commit message generation returned null; using template');
|
|
5059
|
-
return templateMessage;
|
|
5060
|
-
}
|
|
5061
|
-
// For reversions: prefix with revert info + AI description
|
|
5062
|
-
if (this.pendingReversions.length > 0) {
|
|
5063
|
-
const { glob, commit } = this.pendingReversions[0];
|
|
5064
|
-
const shortCommit = commit.slice(0, 7);
|
|
5065
|
-
return `revert: ${glob} to ${shortCommit} — ${aiMessage}`;
|
|
5066
|
-
}
|
|
5067
|
-
return aiMessage;
|
|
5068
|
-
}
|
|
5069
|
-
catch (error) {
|
|
5070
|
-
this.logger.warn({ err: normalizeError(error) }, 'AI commit message generation failed; using template');
|
|
5071
|
-
return templateMessage;
|
|
5072
|
-
}
|
|
5073
|
-
}
|
|
5074
5112
|
/**
|
|
5075
5113
|
* Commit a batch of files to the git repo with index.lock retry.
|
|
5076
5114
|
*/
|
|
@@ -5080,23 +5118,21 @@ class VcsManager {
|
|
|
5080
5118
|
try {
|
|
5081
5119
|
await retry(async (attempt) => {
|
|
5082
5120
|
await gitAddViaStdin(files, this.rootPath);
|
|
5083
|
-
// Build message after staging so getStagedDiff can see the changes
|
|
5084
|
-
// Skip AI for baselines and retries — use template directly
|
|
5121
|
+
// Build message after staging so getStagedDiff can see the changes.
|
|
5122
|
+
// Skip AI for baselines and retries — use template directly.
|
|
5085
5123
|
const message = this.isBaseline || attempt > 1
|
|
5086
|
-
? this.buildTemplateMessage(files.length)
|
|
5087
|
-
: await this.buildCommitMessage(files);
|
|
5124
|
+
? this.commitMessageBuilder.buildTemplateMessage(files.length, this.isBaseline, this.pendingReversions)
|
|
5125
|
+
: await this.commitMessageBuilder.buildCommitMessage(files, this.isBaseline, this.pendingReversions);
|
|
5088
5126
|
this.pendingReversions.length = 0;
|
|
5089
|
-
// Only clear baseline flag when all baseline files have been committed
|
|
5090
|
-
if (this.pending.size === 0) {
|
|
5091
|
-
this.isBaseline = false;
|
|
5092
|
-
}
|
|
5093
5127
|
await execFileAsync('git', ['commit', '-m', message], {
|
|
5094
5128
|
cwd: this.rootPath,
|
|
5095
5129
|
});
|
|
5096
5130
|
const { stdout: hashOut } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], { cwd: this.rootPath });
|
|
5097
5131
|
const hash = hashOut.trim();
|
|
5098
5132
|
this.logger.info({ root: this.rootPath, hash, fileCount: files.length }, 'VCS commit created');
|
|
5099
|
-
await this.
|
|
5133
|
+
const pushTime = await pushToRemote(this.rootPath, this.remoteUrl, this.accessToken, this._pushErrors, this.logger);
|
|
5134
|
+
if (pushTime)
|
|
5135
|
+
this._lastPushTime = pushTime;
|
|
5100
5136
|
}, {
|
|
5101
5137
|
attempts: 4,
|
|
5102
5138
|
baseDelayMs: 500,
|
|
@@ -5114,34 +5150,6 @@ class VcsManager {
|
|
|
5114
5150
|
this.logger.error({ root: this.rootPath, err: normalizeError(error) }, 'VCS commit failed');
|
|
5115
5151
|
}
|
|
5116
5152
|
}
|
|
5117
|
-
/**
|
|
5118
|
-
* Push to the configured remote if remoteUrl is set.
|
|
5119
|
-
* On failure: logs the error and records it in pushErrors; never throws.
|
|
5120
|
-
*/
|
|
5121
|
-
async pushIfConfigured() {
|
|
5122
|
-
if (!this.remoteUrl)
|
|
5123
|
-
return;
|
|
5124
|
-
try {
|
|
5125
|
-
// Build the authenticated URL if a token is available
|
|
5126
|
-
const pushUrl = this.accessToken
|
|
5127
|
-
? this.remoteUrl.replace(/^https:\/\//, `https://${encodeURIComponent(this.accessToken)}@`)
|
|
5128
|
-
: this.remoteUrl;
|
|
5129
|
-
await execFileAsync('git', ['push', pushUrl, 'HEAD'], {
|
|
5130
|
-
cwd: this.rootPath,
|
|
5131
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
5132
|
-
});
|
|
5133
|
-
this._lastPushTime = new Date().toISOString();
|
|
5134
|
-
this.logger.info({ root: this.rootPath, remote: this.remoteUrl }, 'VCS push succeeded');
|
|
5135
|
-
}
|
|
5136
|
-
catch (error) {
|
|
5137
|
-
const pushError = {
|
|
5138
|
-
timestamp: new Date().toISOString(),
|
|
5139
|
-
message: normalizeError(error).message,
|
|
5140
|
-
};
|
|
5141
|
-
this._pushErrors.push(pushError);
|
|
5142
|
-
this.logger.error({ root: this.rootPath, err: normalizeError(error) }, 'VCS push failed');
|
|
5143
|
-
}
|
|
5144
|
-
}
|
|
5145
5153
|
}
|
|
5146
5154
|
|
|
5147
5155
|
/**
|
|
@@ -5197,12 +5205,10 @@ class VcsCoordinator {
|
|
|
5197
5205
|
/**
|
|
5198
5206
|
* Start all VcsManager instances.
|
|
5199
5207
|
*/
|
|
5200
|
-
|
|
5201
|
-
const startPromises = [];
|
|
5208
|
+
start() {
|
|
5202
5209
|
this.managers.forEach((manager) => {
|
|
5203
|
-
|
|
5210
|
+
manager.start();
|
|
5204
5211
|
});
|
|
5205
|
-
await Promise.all(startPromises);
|
|
5206
5212
|
this.logger.info({ rootCount: this.managers.size }, 'VcsCoordinator started');
|
|
5207
5213
|
}
|
|
5208
5214
|
/**
|
|
@@ -5223,6 +5229,21 @@ class VcsCoordinator {
|
|
|
5223
5229
|
manager.fileChanged(normalizedPath);
|
|
5224
5230
|
}
|
|
5225
5231
|
}
|
|
5232
|
+
/**
|
|
5233
|
+
* Signal that the initial filesystem scan is complete.
|
|
5234
|
+
*
|
|
5235
|
+
* Flushes each manager's debounce buffer before ending baseline mode, ensuring
|
|
5236
|
+
* any files still pending in the VCS debounce are committed with a
|
|
5237
|
+
* `"baseline:"` prefix rather than `"watcher:"`. AI generation is not enabled
|
|
5238
|
+
* until all managers have completed their flush.
|
|
5239
|
+
*/
|
|
5240
|
+
async onInitialScanComplete() {
|
|
5241
|
+
for (const manager of this.managers.values()) {
|
|
5242
|
+
await manager.flush();
|
|
5243
|
+
manager.endBaseline();
|
|
5244
|
+
}
|
|
5245
|
+
this.logger.debug('VcsCoordinator: initial scan complete, baseline ended');
|
|
5246
|
+
}
|
|
5226
5247
|
/**
|
|
5227
5248
|
* Flush all managers and clean up.
|
|
5228
5249
|
*/
|
|
@@ -5265,6 +5286,68 @@ class VcsCoordinator {
|
|
|
5265
5286
|
}
|
|
5266
5287
|
}
|
|
5267
5288
|
|
|
5289
|
+
/**
|
|
5290
|
+
* @module app/watcherFactory
|
|
5291
|
+
* Factory helpers for creating and rebuilding the filesystem watcher.
|
|
5292
|
+
* Extracted from initialization.ts to follow SRP.
|
|
5293
|
+
*/
|
|
5294
|
+
/**
|
|
5295
|
+
* Create file system watcher with gitignore filtering.
|
|
5296
|
+
*/
|
|
5297
|
+
function createWatcher(config, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange, onInitialScanComplete) {
|
|
5298
|
+
const respectGitignore = config.watch.respectGitignore ?? true;
|
|
5299
|
+
const gitignoreFilter = respectGitignore
|
|
5300
|
+
? new GitignoreFilter(extractWatchPathStrings(config.watch.paths))
|
|
5301
|
+
: undefined;
|
|
5302
|
+
const watcher = factories.createFileSystemWatcher(config.watch, queue, processor, logger, {
|
|
5303
|
+
maxRetries: config.maxRetries ?? runtimeOptions.maxRetries,
|
|
5304
|
+
maxBackoffMs: config.maxBackoffMs ?? runtimeOptions.maxBackoffMs,
|
|
5305
|
+
onFatalError: runtimeOptions.onFatalError,
|
|
5306
|
+
gitignoreFilter,
|
|
5307
|
+
initialScanTracker,
|
|
5308
|
+
contentHashCache,
|
|
5309
|
+
onVcsFileChange,
|
|
5310
|
+
onInitialScanComplete,
|
|
5311
|
+
});
|
|
5312
|
+
return { watcher, gitignoreFilter };
|
|
5313
|
+
}
|
|
5314
|
+
/**
|
|
5315
|
+
* Tear down and rebuild the filesystem watcher with new config.
|
|
5316
|
+
* Falls back to the old watcher if the new one fails to start.
|
|
5317
|
+
*/
|
|
5318
|
+
async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache) {
|
|
5319
|
+
try {
|
|
5320
|
+
await oldState.watcher.stop();
|
|
5321
|
+
const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache);
|
|
5322
|
+
newWatcher.start();
|
|
5323
|
+
logger.info('Filesystem watcher rebuilt successfully');
|
|
5324
|
+
return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
|
|
5325
|
+
}
|
|
5326
|
+
catch (error) {
|
|
5327
|
+
logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
|
|
5328
|
+
try {
|
|
5329
|
+
oldState.watcher.start();
|
|
5330
|
+
}
|
|
5331
|
+
catch (restartError) {
|
|
5332
|
+
logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
|
|
5333
|
+
}
|
|
5334
|
+
return oldState;
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
/**
|
|
5338
|
+
* Check whether watch-relevant config fields changed between old and new config.
|
|
5339
|
+
*/
|
|
5340
|
+
function watchConfigChanged(oldConfig, newConfig) {
|
|
5341
|
+
return (JSON.stringify(oldConfig.watch.paths) !==
|
|
5342
|
+
JSON.stringify(newConfig.watch.paths) ||
|
|
5343
|
+
JSON.stringify(oldConfig.watch.ignored) !==
|
|
5344
|
+
JSON.stringify(newConfig.watch.ignored) ||
|
|
5345
|
+
JSON.stringify(oldConfig.watch.moveDetection) !==
|
|
5346
|
+
JSON.stringify(newConfig.watch.moveDetection) ||
|
|
5347
|
+
(oldConfig.watch.respectGitignore ?? true) !==
|
|
5348
|
+
(newConfig.watch.respectGitignore ?? true));
|
|
5349
|
+
}
|
|
5350
|
+
|
|
5268
5351
|
/**
|
|
5269
5352
|
* @module app/initialization
|
|
5270
5353
|
* Initialization helpers for JeevesWatcher. Extracted to follow SRP.
|
|
@@ -5329,25 +5412,6 @@ function createProcessorConfig(config, configDir, customMapLib) {
|
|
|
5329
5412
|
globalSchemas: config.schemas,
|
|
5330
5413
|
};
|
|
5331
5414
|
}
|
|
5332
|
-
/**
|
|
5333
|
-
* Create file system watcher with gitignore filtering.
|
|
5334
|
-
*/
|
|
5335
|
-
function createWatcher(config, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache, onVcsFileChange) {
|
|
5336
|
-
const respectGitignore = config.watch.respectGitignore ?? true;
|
|
5337
|
-
const gitignoreFilter = respectGitignore
|
|
5338
|
-
? new GitignoreFilter(extractWatchPathStrings(config.watch.paths))
|
|
5339
|
-
: undefined;
|
|
5340
|
-
const watcher = factories.createFileSystemWatcher(config.watch, queue, processor, logger, {
|
|
5341
|
-
maxRetries: config.maxRetries ?? runtimeOptions.maxRetries,
|
|
5342
|
-
maxBackoffMs: config.maxBackoffMs ?? runtimeOptions.maxBackoffMs,
|
|
5343
|
-
onFatalError: runtimeOptions.onFatalError,
|
|
5344
|
-
gitignoreFilter,
|
|
5345
|
-
initialScanTracker,
|
|
5346
|
-
contentHashCache,
|
|
5347
|
-
onVcsFileChange,
|
|
5348
|
-
});
|
|
5349
|
-
return { watcher, gitignoreFilter };
|
|
5350
|
-
}
|
|
5351
5415
|
/**
|
|
5352
5416
|
* Introspect all helpers for merged document.
|
|
5353
5417
|
*/
|
|
@@ -5363,42 +5427,6 @@ async function introspectHelpers(config, configDir) {
|
|
|
5363
5427
|
function getConfigDir(configPath) {
|
|
5364
5428
|
return configPath ? dirname(configPath) : '.';
|
|
5365
5429
|
}
|
|
5366
|
-
/**
|
|
5367
|
-
* Tear down and rebuild the filesystem watcher with new config.
|
|
5368
|
-
* Falls back to the old watcher if the new one fails to start.
|
|
5369
|
-
*/
|
|
5370
|
-
async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache) {
|
|
5371
|
-
try {
|
|
5372
|
-
await oldState.watcher.stop();
|
|
5373
|
-
const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache);
|
|
5374
|
-
newWatcher.start();
|
|
5375
|
-
logger.info('Filesystem watcher rebuilt successfully');
|
|
5376
|
-
return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
|
|
5377
|
-
}
|
|
5378
|
-
catch (error) {
|
|
5379
|
-
logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
|
|
5380
|
-
try {
|
|
5381
|
-
oldState.watcher.start();
|
|
5382
|
-
}
|
|
5383
|
-
catch (restartError) {
|
|
5384
|
-
logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
|
|
5385
|
-
}
|
|
5386
|
-
return oldState;
|
|
5387
|
-
}
|
|
5388
|
-
}
|
|
5389
|
-
/**
|
|
5390
|
-
* Check whether watch-relevant config fields changed between old and new config.
|
|
5391
|
-
*/
|
|
5392
|
-
function watchConfigChanged(oldConfig, newConfig) {
|
|
5393
|
-
return (JSON.stringify(oldConfig.watch.paths) !==
|
|
5394
|
-
JSON.stringify(newConfig.watch.paths) ||
|
|
5395
|
-
JSON.stringify(oldConfig.watch.ignored) !==
|
|
5396
|
-
JSON.stringify(newConfig.watch.ignored) ||
|
|
5397
|
-
JSON.stringify(oldConfig.watch.moveDetection) !==
|
|
5398
|
-
JSON.stringify(newConfig.watch.moveDetection) ||
|
|
5399
|
-
(oldConfig.watch.respectGitignore ?? true) !==
|
|
5400
|
-
(newConfig.watch.respectGitignore ?? true));
|
|
5401
|
-
}
|
|
5402
5430
|
/**
|
|
5403
5431
|
* Resolve package version from nearest package.json.
|
|
5404
5432
|
*/
|
|
@@ -8470,6 +8498,37 @@ class MoveCorrelator {
|
|
|
8470
8498
|
}
|
|
8471
8499
|
}
|
|
8472
8500
|
|
|
8501
|
+
/**
|
|
8502
|
+
* @module watcher/ScanStats
|
|
8503
|
+
* Tracks initial filesystem scan statistics for diagnostics.
|
|
8504
|
+
*/
|
|
8505
|
+
/**
|
|
8506
|
+
* Mutable counter bag updated during the chokidar initial scan.
|
|
8507
|
+
* Extracted from the watcher start() method for clarity.
|
|
8508
|
+
*/
|
|
8509
|
+
class ScanStats {
|
|
8510
|
+
total = 0;
|
|
8511
|
+
matched = 0;
|
|
8512
|
+
globRejected = 0;
|
|
8513
|
+
gitignored = 0;
|
|
8514
|
+
byRoot;
|
|
8515
|
+
constructor(roots) {
|
|
8516
|
+
this.byRoot = Object.fromEntries(roots.map((r) => [r, 0]));
|
|
8517
|
+
}
|
|
8518
|
+
/**
|
|
8519
|
+
* Attribute a newly discovered file to its root for per-root diagnostics.
|
|
8520
|
+
*/
|
|
8521
|
+
classifyByRoot(path) {
|
|
8522
|
+
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
8523
|
+
for (const root of Object.keys(this.byRoot)) {
|
|
8524
|
+
if (normalized.startsWith(`${root}/`) || normalized === root) {
|
|
8525
|
+
this.byRoot[root] = (this.byRoot[root] ?? 0) + 1;
|
|
8526
|
+
break;
|
|
8527
|
+
}
|
|
8528
|
+
}
|
|
8529
|
+
}
|
|
8530
|
+
}
|
|
8531
|
+
|
|
8473
8532
|
/**
|
|
8474
8533
|
* @module watcher
|
|
8475
8534
|
* Filesystem watcher wrapping chokidar. I/O: watches files/directories for add/change/unlink events, enqueues to processing queue.
|
|
@@ -8487,6 +8546,7 @@ class FileSystemWatcher {
|
|
|
8487
8546
|
initialScanTracker;
|
|
8488
8547
|
contentHashCache;
|
|
8489
8548
|
onVcsFileChange;
|
|
8549
|
+
onInitialScanComplete;
|
|
8490
8550
|
moveCorrelator;
|
|
8491
8551
|
globMatches;
|
|
8492
8552
|
watcher;
|
|
@@ -8508,6 +8568,7 @@ class FileSystemWatcher {
|
|
|
8508
8568
|
this.initialScanTracker = options.initialScanTracker;
|
|
8509
8569
|
this.contentHashCache = options.contentHashCache;
|
|
8510
8570
|
this.onVcsFileChange = options.onVcsFileChange;
|
|
8571
|
+
this.onInitialScanComplete = options.onInitialScanComplete;
|
|
8511
8572
|
this.globMatches = () => true;
|
|
8512
8573
|
const healthOptions = {
|
|
8513
8574
|
maxRetries: options.maxRetries,
|
|
@@ -8535,24 +8596,9 @@ class FileSystemWatcher {
|
|
|
8535
8596
|
? resolveIgnored(this.config.ignored)
|
|
8536
8597
|
: undefined;
|
|
8537
8598
|
// Track initial scan statistics per root for diagnostics.
|
|
8538
|
-
const scanStats =
|
|
8539
|
-
total: 0,
|
|
8540
|
-
matched: 0,
|
|
8541
|
-
globRejected: 0,
|
|
8542
|
-
gitignored: 0,
|
|
8543
|
-
byRoot: Object.fromEntries(roots.map((r) => [r, 0])),
|
|
8544
|
-
};
|
|
8599
|
+
const scanStats = new ScanStats(roots);
|
|
8545
8600
|
let initialScanComplete = false;
|
|
8546
8601
|
this.initialScanTracker?.start();
|
|
8547
|
-
const classifyByRoot = (path) => {
|
|
8548
|
-
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
8549
|
-
for (const root of roots) {
|
|
8550
|
-
if (normalized.startsWith(root + '/') || normalized === root) {
|
|
8551
|
-
scanStats.byRoot[root] = (scanStats.byRoot[root] ?? 0) + 1;
|
|
8552
|
-
break;
|
|
8553
|
-
}
|
|
8554
|
-
}
|
|
8555
|
-
};
|
|
8556
8602
|
// Create move correlator if move detection is configured and cache is available.
|
|
8557
8603
|
const moveConfig = this.config.moveDetection;
|
|
8558
8604
|
if (moveConfig?.enabled && this.contentHashCache) {
|
|
@@ -8585,7 +8631,7 @@ class FileSystemWatcher {
|
|
|
8585
8631
|
this.watcher.on('add', (path) => {
|
|
8586
8632
|
if (!initialScanComplete) {
|
|
8587
8633
|
scanStats.total++;
|
|
8588
|
-
classifyByRoot(path);
|
|
8634
|
+
scanStats.classifyByRoot(path);
|
|
8589
8635
|
}
|
|
8590
8636
|
this.handleGitignoreChange(path);
|
|
8591
8637
|
if (!this.globMatches(path)) {
|
|
@@ -8606,8 +8652,7 @@ class FileSystemWatcher {
|
|
|
8606
8652
|
this.initialScanTracker?.incrementEnqueued();
|
|
8607
8653
|
}
|
|
8608
8654
|
this.logger.debug({ path }, 'File added');
|
|
8609
|
-
|
|
8610
|
-
this.onVcsFileChange?.(path, 'add');
|
|
8655
|
+
this.onVcsFileChange?.(path, 'add');
|
|
8611
8656
|
if (correlator && initialScanComplete) {
|
|
8612
8657
|
void correlator.handleAdd(path);
|
|
8613
8658
|
}
|
|
@@ -8646,6 +8691,7 @@ class FileSystemWatcher {
|
|
|
8646
8691
|
initialScanComplete = true;
|
|
8647
8692
|
this.initialScanTracker?.complete();
|
|
8648
8693
|
this.logger.info({ scanStats }, 'Initial scan complete');
|
|
8694
|
+
void this.onInitialScanComplete?.();
|
|
8649
8695
|
});
|
|
8650
8696
|
this.watcher.on('error', (error) => {
|
|
8651
8697
|
this.logger.error({ err: normalizeError(error) }, 'Watcher error');
|
|
@@ -8919,6 +8965,12 @@ class JeevesWatcher {
|
|
|
8919
8965
|
* Start the watcher, API server, and all components.
|
|
8920
8966
|
*/
|
|
8921
8967
|
async start() {
|
|
8968
|
+
await this.initInfrastructure();
|
|
8969
|
+
await this.initPipeline();
|
|
8970
|
+
await this.startServicesAndWatcher();
|
|
8971
|
+
}
|
|
8972
|
+
/** Set up logger, VCS, embedding provider, and vector store. */
|
|
8973
|
+
async initInfrastructure() {
|
|
8922
8974
|
const logger = this.factories.createLogger(this.config.logging);
|
|
8923
8975
|
this.logger = logger;
|
|
8924
8976
|
await initVcs(this.config, logger);
|
|
@@ -8928,6 +8980,10 @@ class JeevesWatcher {
|
|
|
8928
8980
|
if (this.config.search?.hybrid?.enabled) {
|
|
8929
8981
|
await vectorStore.ensureTextIndex('chunk_text');
|
|
8930
8982
|
}
|
|
8983
|
+
}
|
|
8984
|
+
/** Build the processing pipeline: rules, templates, processor, queue, VCS. */
|
|
8985
|
+
async initPipeline() {
|
|
8986
|
+
const logger = this.logger;
|
|
8931
8987
|
const compiledRules = this.factories.compileRules(this.config.inferenceRules ?? []);
|
|
8932
8988
|
const configDir = getConfigDir(this.configPath);
|
|
8933
8989
|
const { templateEngine, customMapLib } = await buildTemplateEngineAndCustomMapLib(this.config, configDir);
|
|
@@ -8937,36 +8993,39 @@ class JeevesWatcher {
|
|
|
8937
8993
|
this.issuesManager = new IssuesManager(stateDir, logger);
|
|
8938
8994
|
this.valuesManager = new ValuesManager(stateDir, logger);
|
|
8939
8995
|
this.enrichmentStore = new EnrichmentStore(stateDir, logger);
|
|
8940
|
-
const enrichmentStore = this.enrichmentStore;
|
|
8941
8996
|
this.contentHashCache = new ContentHashCache();
|
|
8942
|
-
|
|
8943
|
-
const processor = this.factories.createDocumentProcessor({
|
|
8997
|
+
this.processor = this.factories.createDocumentProcessor({
|
|
8944
8998
|
config: processorConfig,
|
|
8945
|
-
embeddingProvider,
|
|
8946
|
-
vectorStore,
|
|
8999
|
+
embeddingProvider: this.embeddingProvider,
|
|
9000
|
+
vectorStore: this.vectorStore,
|
|
8947
9001
|
compiledRules,
|
|
8948
9002
|
logger,
|
|
8949
9003
|
templateEngine,
|
|
8950
|
-
enrichmentStore,
|
|
9004
|
+
enrichmentStore: this.enrichmentStore,
|
|
8951
9005
|
issuesManager: this.issuesManager,
|
|
8952
9006
|
valuesManager: this.valuesManager,
|
|
8953
|
-
contentHashCache,
|
|
9007
|
+
contentHashCache: this.contentHashCache,
|
|
8954
9008
|
});
|
|
8955
|
-
this.processor = processor;
|
|
8956
9009
|
this.queue = this.factories.createEventQueue({
|
|
8957
9010
|
debounceMs: this.config.watch.debounceMs ?? 2000,
|
|
8958
9011
|
concurrency: this.config.embedding.concurrency ?? 5,
|
|
8959
9012
|
rateLimitPerMinute: this.config.embedding.rateLimitPerMinute,
|
|
8960
9013
|
});
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
9014
|
+
this.vcsCoordinator = new VcsCoordinator(this.config, logger);
|
|
9015
|
+
}
|
|
9016
|
+
/** Wire up watcher and VCS coordinator, then start all services. */
|
|
9017
|
+
async startServicesAndWatcher() {
|
|
9018
|
+
const logger = this.logger;
|
|
9019
|
+
const vcsCoordinator = this.vcsCoordinator;
|
|
9020
|
+
const { watcher, gitignoreFilter } = createWatcher(this.config, this.factories, this.queue, this.processor, logger, this.runtimeOptions, this.initialScanTracker, this.contentHashCache, (filePath, event) => {
|
|
8964
9021
|
vcsCoordinator.onFileChange(filePath, event);
|
|
9022
|
+
}, () => {
|
|
9023
|
+
void vcsCoordinator.onInitialScanComplete();
|
|
8965
9024
|
});
|
|
8966
9025
|
this.watcher = watcher;
|
|
8967
9026
|
this.gitignoreFilter = gitignoreFilter;
|
|
8968
9027
|
this.server = await this.startApiServer();
|
|
8969
|
-
|
|
9028
|
+
vcsCoordinator.start();
|
|
8970
9029
|
this.watcher.start();
|
|
8971
9030
|
this.startConfigWatch();
|
|
8972
9031
|
logger.info('jeeves-watcher started');
|