@karmaniverous/jeeves-watcher 0.18.9 → 0.18.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/config.schema.json +203 -180
- package/dist/cli/jeeves-watcher/{index-xP2XrV4W.js → index-ZbXuE9a7.js} +335 -182
- package/dist/cli/jeeves-watcher/index.js +48 -3
- package/dist/index.d.ts +56 -13
- package/dist/index.js +291 -93
- 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 { p as normalizeSlashes, n as normalizeError, e as execFileAsync, b as GIT_TIMEOUT_STANDARD, G as GIT_TIMEOUT_CHERRY_PICK, d as buildAuthenticatedPushUrl, a as GIT_TIMEOUT_PUSH, h as getExecErrorFields, r as retry, j as gitAddViaStdin, o as normalizePathCase, g as findRootForPath, c as GitignoreFilter, f as executeReindex, V as VALID_REINDEX_SCOPES, m as mergeInferenceRules, k as isPathWatched, i as getWatchRootBases, C as CONFIG_WATCH_VALID_SCOPES, s as sleep, l as loadConfig } from './index.js';
|
|
4
|
+
export { q 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,9 +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, vcsRetentionConfigSchema, extractWatchPathStrings, vcsAuthorConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema } from '@karmaniverous/jeeves-watcher-core';
|
|
13
|
+
import { normalizeWatchPaths, vcsRetentionConfigSchema, extractWatchPathStrings, vcsAuthorConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema, getEndpoint } from '@karmaniverous/jeeves-watcher-core';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
|
-
import { access, rm,
|
|
15
|
+
import { access, rm, readFile, writeFile, appendFile, stat, mkdir } from 'node:fs/promises';
|
|
16
16
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
17
17
|
import { packageDirectorySync } from 'package-directory';
|
|
18
18
|
import { JsonMap } from '@karmaniverous/jsonmap';
|
|
@@ -803,17 +803,36 @@ function buildGlobMatcher(globs) {
|
|
|
803
803
|
return isMatch(normalized);
|
|
804
804
|
};
|
|
805
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* Expand a bare directory path (no glob characters) to a recursive glob.
|
|
808
|
+
* A path like `/opt/jeeves/content` becomes `/opt/jeeves/content/**`.
|
|
809
|
+
* Paths that already contain glob characters are returned unchanged.
|
|
810
|
+
*
|
|
811
|
+
* @param glob - A glob pattern or bare directory path.
|
|
812
|
+
* @returns The original glob, or the path suffixed with `/**` if bare.
|
|
813
|
+
*/
|
|
814
|
+
function expandBareDirectoryGlob(glob) {
|
|
815
|
+
const normalized = normalizeSlashes(glob);
|
|
816
|
+
if (/[*?{[\]]/.test(normalized))
|
|
817
|
+
return glob;
|
|
818
|
+
return glob.replace(/\/+$/, '') + '/**';
|
|
819
|
+
}
|
|
806
820
|
/**
|
|
807
821
|
* Convert an array of glob patterns into chokidar-compatible directory roots
|
|
808
822
|
* and a filter function for post-hoc event filtering.
|
|
809
823
|
*
|
|
824
|
+
* Bare directory paths (no glob characters, e.g. `/opt/jeeves/content`) are
|
|
825
|
+
* automatically expanded to recursive globs (`/opt/jeeves/content/**`) so that
|
|
826
|
+
* picomatch matches all files underneath them.
|
|
827
|
+
*
|
|
810
828
|
* @param globs - Glob patterns from the watch config.
|
|
811
829
|
* @returns Object with `roots` (directories for chokidar) and `matches` (filter function).
|
|
812
830
|
*/
|
|
813
831
|
function resolveWatchPaths(globs) {
|
|
814
|
-
const
|
|
832
|
+
const expandedGlobs = globs.map(expandBareDirectoryGlob);
|
|
833
|
+
const rawRoots = expandedGlobs.map(globRoot);
|
|
815
834
|
const roots = deduplicateRoots(rawRoots);
|
|
816
|
-
const matches = buildGlobMatcher(
|
|
835
|
+
const matches = buildGlobMatcher(expandedGlobs);
|
|
817
836
|
return { roots, matches };
|
|
818
837
|
}
|
|
819
838
|
/**
|
|
@@ -1033,8 +1052,8 @@ class CommitMessageBuilder {
|
|
|
1033
1052
|
*/
|
|
1034
1053
|
async getStagedDiff() {
|
|
1035
1054
|
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 });
|
|
1055
|
+
const { stdout: stat } = await execFileAsync('git', ['diff', '--cached', '--stat'], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
|
|
1056
|
+
const { stdout: patch } = await execFileAsync('git', ['diff', '--cached'], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
|
|
1038
1057
|
return `${stat}\n${patch}`;
|
|
1039
1058
|
}
|
|
1040
1059
|
catch {
|
|
@@ -1102,29 +1121,26 @@ function partMatches(part, value, min, max) {
|
|
|
1102
1121
|
// Number with step doesn't make sense for single values, treat as exact
|
|
1103
1122
|
return value === num;
|
|
1104
1123
|
}
|
|
1105
|
-
/**
|
|
1106
|
-
* Manages scheduled squash retention for a single VCS root.
|
|
1107
|
-
*
|
|
1108
|
-
* Runs on a cron schedule (checked every 60s). When triggered, squashes
|
|
1109
|
-
* commits older than the retention boundary (the tighter of maxAgeDays
|
|
1110
|
-
* and maxVersions) into a single "historical baseline" orphan commit,
|
|
1111
|
-
* then cherry-picks retained commits on top. Force-pushes to remote
|
|
1112
|
-
* if configured (D13).
|
|
1113
|
-
*/
|
|
1114
1124
|
class SquashManager {
|
|
1115
1125
|
rootPath;
|
|
1116
1126
|
retention;
|
|
1127
|
+
branch;
|
|
1128
|
+
pauseCommits;
|
|
1129
|
+
resumeCommits;
|
|
1117
1130
|
remoteUrl;
|
|
1118
1131
|
accessToken;
|
|
1119
1132
|
logger;
|
|
1120
1133
|
intervalTimer;
|
|
1121
1134
|
running = false;
|
|
1122
|
-
constructor(rootPath, retention, logger,
|
|
1135
|
+
constructor(rootPath, retention, logger, options = {}) {
|
|
1123
1136
|
this.rootPath = rootPath;
|
|
1124
1137
|
this.retention = retention;
|
|
1125
1138
|
this.logger = logger;
|
|
1126
|
-
this.
|
|
1127
|
-
this.
|
|
1139
|
+
this.branch = options.branch ?? 'master';
|
|
1140
|
+
this.pauseCommits = options.pauseCommits;
|
|
1141
|
+
this.resumeCommits = options.resumeCommits;
|
|
1142
|
+
this.remoteUrl = options.remoteUrl;
|
|
1143
|
+
this.accessToken = options.accessToken;
|
|
1128
1144
|
}
|
|
1129
1145
|
/**
|
|
1130
1146
|
* Start the cron-based squash scheduler.
|
|
@@ -1154,6 +1170,9 @@ class SquashManager {
|
|
|
1154
1170
|
}
|
|
1155
1171
|
/**
|
|
1156
1172
|
* Run the squash operation. Exposed publicly for testing.
|
|
1173
|
+
*
|
|
1174
|
+
* Coordinates with VcsManager by calling pause/resume callbacks
|
|
1175
|
+
* to drain and suspend the commit pipeline during the squash.
|
|
1157
1176
|
*/
|
|
1158
1177
|
async runSquash() {
|
|
1159
1178
|
// Check for index.lock before starting multi-step operation
|
|
@@ -1167,6 +1186,16 @@ class SquashManager {
|
|
|
1167
1186
|
catch {
|
|
1168
1187
|
// No lock — proceed
|
|
1169
1188
|
}
|
|
1189
|
+
// Bug 3: Pause the commit pipeline before squash
|
|
1190
|
+
try {
|
|
1191
|
+
await this.pauseCommits?.();
|
|
1192
|
+
}
|
|
1193
|
+
catch (error) {
|
|
1194
|
+
this.logger.error({ root: this.rootPath, err: normalizeError(error) }, 'Failed to pause commit pipeline before squash');
|
|
1195
|
+
// Resume in case pause partially executed (flush succeeded but flag not set)
|
|
1196
|
+
this.resumeCommits?.();
|
|
1197
|
+
return { squashed: false, error: normalizeError(error).message };
|
|
1198
|
+
}
|
|
1170
1199
|
try {
|
|
1171
1200
|
const commits = await this.getCommitLog();
|
|
1172
1201
|
if (commits.length <= 1) {
|
|
@@ -1191,13 +1220,17 @@ class SquashManager {
|
|
|
1191
1220
|
this.logger.error({ root: this.rootPath, err: normalizeError(error) }, 'Squash operation failed');
|
|
1192
1221
|
return { squashed: false, error: message };
|
|
1193
1222
|
}
|
|
1223
|
+
finally {
|
|
1224
|
+
// Bug 3: Always resume the commit pipeline
|
|
1225
|
+
this.resumeCommits?.();
|
|
1226
|
+
}
|
|
1194
1227
|
}
|
|
1195
1228
|
/**
|
|
1196
1229
|
* Get all commits in chronological order (oldest first).
|
|
1197
1230
|
*/
|
|
1198
1231
|
async getCommitLog() {
|
|
1199
1232
|
try {
|
|
1200
|
-
const { stdout } = await execFileAsync('git', ['log', '--format=%H %aI', '--reverse'], { cwd: this.rootPath });
|
|
1233
|
+
const { stdout } = await execFileAsync('git', ['log', '--format=%H %aI', '--reverse'], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
|
|
1201
1234
|
if (!stdout.trim())
|
|
1202
1235
|
return [];
|
|
1203
1236
|
return stdout
|
|
@@ -1244,32 +1277,35 @@ class SquashManager {
|
|
|
1244
1277
|
}
|
|
1245
1278
|
/**
|
|
1246
1279
|
* Perform the squash: create orphan with baseline + cherry-pick retained commits.
|
|
1280
|
+
* Uses the configured branch name instead of dynamic detection (Bug 1).
|
|
1247
1281
|
*/
|
|
1248
1282
|
async performSquash(commits, boundaryIndex) {
|
|
1249
1283
|
// The commit just before boundary is the last one to squash
|
|
1250
1284
|
const baselineHash = commits[boundaryIndex - 1].hash;
|
|
1251
|
-
//
|
|
1252
|
-
const
|
|
1253
|
-
const currentBranch = branchOut.trim();
|
|
1285
|
+
// Bug 1: Use configured branch name, not dynamic detection
|
|
1286
|
+
const targetBranch = this.branch;
|
|
1254
1287
|
const orphanBranch = `__squash_orphan_${String(Date.now())}`;
|
|
1255
1288
|
try {
|
|
1256
1289
|
// 1. Create orphan branch with the tree state at the baseline commit
|
|
1257
1290
|
await execFileAsync('git', ['checkout', '--orphan', orphanBranch], {
|
|
1258
1291
|
cwd: this.rootPath,
|
|
1292
|
+
timeout: GIT_TIMEOUT_STANDARD,
|
|
1259
1293
|
});
|
|
1260
1294
|
// 2. Reset index to the baseline tree (the last squashed commit's tree)
|
|
1261
1295
|
await execFileAsync('git', ['reset', '--hard', baselineHash], {
|
|
1262
1296
|
cwd: this.rootPath,
|
|
1297
|
+
timeout: GIT_TIMEOUT_STANDARD,
|
|
1263
1298
|
});
|
|
1264
1299
|
// 3. Create the baseline commit with the same tree
|
|
1265
1300
|
// Use commit-tree for a clean orphan commit
|
|
1266
|
-
const { stdout: treeOut } = await execFileAsync('git', ['rev-parse', `${baselineHash}^{tree}`], { cwd: this.rootPath });
|
|
1301
|
+
const { stdout: treeOut } = await execFileAsync('git', ['rev-parse', `${baselineHash}^{tree}`], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
|
|
1267
1302
|
const treeHash = treeOut.trim();
|
|
1268
|
-
const { stdout: commitOut } = await execFileAsync('git', ['commit-tree', treeHash, '-m', 'historical baseline'], { cwd: this.rootPath });
|
|
1303
|
+
const { stdout: commitOut } = await execFileAsync('git', ['commit-tree', treeHash, '-m', 'historical baseline'], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
|
|
1269
1304
|
const baselineCommitHash = commitOut.trim();
|
|
1270
1305
|
// 4. Reset orphan branch to this new baseline commit
|
|
1271
1306
|
await execFileAsync('git', ['reset', '--hard', baselineCommitHash], {
|
|
1272
1307
|
cwd: this.rootPath,
|
|
1308
|
+
timeout: GIT_TIMEOUT_STANDARD,
|
|
1273
1309
|
});
|
|
1274
1310
|
// 5. Cherry-pick boundary..HEAD (the retained commits)
|
|
1275
1311
|
// Get the list of commits to cherry-pick (from boundary to the end)
|
|
@@ -1277,28 +1313,33 @@ class SquashManager {
|
|
|
1277
1313
|
if (commitsToCherry.length > 0) {
|
|
1278
1314
|
await execFileAsync('git', ['cherry-pick', ...commitsToCherry], {
|
|
1279
1315
|
cwd: this.rootPath,
|
|
1316
|
+
timeout: GIT_TIMEOUT_CHERRY_PICK,
|
|
1280
1317
|
});
|
|
1281
1318
|
}
|
|
1282
|
-
// 6. Force-update the
|
|
1283
|
-
const { stdout: newHead } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: this.rootPath });
|
|
1284
|
-
await execFileAsync('git', ['branch', '-f',
|
|
1285
|
-
// 7. Switch back to
|
|
1286
|
-
await execFileAsync('git', ['checkout',
|
|
1319
|
+
// 6. Force-update the configured branch to the orphan
|
|
1320
|
+
const { stdout: newHead } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
|
|
1321
|
+
await execFileAsync('git', ['branch', '-f', targetBranch, newHead.trim()], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
|
|
1322
|
+
// 7. Switch back to configured branch
|
|
1323
|
+
await execFileAsync('git', ['checkout', targetBranch], {
|
|
1287
1324
|
cwd: this.rootPath,
|
|
1325
|
+
timeout: GIT_TIMEOUT_STANDARD,
|
|
1288
1326
|
});
|
|
1289
1327
|
// 8. Delete orphan branch
|
|
1290
1328
|
await execFileAsync('git', ['branch', '-D', orphanBranch], {
|
|
1291
1329
|
cwd: this.rootPath,
|
|
1330
|
+
timeout: GIT_TIMEOUT_STANDARD,
|
|
1292
1331
|
});
|
|
1293
1332
|
}
|
|
1294
1333
|
catch (error) {
|
|
1295
|
-
// Attempt cleanup: try to get back to
|
|
1334
|
+
// Attempt cleanup: try to get back to configured branch
|
|
1296
1335
|
try {
|
|
1297
|
-
await execFileAsync('git', ['checkout', '-f',
|
|
1336
|
+
await execFileAsync('git', ['checkout', '-f', targetBranch], {
|
|
1298
1337
|
cwd: this.rootPath,
|
|
1338
|
+
timeout: GIT_TIMEOUT_STANDARD,
|
|
1299
1339
|
});
|
|
1300
1340
|
await execFileAsync('git', ['branch', '-D', orphanBranch], {
|
|
1301
1341
|
cwd: this.rootPath,
|
|
1342
|
+
timeout: GIT_TIMEOUT_STANDARD,
|
|
1302
1343
|
});
|
|
1303
1344
|
}
|
|
1304
1345
|
catch {
|
|
@@ -1322,12 +1363,11 @@ class SquashManager {
|
|
|
1322
1363
|
if (!this.remoteUrl)
|
|
1323
1364
|
return;
|
|
1324
1365
|
try {
|
|
1325
|
-
const pushUrl = this.accessToken
|
|
1326
|
-
? this.remoteUrl.replace(/^https:\/\//, `https://${encodeURIComponent(this.accessToken)}@`)
|
|
1327
|
-
: this.remoteUrl;
|
|
1366
|
+
const pushUrl = buildAuthenticatedPushUrl(this.remoteUrl, this.accessToken);
|
|
1328
1367
|
await execFileAsync('git', ['push', '--force', pushUrl, 'HEAD'], {
|
|
1329
1368
|
cwd: this.rootPath,
|
|
1330
1369
|
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
1370
|
+
timeout: GIT_TIMEOUT_PUSH,
|
|
1331
1371
|
});
|
|
1332
1372
|
this.logger.info({ root: this.rootPath, remote: this.remoteUrl }, 'Squash force push succeeded');
|
|
1333
1373
|
}
|
|
@@ -1337,6 +1377,133 @@ class SquashManager {
|
|
|
1337
1377
|
}
|
|
1338
1378
|
}
|
|
1339
1379
|
|
|
1380
|
+
/**
|
|
1381
|
+
* @module vcs/vcsBootstrap
|
|
1382
|
+
* Static bootstrap utilities for VCS-managed watch roots.
|
|
1383
|
+
*/
|
|
1384
|
+
/** Always-on .gitignore entries for VCS-managed watch roots. */
|
|
1385
|
+
const ALWAYS_GITIGNORE_ENTRIES = [
|
|
1386
|
+
'.git/',
|
|
1387
|
+
'node_modules/',
|
|
1388
|
+
'.jeeves-watcher/',
|
|
1389
|
+
'.jeeves-metadata/',
|
|
1390
|
+
];
|
|
1391
|
+
/**
|
|
1392
|
+
* Check whether git is available on the system PATH.
|
|
1393
|
+
*
|
|
1394
|
+
* @returns true if git is available, false otherwise.
|
|
1395
|
+
*/
|
|
1396
|
+
async function checkGitAvailable() {
|
|
1397
|
+
try {
|
|
1398
|
+
await execFileAsync('git', ['--version']);
|
|
1399
|
+
return true;
|
|
1400
|
+
}
|
|
1401
|
+
catch {
|
|
1402
|
+
return false;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Initialize a git repository at the given path if none exists.
|
|
1407
|
+
* Idempotent — does nothing if .git/ already exists.
|
|
1408
|
+
*
|
|
1409
|
+
* @param rootPath - Directory to initialize as a git repo.
|
|
1410
|
+
*/
|
|
1411
|
+
async function initRepo(rootPath) {
|
|
1412
|
+
const gitDir = join(rootPath, '.git');
|
|
1413
|
+
try {
|
|
1414
|
+
await access(gitDir);
|
|
1415
|
+
// .git exists, nothing to do
|
|
1416
|
+
}
|
|
1417
|
+
catch {
|
|
1418
|
+
await execFileAsync('git', ['init'], { cwd: rootPath });
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Configure local git identity for a repository.
|
|
1423
|
+
* Sets repo-level (not global) user.name and user.email.
|
|
1424
|
+
*
|
|
1425
|
+
* @param rootPath - Directory of the git repository.
|
|
1426
|
+
* @param name - Author name for commits.
|
|
1427
|
+
* @param email - Author email for commits.
|
|
1428
|
+
*/
|
|
1429
|
+
async function configureRepoIdentity(rootPath, name, email) {
|
|
1430
|
+
await execFileAsync('git', ['config', '--local', 'user.name', name], {
|
|
1431
|
+
cwd: rootPath,
|
|
1432
|
+
});
|
|
1433
|
+
await execFileAsync('git', ['config', '--local', 'user.email', email], {
|
|
1434
|
+
cwd: rootPath,
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Detect and recover from orphan branch state on startup.
|
|
1439
|
+
*
|
|
1440
|
+
* If the repo is on an unexpected branch (e.g. a leftover squash orphan),
|
|
1441
|
+
* force-updates the expected branch to the current HEAD and checks it out.
|
|
1442
|
+
* Orphan branches are NOT deleted — they serve as a recovery safety net.
|
|
1443
|
+
*
|
|
1444
|
+
* @param rootPath - Directory of the git repository.
|
|
1445
|
+
* @param expectedBranch - The configured branch name (e.g. "master").
|
|
1446
|
+
* @param logger - Logger instance for warnings and info.
|
|
1447
|
+
*/
|
|
1448
|
+
async function detectAndRecoverOrphanBranch(rootPath, expectedBranch, logger) {
|
|
1449
|
+
const gitDir = join(rootPath, '.git');
|
|
1450
|
+
try {
|
|
1451
|
+
await access(gitDir);
|
|
1452
|
+
}
|
|
1453
|
+
catch {
|
|
1454
|
+
// Not a git repo — nothing to detect
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
const { stdout: branchOut } = await execFileAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: rootPath, timeout: GIT_TIMEOUT_STANDARD });
|
|
1458
|
+
const currentBranch = branchOut.trim();
|
|
1459
|
+
if (currentBranch === expectedBranch)
|
|
1460
|
+
return;
|
|
1461
|
+
logger.warn({ root: rootPath, currentBranch, expectedBranch }, 'Repo is on unexpected branch — recovering to configured branch');
|
|
1462
|
+
// Get current HEAD
|
|
1463
|
+
const { stdout: headOut } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: rootPath, timeout: GIT_TIMEOUT_STANDARD });
|
|
1464
|
+
const headHash = headOut.trim();
|
|
1465
|
+
// Force-update the expected branch to current HEAD
|
|
1466
|
+
await execFileAsync('git', ['branch', '-f', expectedBranch, headHash], {
|
|
1467
|
+
cwd: rootPath,
|
|
1468
|
+
timeout: GIT_TIMEOUT_STANDARD,
|
|
1469
|
+
});
|
|
1470
|
+
// Checkout the expected branch
|
|
1471
|
+
await execFileAsync('git', ['checkout', expectedBranch], {
|
|
1472
|
+
cwd: rootPath,
|
|
1473
|
+
timeout: GIT_TIMEOUT_STANDARD,
|
|
1474
|
+
});
|
|
1475
|
+
logger.info({ root: rootPath, recoveredFrom: currentBranch, expectedBranch, headHash }, 'Orphan branch recovery complete — now on configured branch');
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Ensure a .gitignore file exists at rootPath with all always-on entries.
|
|
1479
|
+
* Creates the file if missing. Appends only entries not already present.
|
|
1480
|
+
*
|
|
1481
|
+
* @param rootPath - Directory containing the .gitignore.
|
|
1482
|
+
* @param alwaysOnEntries - Additional always-on entries beyond the defaults.
|
|
1483
|
+
*/
|
|
1484
|
+
async function ensureGitignore(rootPath, alwaysOnEntries = []) {
|
|
1485
|
+
const gitignorePath = join(rootPath, '.gitignore');
|
|
1486
|
+
const requiredEntries = [...ALWAYS_GITIGNORE_ENTRIES, ...alwaysOnEntries];
|
|
1487
|
+
let existingContent = '';
|
|
1488
|
+
try {
|
|
1489
|
+
existingContent = await readFile(gitignorePath, 'utf8');
|
|
1490
|
+
}
|
|
1491
|
+
catch {
|
|
1492
|
+
// File doesn't exist yet
|
|
1493
|
+
}
|
|
1494
|
+
const existingLines = new Set(existingContent.split('\n').map((line) => line.trim()));
|
|
1495
|
+
const missing = requiredEntries.filter((entry) => !existingLines.has(entry));
|
|
1496
|
+
if (missing.length === 0)
|
|
1497
|
+
return;
|
|
1498
|
+
if (existingContent.length === 0) {
|
|
1499
|
+
await writeFile(gitignorePath, missing.join('\n') + '\n', 'utf8');
|
|
1500
|
+
}
|
|
1501
|
+
else {
|
|
1502
|
+
const prefix = existingContent.endsWith('\n') ? '' : '\n';
|
|
1503
|
+
await appendFile(gitignorePath, prefix + missing.join('\n') + '\n', 'utf8');
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1340
1507
|
/**
|
|
1341
1508
|
* @module vcs/vcsPush
|
|
1342
1509
|
* Push logic for VCS commits.
|
|
@@ -1358,13 +1525,11 @@ async function pushToRemote(rootPath, remoteUrl, accessToken, pushErrors, logger
|
|
|
1358
1525
|
if (!remoteUrl)
|
|
1359
1526
|
return undefined;
|
|
1360
1527
|
try {
|
|
1361
|
-
|
|
1362
|
-
const pushUrl = accessToken
|
|
1363
|
-
? remoteUrl.replace(/^https:\/\//, `https://${encodeURIComponent(accessToken)}@`)
|
|
1364
|
-
: remoteUrl;
|
|
1528
|
+
const pushUrl = buildAuthenticatedPushUrl(remoteUrl, accessToken);
|
|
1365
1529
|
await execFileAsync('git', ['push', pushUrl, 'HEAD'], {
|
|
1366
1530
|
cwd: rootPath,
|
|
1367
1531
|
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
1532
|
+
timeout: GIT_TIMEOUT_PUSH,
|
|
1368
1533
|
});
|
|
1369
1534
|
const timestamp = new Date().toISOString();
|
|
1370
1535
|
logger.info({ root: rootPath, remote: remoteUrl }, 'VCS push succeeded');
|
|
@@ -1389,7 +1554,7 @@ async function pushToRemote(rootPath, remoteUrl, accessToken, pushErrors, logger
|
|
|
1389
1554
|
* Per-root VCS manager for git-backed content versioning.
|
|
1390
1555
|
*
|
|
1391
1556
|
* Each VCS-enabled watch root gets its own VcsManager instance, which owns
|
|
1392
|
-
* a
|
|
1557
|
+
* a throttled commit pipeline: file changes are batched, staged, committed
|
|
1393
1558
|
* (with optional AI-generated messages), and optionally pushed to a remote.
|
|
1394
1559
|
*
|
|
1395
1560
|
* Concurrency: only one commit is in-flight at a time per root. Index.lock
|
|
@@ -1406,10 +1571,11 @@ class VcsManager {
|
|
|
1406
1571
|
pendingReversions = [];
|
|
1407
1572
|
_pushErrors = [];
|
|
1408
1573
|
squashManager;
|
|
1409
|
-
|
|
1574
|
+
throttleTimer;
|
|
1410
1575
|
commitInFlight = Promise.resolve();
|
|
1411
1576
|
consecutiveCommitFailures = 0;
|
|
1412
1577
|
started = false;
|
|
1578
|
+
paused = false;
|
|
1413
1579
|
isBaseline = true;
|
|
1414
1580
|
_lastPushTime = null;
|
|
1415
1581
|
constructor(rootPath, config, logger, commitMessageGenerator, remoteUrl, accessToken) {
|
|
@@ -1420,7 +1586,17 @@ class VcsManager {
|
|
|
1420
1586
|
this.accessToken = accessToken;
|
|
1421
1587
|
this.commitMessageBuilder = new CommitMessageBuilder(rootPath, logger, commitMessageGenerator);
|
|
1422
1588
|
if (config.retention) {
|
|
1423
|
-
this.squashManager = new SquashManager(rootPath, config.retention, logger,
|
|
1589
|
+
this.squashManager = new SquashManager(rootPath, config.retention, logger, {
|
|
1590
|
+
branch: config.branch,
|
|
1591
|
+
pauseCommits: () => {
|
|
1592
|
+
return this.pause();
|
|
1593
|
+
},
|
|
1594
|
+
resumeCommits: () => {
|
|
1595
|
+
this.resume();
|
|
1596
|
+
},
|
|
1597
|
+
remoteUrl,
|
|
1598
|
+
accessToken,
|
|
1599
|
+
});
|
|
1424
1600
|
}
|
|
1425
1601
|
}
|
|
1426
1602
|
get lastPushTime() {
|
|
@@ -1433,10 +1609,21 @@ class VcsManager {
|
|
|
1433
1609
|
* Begin accepting file changes. Sets the manager to started state and
|
|
1434
1610
|
* starts the squash manager if configured.
|
|
1435
1611
|
*
|
|
1612
|
+
* Performs startup orphan detection/recovery before starting the squash
|
|
1613
|
+
* manager, ensuring the repo is on the configured branch.
|
|
1614
|
+
*
|
|
1436
1615
|
* Baseline mode (commit prefix `"baseline:"`) remains active until
|
|
1437
1616
|
* {@link endBaseline} is called by the coordinator after the initial scan.
|
|
1438
1617
|
*/
|
|
1439
|
-
start() {
|
|
1618
|
+
async start() {
|
|
1619
|
+
// Bug 6: Detect and recover from orphan branches before normal operations
|
|
1620
|
+
const branch = this.config.branch;
|
|
1621
|
+
try {
|
|
1622
|
+
await detectAndRecoverOrphanBranch(this.rootPath, branch, this.logger);
|
|
1623
|
+
}
|
|
1624
|
+
catch (error) {
|
|
1625
|
+
this.logger.error({ root: this.rootPath, err: normalizeError(error) }, 'Orphan branch recovery failed — continuing with current state');
|
|
1626
|
+
}
|
|
1440
1627
|
this.started = true;
|
|
1441
1628
|
this.squashManager?.start();
|
|
1442
1629
|
this.logger.info({ root: this.rootPath }, 'VcsManager started');
|
|
@@ -1449,7 +1636,7 @@ class VcsManager {
|
|
|
1449
1636
|
this.isBaseline = false;
|
|
1450
1637
|
}
|
|
1451
1638
|
/**
|
|
1452
|
-
* Add a file to the pending set and
|
|
1639
|
+
* Add a file to the pending set and start the throttle timer.
|
|
1453
1640
|
* If the pending set exceeds maxBatchSize, flush immediately.
|
|
1454
1641
|
*
|
|
1455
1642
|
* @param filePath - Absolute path of the changed file.
|
|
@@ -1464,15 +1651,15 @@ class VcsManager {
|
|
|
1464
1651
|
}
|
|
1465
1652
|
this.pending.add(filePath);
|
|
1466
1653
|
if (this.pending.size >= this.config.maxBatchSize) {
|
|
1467
|
-
this.
|
|
1654
|
+
this.clearThrottle();
|
|
1468
1655
|
const batch = this.takeBatch(this.config.maxBatchSize);
|
|
1469
1656
|
this.commitInFlight = this.commitInFlight.then(() => this.commitBatch(batch));
|
|
1470
1657
|
if (this.pending.size > 0) {
|
|
1471
|
-
this.
|
|
1658
|
+
this.resetThrottle();
|
|
1472
1659
|
}
|
|
1473
1660
|
return;
|
|
1474
1661
|
}
|
|
1475
|
-
this.
|
|
1662
|
+
this.resetThrottle();
|
|
1476
1663
|
}
|
|
1477
1664
|
/**
|
|
1478
1665
|
* Record reversion metadata so the next commit uses a revert-prefixed message.
|
|
@@ -1493,11 +1680,33 @@ class VcsManager {
|
|
|
1493
1680
|
return;
|
|
1494
1681
|
this.fileChanged(filePath);
|
|
1495
1682
|
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Pause the commit pipeline. Drains pending commits via flush(), then
|
|
1685
|
+
* sets a flag that makes commitBatch queue instead of execute.
|
|
1686
|
+
* Used by SquashManager to coordinate squash operations.
|
|
1687
|
+
*/
|
|
1688
|
+
async pause() {
|
|
1689
|
+
await this.flush();
|
|
1690
|
+
this.paused = true;
|
|
1691
|
+
this.logger.debug({ root: this.rootPath }, 'VcsManager paused');
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Resume the commit pipeline after a pause.
|
|
1695
|
+
* Re-enables normal commit operations and starts a throttle timer
|
|
1696
|
+
* if there are pending files that accumulated during the pause.
|
|
1697
|
+
*/
|
|
1698
|
+
resume() {
|
|
1699
|
+
this.paused = false;
|
|
1700
|
+
this.logger.debug({ root: this.rootPath }, 'VcsManager resumed');
|
|
1701
|
+
if (this.pending.size > 0) {
|
|
1702
|
+
this.resetThrottle();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1496
1705
|
/**
|
|
1497
1706
|
* Flush all pending files immediately.
|
|
1498
1707
|
*/
|
|
1499
1708
|
async flush() {
|
|
1500
|
-
this.
|
|
1709
|
+
this.clearThrottle();
|
|
1501
1710
|
if (this.pending.size > 0) {
|
|
1502
1711
|
const batch = [...this.pending];
|
|
1503
1712
|
this.pending.clear();
|
|
@@ -1530,23 +1739,25 @@ class VcsManager {
|
|
|
1530
1739
|
return batch;
|
|
1531
1740
|
}
|
|
1532
1741
|
/**
|
|
1533
|
-
* Clear the
|
|
1742
|
+
* Clear the throttle timer.
|
|
1534
1743
|
*/
|
|
1535
|
-
|
|
1536
|
-
if (this.
|
|
1537
|
-
clearTimeout(this.
|
|
1538
|
-
this.
|
|
1744
|
+
clearThrottle() {
|
|
1745
|
+
if (this.throttleTimer !== undefined) {
|
|
1746
|
+
clearTimeout(this.throttleTimer);
|
|
1747
|
+
this.throttleTimer = undefined;
|
|
1539
1748
|
}
|
|
1540
1749
|
}
|
|
1541
1750
|
/**
|
|
1542
|
-
*
|
|
1751
|
+
* Start the throttle timer to fire flush() after commitThrottleMs.
|
|
1752
|
+
* If a timer is already running, do not reset it — this is throttle, not debounce.
|
|
1543
1753
|
*/
|
|
1544
|
-
|
|
1545
|
-
this.
|
|
1546
|
-
|
|
1547
|
-
|
|
1754
|
+
resetThrottle() {
|
|
1755
|
+
if (this.throttleTimer !== undefined)
|
|
1756
|
+
return;
|
|
1757
|
+
this.throttleTimer = setTimeout(() => {
|
|
1758
|
+
this.throttleTimer = undefined;
|
|
1548
1759
|
void this.flush();
|
|
1549
|
-
}, this.config.
|
|
1760
|
+
}, this.config.commitThrottleMs);
|
|
1550
1761
|
}
|
|
1551
1762
|
/**
|
|
1552
1763
|
* Check for and remove stale index.lock files before retrying.
|
|
@@ -1568,13 +1779,31 @@ class VcsManager {
|
|
|
1568
1779
|
}
|
|
1569
1780
|
}
|
|
1570
1781
|
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Check whether a git error represents a "nothing to commit" state.
|
|
1784
|
+
* Git outputs this message to stdout (not stderr) on exit code 1.
|
|
1785
|
+
*/
|
|
1786
|
+
isNothingToCommitError(error) {
|
|
1787
|
+
const { message, stderr, stdout } = getExecErrorFields(error);
|
|
1788
|
+
return (message.includes('nothing to commit') ||
|
|
1789
|
+
stderr.includes('nothing to commit') ||
|
|
1790
|
+
stdout.includes('nothing to commit'));
|
|
1791
|
+
}
|
|
1571
1792
|
/**
|
|
1572
1793
|
* Commit a batch of files to the git repo with index.lock retry,
|
|
1573
|
-
* stale lock detection, circuit breaker, and re-queue cap.
|
|
1794
|
+
* stale lock detection, circuit breaker, pause support, and re-queue cap.
|
|
1574
1795
|
*/
|
|
1575
1796
|
async commitBatch(files) {
|
|
1576
1797
|
if (files.length === 0)
|
|
1577
1798
|
return;
|
|
1799
|
+
// Pause support: re-queue files without counting as failure
|
|
1800
|
+
if (this.paused) {
|
|
1801
|
+
for (const f of files) {
|
|
1802
|
+
this.pending.add(f);
|
|
1803
|
+
}
|
|
1804
|
+
this.logger.debug({ root: this.rootPath, fileCount: files.length }, 'VcsManager paused — files re-queued');
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1578
1807
|
// Circuit breaker: skip if too many consecutive failures
|
|
1579
1808
|
if (this.consecutiveCommitFailures >= this.config.maxConsecutiveFailures) {
|
|
1580
1809
|
this.logger.error({
|
|
@@ -1588,7 +1817,7 @@ class VcsManager {
|
|
|
1588
1817
|
// Check for stale lock before first attempt
|
|
1589
1818
|
await this.removeStaleLock();
|
|
1590
1819
|
await retry(async (attempt) => {
|
|
1591
|
-
await gitAddViaStdin(files, this.rootPath);
|
|
1820
|
+
await gitAddViaStdin(files, this.rootPath, GIT_TIMEOUT_STANDARD);
|
|
1592
1821
|
// Build message after staging so getStagedDiff can see the changes.
|
|
1593
1822
|
// Skip AI for baselines and retries — use template directly.
|
|
1594
1823
|
const message = this.isBaseline || attempt > 1
|
|
@@ -1597,8 +1826,9 @@ class VcsManager {
|
|
|
1597
1826
|
this.pendingReversions.length = 0;
|
|
1598
1827
|
await execFileAsync('git', ['commit', '-m', message], {
|
|
1599
1828
|
cwd: this.rootPath,
|
|
1829
|
+
timeout: GIT_TIMEOUT_STANDARD,
|
|
1600
1830
|
});
|
|
1601
|
-
const { stdout: hashOut } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], { cwd: this.rootPath });
|
|
1831
|
+
const { stdout: hashOut } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], { cwd: this.rootPath, timeout: GIT_TIMEOUT_STANDARD });
|
|
1602
1832
|
const hash = hashOut.trim();
|
|
1603
1833
|
this.logger.info({ root: this.rootPath, hash, fileCount: files.length }, 'VCS commit created');
|
|
1604
1834
|
const pushTime = await pushToRemote(this.rootPath, this.remoteUrl, this.accessToken, this._pushErrors, this.logger);
|
|
@@ -1616,6 +1846,11 @@ class VcsManager {
|
|
|
1616
1846
|
this.consecutiveCommitFailures = 0;
|
|
1617
1847
|
}
|
|
1618
1848
|
catch (error) {
|
|
1849
|
+
// Bug 5: "nothing to commit" is a no-op, not a failure
|
|
1850
|
+
if (this.isNothingToCommitError(error)) {
|
|
1851
|
+
this.logger.info({ root: this.rootPath, fileCount: files.length }, 'VCS commit skipped — nothing to commit');
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1619
1854
|
this.consecutiveCommitFailures++;
|
|
1620
1855
|
// Re-queue cap: only re-queue up to maxBatchSize files
|
|
1621
1856
|
const cap = this.config.maxBatchSize;
|
|
@@ -1629,6 +1864,8 @@ class VcsManager {
|
|
|
1629
1864
|
}
|
|
1630
1865
|
this.logger.warn({ root: this.rootPath, fileCount: toRequeue.length }, 'Re-queued files after commit failure');
|
|
1631
1866
|
this.logger.error({ root: this.rootPath, err: normalizeError(error) }, 'VCS commit failed');
|
|
1867
|
+
// Bug 2: Restart throttle timer so re-queued files get retried
|
|
1868
|
+
this.resetThrottle();
|
|
1632
1869
|
}
|
|
1633
1870
|
}
|
|
1634
1871
|
}
|
|
@@ -1689,11 +1926,14 @@ class VcsCoordinator {
|
|
|
1689
1926
|
}
|
|
1690
1927
|
/**
|
|
1691
1928
|
* Start all VcsManager instances.
|
|
1929
|
+
* Runs startup orphan detection/recovery for each root.
|
|
1692
1930
|
*/
|
|
1693
|
-
start() {
|
|
1931
|
+
async start() {
|
|
1932
|
+
const startPromises = [];
|
|
1694
1933
|
this.managers.forEach((manager) => {
|
|
1695
|
-
manager.start();
|
|
1934
|
+
startPromises.push(manager.start());
|
|
1696
1935
|
});
|
|
1936
|
+
await Promise.all(startPromises);
|
|
1697
1937
|
this.logger.info({ rootCount: this.managers.size }, 'VcsCoordinator started');
|
|
1698
1938
|
}
|
|
1699
1939
|
/**
|
|
@@ -1717,8 +1957,8 @@ class VcsCoordinator {
|
|
|
1717
1957
|
/**
|
|
1718
1958
|
* Signal that the initial filesystem scan is complete.
|
|
1719
1959
|
*
|
|
1720
|
-
* Flushes each manager's
|
|
1721
|
-
* any files still pending in the VCS
|
|
1960
|
+
* Flushes each manager's throttle buffer before ending baseline mode, ensuring
|
|
1961
|
+
* any files still pending in the VCS throttle are committed with a
|
|
1722
1962
|
* `"baseline:"` prefix rather than `"watcher:"`. AI generation is not enabled
|
|
1723
1963
|
* until all managers have completed their flush.
|
|
1724
1964
|
*/
|
|
@@ -3044,93 +3284,6 @@ function validateStateDirOverlap(stateDir, watchPaths) {
|
|
|
3044
3284
|
}
|
|
3045
3285
|
}
|
|
3046
3286
|
|
|
3047
|
-
/**
|
|
3048
|
-
* @module vcs/vcsBootstrap
|
|
3049
|
-
* Static bootstrap utilities for VCS-managed watch roots.
|
|
3050
|
-
*/
|
|
3051
|
-
/** Always-on .gitignore entries for VCS-managed watch roots. */
|
|
3052
|
-
const ALWAYS_GITIGNORE_ENTRIES = [
|
|
3053
|
-
'.git/',
|
|
3054
|
-
'node_modules/',
|
|
3055
|
-
'.jeeves-watcher/',
|
|
3056
|
-
'.jeeves-metadata/',
|
|
3057
|
-
];
|
|
3058
|
-
/**
|
|
3059
|
-
* Check whether git is available on the system PATH.
|
|
3060
|
-
*
|
|
3061
|
-
* @returns true if git is available, false otherwise.
|
|
3062
|
-
*/
|
|
3063
|
-
async function checkGitAvailable() {
|
|
3064
|
-
try {
|
|
3065
|
-
await execFileAsync('git', ['--version']);
|
|
3066
|
-
return true;
|
|
3067
|
-
}
|
|
3068
|
-
catch {
|
|
3069
|
-
return false;
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
/**
|
|
3073
|
-
* Initialize a git repository at the given path if none exists.
|
|
3074
|
-
* Idempotent — does nothing if .git/ already exists.
|
|
3075
|
-
*
|
|
3076
|
-
* @param rootPath - Directory to initialize as a git repo.
|
|
3077
|
-
*/
|
|
3078
|
-
async function initRepo(rootPath) {
|
|
3079
|
-
const gitDir = join(rootPath, '.git');
|
|
3080
|
-
try {
|
|
3081
|
-
await access(gitDir);
|
|
3082
|
-
// .git exists, nothing to do
|
|
3083
|
-
}
|
|
3084
|
-
catch {
|
|
3085
|
-
await execFileAsync('git', ['init'], { cwd: rootPath });
|
|
3086
|
-
}
|
|
3087
|
-
}
|
|
3088
|
-
/**
|
|
3089
|
-
* Configure local git identity for a repository.
|
|
3090
|
-
* Sets repo-level (not global) user.name and user.email.
|
|
3091
|
-
*
|
|
3092
|
-
* @param rootPath - Directory of the git repository.
|
|
3093
|
-
* @param name - Author name for commits.
|
|
3094
|
-
* @param email - Author email for commits.
|
|
3095
|
-
*/
|
|
3096
|
-
async function configureRepoIdentity(rootPath, name, email) {
|
|
3097
|
-
await execFileAsync('git', ['config', '--local', 'user.name', name], {
|
|
3098
|
-
cwd: rootPath,
|
|
3099
|
-
});
|
|
3100
|
-
await execFileAsync('git', ['config', '--local', 'user.email', email], {
|
|
3101
|
-
cwd: rootPath,
|
|
3102
|
-
});
|
|
3103
|
-
}
|
|
3104
|
-
/**
|
|
3105
|
-
* Ensure a .gitignore file exists at rootPath with all always-on entries.
|
|
3106
|
-
* Creates the file if missing. Appends only entries not already present.
|
|
3107
|
-
*
|
|
3108
|
-
* @param rootPath - Directory containing the .gitignore.
|
|
3109
|
-
* @param alwaysOnEntries - Additional always-on entries beyond the defaults.
|
|
3110
|
-
*/
|
|
3111
|
-
async function ensureGitignore(rootPath, alwaysOnEntries = []) {
|
|
3112
|
-
const gitignorePath = join(rootPath, '.gitignore');
|
|
3113
|
-
const requiredEntries = [...ALWAYS_GITIGNORE_ENTRIES, ...alwaysOnEntries];
|
|
3114
|
-
let existingContent = '';
|
|
3115
|
-
try {
|
|
3116
|
-
existingContent = await readFile(gitignorePath, 'utf8');
|
|
3117
|
-
}
|
|
3118
|
-
catch {
|
|
3119
|
-
// File doesn't exist yet
|
|
3120
|
-
}
|
|
3121
|
-
const existingLines = new Set(existingContent.split('\n').map((line) => line.trim()));
|
|
3122
|
-
const missing = requiredEntries.filter((entry) => !existingLines.has(entry));
|
|
3123
|
-
if (missing.length === 0)
|
|
3124
|
-
return;
|
|
3125
|
-
if (existingContent.length === 0) {
|
|
3126
|
-
await writeFile(gitignorePath, missing.join('\n') + '\n', 'utf8');
|
|
3127
|
-
}
|
|
3128
|
-
else {
|
|
3129
|
-
const prefix = existingContent.endsWith('\n') ? '' : '\n';
|
|
3130
|
-
await appendFile(gitignorePath, prefix + missing.join('\n') + '\n', 'utf8');
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
3287
|
/**
|
|
3135
3288
|
* @module app/watcherFactory
|
|
3136
3289
|
* Factory helpers for creating and rebuilding the filesystem watcher.
|
|
@@ -5412,36 +5565,36 @@ function createApiServer(options) {
|
|
|
5412
5565
|
};
|
|
5413
5566
|
},
|
|
5414
5567
|
});
|
|
5415
|
-
app.get('
|
|
5568
|
+
app.get(getEndpoint('status').path, withCache(cacheTtlMs, async () => {
|
|
5416
5569
|
const result = await coreStatusHandler();
|
|
5417
5570
|
return result.body;
|
|
5418
5571
|
}));
|
|
5419
|
-
app.post('
|
|
5572
|
+
app.post(getEndpoint('metadata').path, createMetadataHandler({
|
|
5420
5573
|
processor,
|
|
5421
5574
|
getConfig,
|
|
5422
5575
|
logger,
|
|
5423
5576
|
configDir: dirname(configPath),
|
|
5424
5577
|
}));
|
|
5425
|
-
app.post('
|
|
5578
|
+
app.post(getEndpoint('render').path, withCache(cacheTtlMs, createRenderHandler({
|
|
5426
5579
|
processor,
|
|
5427
5580
|
getWatch: () => getConfig().watch,
|
|
5428
5581
|
logger,
|
|
5429
5582
|
})));
|
|
5430
|
-
app.get('
|
|
5583
|
+
app.get(getEndpoint('searchFacets').path, createFacetsHandler({
|
|
5431
5584
|
getConfig,
|
|
5432
5585
|
valuesManager,
|
|
5433
5586
|
configDir: dirname(configPath),
|
|
5434
5587
|
}));
|
|
5435
|
-
app.post('
|
|
5588
|
+
app.post(getEndpoint('scan').path, createScanHandler({
|
|
5436
5589
|
vectorStore,
|
|
5437
5590
|
logger,
|
|
5438
5591
|
}));
|
|
5439
|
-
app.post('
|
|
5592
|
+
app.post(getEndpoint('walk').path, createWalkHandler({
|
|
5440
5593
|
getWatchPaths: () => extractWatchPathStrings(getConfig().watch.paths),
|
|
5441
5594
|
getFileSystemWatcher,
|
|
5442
5595
|
logger,
|
|
5443
5596
|
}));
|
|
5444
|
-
app.post('
|
|
5597
|
+
app.post(getEndpoint('search').path, createSearchHandler({
|
|
5445
5598
|
embeddingProvider,
|
|
5446
5599
|
vectorStore,
|
|
5447
5600
|
logger,
|
|
@@ -5452,12 +5605,12 @@ function createApiServer(options) {
|
|
|
5452
5605
|
: undefined;
|
|
5453
5606
|
},
|
|
5454
5607
|
}));
|
|
5455
|
-
app.post('
|
|
5608
|
+
app.post(getEndpoint('rebuildMetadata').path, createRebuildMetadataHandler({
|
|
5456
5609
|
enrichmentStore: options.enrichmentStore,
|
|
5457
5610
|
vectorStore,
|
|
5458
5611
|
logger,
|
|
5459
5612
|
}));
|
|
5460
|
-
app.post('
|
|
5613
|
+
app.post(getEndpoint('reindex').path, createConfigReindexHandler({
|
|
5461
5614
|
getConfig,
|
|
5462
5615
|
processor,
|
|
5463
5616
|
logger,
|
|
@@ -5468,10 +5621,10 @@ function createApiServer(options) {
|
|
|
5468
5621
|
vectorStore,
|
|
5469
5622
|
queue,
|
|
5470
5623
|
}));
|
|
5471
|
-
app.get('
|
|
5472
|
-
app.get('
|
|
5473
|
-
app.post('
|
|
5474
|
-
app.get('
|
|
5624
|
+
app.get(getEndpoint('issues').path, withCache(cacheTtlMs, createIssuesHandler({ issuesManager })));
|
|
5625
|
+
app.get(getEndpoint('configSchema').path, withCache(cacheTtlMs, createConfigSchemaHandler()));
|
|
5626
|
+
app.post(getEndpoint('configMatch').path, createConfigMatchHandler({ getConfig, logger }));
|
|
5627
|
+
app.get(getEndpoint('config').path, withCache(cacheTtlMs, createConfigQueryHandler({
|
|
5475
5628
|
getConfig,
|
|
5476
5629
|
valuesManager,
|
|
5477
5630
|
issuesManager,
|
|
@@ -5481,7 +5634,7 @@ function createApiServer(options) {
|
|
|
5481
5634
|
? () => virtualRuleStore.getAll()
|
|
5482
5635
|
: undefined,
|
|
5483
5636
|
})));
|
|
5484
|
-
app.post('
|
|
5637
|
+
app.post(getEndpoint('configValidate').path, createConfigValidateHandler({
|
|
5485
5638
|
getConfig,
|
|
5486
5639
|
logger,
|
|
5487
5640
|
configDir: dirname(configPath),
|
|
@@ -5495,7 +5648,7 @@ function createApiServer(options) {
|
|
|
5495
5648
|
return Promise.resolve();
|
|
5496
5649
|
},
|
|
5497
5650
|
});
|
|
5498
|
-
app.post('
|
|
5651
|
+
app.post(getEndpoint('configApply').path, async (request, reply) => {
|
|
5499
5652
|
const { patch, replace } = request.body;
|
|
5500
5653
|
const result = await coreConfigApplyHandler({ patch, replace });
|
|
5501
5654
|
return reply.status(result.status).send(result.body);
|
|
@@ -5514,34 +5667,34 @@ function createApiServer(options) {
|
|
|
5514
5667
|
gitignoreFilter,
|
|
5515
5668
|
vectorStore,
|
|
5516
5669
|
});
|
|
5517
|
-
app.post('
|
|
5670
|
+
app.post(getEndpoint('rulesRegister').path, createRulesRegisterHandler({
|
|
5518
5671
|
virtualRuleStore,
|
|
5519
5672
|
logger,
|
|
5520
5673
|
onRulesChanged,
|
|
5521
5674
|
}));
|
|
5522
|
-
app.delete('
|
|
5675
|
+
app.delete(getEndpoint('rulesUnregister').path, createRulesUnregisterHandler({
|
|
5523
5676
|
virtualRuleStore,
|
|
5524
5677
|
logger,
|
|
5525
5678
|
onRulesChanged,
|
|
5526
5679
|
}));
|
|
5527
|
-
app.delete('
|
|
5680
|
+
app.delete(getEndpoint('rulesUnregisterBySource').path, createRulesUnregisterParamHandler({
|
|
5528
5681
|
virtualRuleStore,
|
|
5529
5682
|
logger,
|
|
5530
5683
|
onRulesChanged,
|
|
5531
5684
|
}));
|
|
5532
|
-
app.post('
|
|
5533
|
-
app.post('
|
|
5685
|
+
app.post(getEndpoint('pointsDelete').path, createPointsDeleteHandler({ vectorStore, logger }));
|
|
5686
|
+
app.post(getEndpoint('rulesReapply').path, createRulesReapplyHandler({ processor, vectorStore, logger }));
|
|
5534
5687
|
}
|
|
5535
5688
|
// VCS routes — only registered when a VcsCoordinator is provided
|
|
5536
5689
|
if (options.vcsCoordinator) {
|
|
5537
5690
|
const coordinator = options.vcsCoordinator;
|
|
5538
|
-
app.get('
|
|
5539
|
-
app.get('
|
|
5540
|
-
app.get('
|
|
5541
|
-
app.get('
|
|
5542
|
-
app.get('
|
|
5543
|
-
app.post('
|
|
5544
|
-
app.post('
|
|
5691
|
+
app.get(getEndpoint('vcsStatus').path, createVcsStatusHandler({ coordinator, logger }));
|
|
5692
|
+
app.get(getEndpoint('vcsHistory').path, createVcsHistoryHandler({ coordinator, logger }));
|
|
5693
|
+
app.get(getEndpoint('vcsShow').path, createVcsShowHandler({ coordinator, logger }));
|
|
5694
|
+
app.get(getEndpoint('vcsDiff').path, createVcsDiffHandler({ coordinator, logger }));
|
|
5695
|
+
app.get(getEndpoint('vcsCheckExclusion').path, createVcsCheckExclusionHandler({ coordinator, logger }));
|
|
5696
|
+
app.post(getEndpoint('vcsRevert').path, createVcsRevertHandler({ coordinator, logger }));
|
|
5697
|
+
app.post(getEndpoint('vcsExclude').path, createVcsExcludeHandler({ coordinator, logger }));
|
|
5545
5698
|
}
|
|
5546
5699
|
return app;
|
|
5547
5700
|
}
|
|
@@ -7855,7 +8008,7 @@ class JeevesWatcher {
|
|
|
7855
8008
|
this.watcher = watcher;
|
|
7856
8009
|
this.gitignoreFilter = gitignoreFilter;
|
|
7857
8010
|
this.server = await this.startApiServer();
|
|
7858
|
-
vcsCoordinator.start();
|
|
8011
|
+
await vcsCoordinator.start();
|
|
7859
8012
|
this.watcher.start();
|
|
7860
8013
|
this.startConfigWatch();
|
|
7861
8014
|
logger.info('jeeves-watcher started');
|