@karmaniverous/jeeves-watcher 0.18.2 → 0.18.4
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.
|
@@ -747,6 +747,104 @@ class ValuesManager extends BinaryFileStore {
|
|
|
747
747
|
}
|
|
748
748
|
}
|
|
749
749
|
|
|
750
|
+
/**
|
|
751
|
+
* @module watcher/globToDir
|
|
752
|
+
* Adapts glob-based watch config to chokidar v4+, which removed glob support
|
|
753
|
+
* (see paulmillr/chokidar#1350). Chokidar v4 treats glob patterns as literal
|
|
754
|
+
* strings, silently producing zero events. This module extracts static directory
|
|
755
|
+
* roots from glob patterns for chokidar to watch, then filters emitted events
|
|
756
|
+
* against the original globs via picomatch.
|
|
757
|
+
*/
|
|
758
|
+
/**
|
|
759
|
+
* Extract the static directory root from a glob pattern.
|
|
760
|
+
* Stops at the first segment containing glob characters (`*`, `{`, `?`, `[`).
|
|
761
|
+
*
|
|
762
|
+
* @param glob - A glob pattern (e.g., `j:/domains/**\/*.json`).
|
|
763
|
+
* @returns The static directory prefix (e.g., `j:/domains`).
|
|
764
|
+
*/
|
|
765
|
+
function globRoot(glob) {
|
|
766
|
+
const normalized = normalizeSlashes(glob);
|
|
767
|
+
const segments = normalized.split('/');
|
|
768
|
+
const staticSegments = [];
|
|
769
|
+
for (const seg of segments) {
|
|
770
|
+
if (/[*?{[\]]/.test(seg))
|
|
771
|
+
break;
|
|
772
|
+
staticSegments.push(seg);
|
|
773
|
+
}
|
|
774
|
+
return staticSegments.join('/') || '.';
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Deduplicate directory roots, removing paths that are subdirectories of others.
|
|
778
|
+
*
|
|
779
|
+
* @param roots - Array of directory paths.
|
|
780
|
+
* @returns Deduplicated array with subdirectories removed.
|
|
781
|
+
*/
|
|
782
|
+
function deduplicateRoots(roots) {
|
|
783
|
+
const normalized = roots.map((r) => normalizeSlashes(r).toLowerCase());
|
|
784
|
+
const sorted = [...new Set(normalized)].sort();
|
|
785
|
+
return sorted.filter((root, _i, arr) => {
|
|
786
|
+
const withSlash = root.endsWith('/') ? root : root + '/';
|
|
787
|
+
return !arr.some((other) => other !== root && withSlash.startsWith(other + '/'));
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Build a picomatch matcher from an array of glob patterns.
|
|
792
|
+
* Normalizes Windows paths (backslash → forward slash, lowercase drive letter)
|
|
793
|
+
* before matching.
|
|
794
|
+
*
|
|
795
|
+
* @param globs - Glob patterns to match against.
|
|
796
|
+
* @returns A function that tests whether a file path matches any of the globs.
|
|
797
|
+
*/
|
|
798
|
+
function buildGlobMatcher(globs) {
|
|
799
|
+
const normalizedGlobs = globs.map((g) => normalizeSlashes(g));
|
|
800
|
+
const isMatch = picomatch(normalizedGlobs, { dot: true, nocase: true });
|
|
801
|
+
return (filePath) => {
|
|
802
|
+
const normalized = normalizeSlashes(filePath);
|
|
803
|
+
return isMatch(normalized);
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Convert an array of glob patterns into chokidar-compatible directory roots
|
|
808
|
+
* and a filter function for post-hoc event filtering.
|
|
809
|
+
*
|
|
810
|
+
* @param globs - Glob patterns from the watch config.
|
|
811
|
+
* @returns Object with `roots` (directories for chokidar) and `matches` (filter function).
|
|
812
|
+
*/
|
|
813
|
+
function resolveWatchPaths(globs) {
|
|
814
|
+
const rawRoots = globs.map(globRoot);
|
|
815
|
+
const roots = deduplicateRoots(rawRoots);
|
|
816
|
+
const matches = buildGlobMatcher(globs);
|
|
817
|
+
return { roots, matches };
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* Convert ignored glob patterns to picomatch matcher functions.
|
|
821
|
+
*
|
|
822
|
+
* Chokidar v5 replaced the external `anymatch` dependency with an inline
|
|
823
|
+
* implementation that does **exact string equality** for string matchers,
|
|
824
|
+
* breaking glob-based `ignored` patterns. This function converts glob strings
|
|
825
|
+
* to picomatch functions that chokidar's `createPattern` passes through
|
|
826
|
+
* unchanged (`typeof matcher === 'function'`).
|
|
827
|
+
*
|
|
828
|
+
* Non-string entries (functions, RegExps) are passed through as-is.
|
|
829
|
+
*
|
|
830
|
+
* @param ignored - Array of ignored patterns (globs, functions, RegExps).
|
|
831
|
+
* @returns Array with glob strings replaced by picomatch matcher functions.
|
|
832
|
+
*/
|
|
833
|
+
function resolveIgnored(ignored) {
|
|
834
|
+
return ignored.map((entry) => {
|
|
835
|
+
if (typeof entry !== 'string')
|
|
836
|
+
return entry;
|
|
837
|
+
// If the string contains glob characters, convert to a picomatch function.
|
|
838
|
+
// Literal strings (exact paths) are also converted for consistent matching.
|
|
839
|
+
const normalizedPattern = normalizeSlashes(entry);
|
|
840
|
+
const matcher = picomatch(normalizedPattern, { dot: true, nocase: true });
|
|
841
|
+
return (filePath) => {
|
|
842
|
+
const normalized = normalizeSlashes(filePath);
|
|
843
|
+
return matcher(normalized);
|
|
844
|
+
};
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
750
848
|
/**
|
|
751
849
|
* @module vcs/CommitMessageGenerator
|
|
752
850
|
* Generates AI-powered commit messages via the Anthropic Messages API.
|
|
@@ -836,17 +934,32 @@ class CommitMessageGenerator {
|
|
|
836
934
|
*/
|
|
837
935
|
/** Promisified execFile for git commands. */
|
|
838
936
|
const execFileAsync = promisify(execFile);
|
|
937
|
+
/**
|
|
938
|
+
* Normalize a path for case-insensitive comparison on Windows.
|
|
939
|
+
* On Windows, lowercases the entire path; on other platforms, returns as-is.
|
|
940
|
+
*
|
|
941
|
+
* @param p - The path string to normalize.
|
|
942
|
+
* @param platform - The platform to check against (default: `process.platform`).
|
|
943
|
+
* @returns The normalized path.
|
|
944
|
+
*/
|
|
945
|
+
function normalizePathCase(p, platform = process.platform) {
|
|
946
|
+
return platform === 'win32' ? p.toLowerCase() : p;
|
|
947
|
+
}
|
|
839
948
|
/**
|
|
840
949
|
* Find the longest-prefix-match root for a normalized path.
|
|
841
950
|
* Roots must be sorted longest-first for correct nested matching.
|
|
842
951
|
*
|
|
843
952
|
* @param roots - Root paths sorted longest-first.
|
|
844
953
|
* @param normalizedPath - Normalized absolute path (forward slashes).
|
|
954
|
+
* @param platform - The platform to check against (default: `process.platform`).
|
|
845
955
|
* @returns The matching root, or undefined.
|
|
846
956
|
*/
|
|
847
|
-
function findRootForPath(roots, normalizedPath) {
|
|
957
|
+
function findRootForPath(roots, normalizedPath, platform = process.platform) {
|
|
958
|
+
const comparePath = normalizePathCase(normalizedPath, platform);
|
|
848
959
|
for (const root of roots) {
|
|
849
|
-
|
|
960
|
+
const compareRoot = normalizePathCase(root, platform);
|
|
961
|
+
if (comparePath === compareRoot ||
|
|
962
|
+
comparePath.startsWith(compareRoot + '/')) {
|
|
850
963
|
return root;
|
|
851
964
|
}
|
|
852
965
|
}
|
|
@@ -1205,6 +1318,7 @@ class VcsManager {
|
|
|
1205
1318
|
debounceTimer;
|
|
1206
1319
|
commitInFlight = Promise.resolve();
|
|
1207
1320
|
started = false;
|
|
1321
|
+
isBaseline = false;
|
|
1208
1322
|
_lastPushTime = null;
|
|
1209
1323
|
constructor(rootPath, config, logger, commitMessageGenerator, remoteUrl, accessToken) {
|
|
1210
1324
|
this.rootPath = rootPath;
|
|
@@ -1225,11 +1339,47 @@ class VcsManager {
|
|
|
1225
1339
|
}
|
|
1226
1340
|
/**
|
|
1227
1341
|
* Begin accepting file changes. Sets up the debounce mechanism.
|
|
1342
|
+
* Enqueues a baseline commit for pre-existing files if the repo has no commits.
|
|
1228
1343
|
*/
|
|
1229
|
-
start() {
|
|
1344
|
+
async start() {
|
|
1230
1345
|
this.started = true;
|
|
1231
1346
|
this.squashManager?.start();
|
|
1232
1347
|
this.logger.info({ root: this.rootPath }, 'VcsManager started');
|
|
1348
|
+
await this.enqueueBaselineIfNeeded();
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* If the repo has no commits, enumerate untracked files and feed them
|
|
1352
|
+
* through fileChanged() to create a baseline commit.
|
|
1353
|
+
*/
|
|
1354
|
+
async enqueueBaselineIfNeeded() {
|
|
1355
|
+
try {
|
|
1356
|
+
await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
1357
|
+
cwd: this.rootPath,
|
|
1358
|
+
});
|
|
1359
|
+
// HEAD exists — repo already has commits
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
catch {
|
|
1363
|
+
// No commits yet — proceed with baseline
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
const { stdout } = await execFileAsync('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd: this.rootPath });
|
|
1367
|
+
const files = stdout
|
|
1368
|
+
.split('\0')
|
|
1369
|
+
.map((line) => line.trim())
|
|
1370
|
+
.filter((line) => line.length > 0);
|
|
1371
|
+
if (files.length === 0)
|
|
1372
|
+
return;
|
|
1373
|
+
this.logger.info({ root: this.rootPath, fileCount: files.length }, 'Enqueuing baseline commit for pre-existing files');
|
|
1374
|
+
this.isBaseline = true;
|
|
1375
|
+
const absolutePaths = files.map((f) => this.rootPath + '/' + f);
|
|
1376
|
+
for (const filePath of absolutePaths) {
|
|
1377
|
+
this.fileChanged(filePath);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
catch (error) {
|
|
1381
|
+
this.logger.warn({ root: this.rootPath, err: normalizeError(error) }, 'Failed to enumerate untracked files for baseline commit');
|
|
1382
|
+
}
|
|
1233
1383
|
}
|
|
1234
1384
|
/**
|
|
1235
1385
|
* Add a file to the pending set and reset the debounce timer.
|
|
@@ -1344,6 +1494,9 @@ class VcsManager {
|
|
|
1344
1494
|
*/
|
|
1345
1495
|
buildTemplateMessage(fileCount) {
|
|
1346
1496
|
if (this.pendingReversions.length === 0) {
|
|
1497
|
+
if (this.isBaseline) {
|
|
1498
|
+
return `baseline: batch (${String(fileCount)} files)`;
|
|
1499
|
+
}
|
|
1347
1500
|
const timestamp = new Date().toISOString();
|
|
1348
1501
|
return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
|
|
1349
1502
|
}
|
|
@@ -1401,10 +1554,15 @@ class VcsManager {
|
|
|
1401
1554
|
cwd: this.rootPath,
|
|
1402
1555
|
});
|
|
1403
1556
|
// Build message after staging so getStagedDiff can see the changes
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1557
|
+
// Skip AI for baselines and retries — use template directly
|
|
1558
|
+
const message = this.isBaseline || attempt > 1
|
|
1559
|
+
? this.buildTemplateMessage(files.length)
|
|
1560
|
+
: await this.buildCommitMessage(files);
|
|
1407
1561
|
this.pendingReversions.length = 0;
|
|
1562
|
+
// Only clear baseline flag when all baseline files have been committed
|
|
1563
|
+
if (this.pending.size === 0) {
|
|
1564
|
+
this.isBaseline = false;
|
|
1565
|
+
}
|
|
1408
1566
|
await execFileAsync('git', ['commit', '-m', message], {
|
|
1409
1567
|
cwd: this.rootPath,
|
|
1410
1568
|
});
|
|
@@ -1480,11 +1638,11 @@ class VcsCoordinator {
|
|
|
1480
1638
|
const rootVcs = entry.vcs?.enabled ?? config.vcs.enabled;
|
|
1481
1639
|
if (!rootVcs)
|
|
1482
1640
|
continue;
|
|
1483
|
-
|
|
1484
|
-
|
|
1641
|
+
const resolvedRoot = normalizeSlashes(resolve(globRoot(entry.path)));
|
|
1642
|
+
const rootKey = normalizePathCase(resolvedRoot);
|
|
1643
|
+
// Deduplicate: skip if another glob already resolved to this root.
|
|
1644
|
+
if (this.managers.has(rootKey))
|
|
1485
1645
|
continue;
|
|
1486
|
-
}
|
|
1487
|
-
const resolvedRoot = normalizeSlashes(resolve(entry.path));
|
|
1488
1646
|
const mergedConfig = {
|
|
1489
1647
|
...config.vcs,
|
|
1490
1648
|
...entry.vcs,
|
|
@@ -1503,7 +1661,7 @@ class VcsCoordinator {
|
|
|
1503
1661
|
const remoteUrl = entry.vcs?.remote;
|
|
1504
1662
|
const accessToken = entry.vcs?.accessToken ?? config.vcs.defaultAccessToken;
|
|
1505
1663
|
const manager = new VcsManager(resolvedRoot, mergedConfig, logger.child({ vcsRoot: resolvedRoot }), generator, remoteUrl, accessToken);
|
|
1506
|
-
this.managers.set(
|
|
1664
|
+
this.managers.set(rootKey, manager);
|
|
1507
1665
|
this.roots.push(resolvedRoot);
|
|
1508
1666
|
}
|
|
1509
1667
|
// Sort roots longest-first so nested paths match before parents.
|
|
@@ -1512,10 +1670,12 @@ class VcsCoordinator {
|
|
|
1512
1670
|
/**
|
|
1513
1671
|
* Start all VcsManager instances.
|
|
1514
1672
|
*/
|
|
1515
|
-
start() {
|
|
1673
|
+
async start() {
|
|
1674
|
+
const startPromises = [];
|
|
1516
1675
|
this.managers.forEach((manager) => {
|
|
1517
|
-
manager.start();
|
|
1676
|
+
startPromises.push(manager.start());
|
|
1518
1677
|
});
|
|
1678
|
+
await Promise.all(startPromises);
|
|
1519
1679
|
this.logger.info({ rootCount: this.managers.size }, 'VcsCoordinator started');
|
|
1520
1680
|
}
|
|
1521
1681
|
/**
|
|
@@ -1557,7 +1717,7 @@ class VcsCoordinator {
|
|
|
1557
1717
|
* Get the VcsManager for a specific root path.
|
|
1558
1718
|
*/
|
|
1559
1719
|
getManager(root) {
|
|
1560
|
-
return this.managers.get(root);
|
|
1720
|
+
return this.managers.get(normalizePathCase(root));
|
|
1561
1721
|
}
|
|
1562
1722
|
/**
|
|
1563
1723
|
* Get all managers as [root, manager] pairs.
|
|
@@ -1574,7 +1734,7 @@ class VcsCoordinator {
|
|
|
1574
1734
|
*/
|
|
1575
1735
|
findManagerForPath(normalizedPath) {
|
|
1576
1736
|
const root = findRootForPath(this.roots, normalizedPath);
|
|
1577
|
-
return root ? this.managers.get(root) : undefined;
|
|
1737
|
+
return root ? this.managers.get(normalizePathCase(root)) : undefined;
|
|
1578
1738
|
}
|
|
1579
1739
|
}
|
|
1580
1740
|
|
|
@@ -2795,15 +2955,18 @@ function resolveWatchRootsForGlob(coordinator, glob) {
|
|
|
2795
2955
|
const normalizedGlob = normalizeSlashes(glob);
|
|
2796
2956
|
const roots = coordinator.getRoots();
|
|
2797
2957
|
const results = [];
|
|
2958
|
+
const compareGlob = normalizePathCase(normalizedGlob);
|
|
2798
2959
|
for (const root of roots) {
|
|
2799
|
-
|
|
2960
|
+
const compareRoot = normalizePathCase(root);
|
|
2961
|
+
if (compareGlob.startsWith(compareRoot + '/') ||
|
|
2962
|
+
compareGlob === compareRoot) {
|
|
2800
2963
|
// Glob is under this root
|
|
2801
2964
|
results.push({
|
|
2802
2965
|
root,
|
|
2803
2966
|
relativePath: normalizeSlashes(relative(root, normalizedGlob)),
|
|
2804
2967
|
});
|
|
2805
2968
|
}
|
|
2806
|
-
else if (
|
|
2969
|
+
else if (compareRoot.startsWith(compareGlob.replace(/[*?[\]{}]/g, ''))) {
|
|
2807
2970
|
// Root is under the glob's base path — include with root-relative glob
|
|
2808
2971
|
results.push({
|
|
2809
2972
|
root,
|
|
@@ -3088,20 +3251,22 @@ async function initVcs(config, logger) {
|
|
|
3088
3251
|
}
|
|
3089
3252
|
const stateDir = config.stateDir ?? '.jeeves-metadata';
|
|
3090
3253
|
const pathStrings = extractWatchPathStrings(config.watch.paths);
|
|
3091
|
-
const staticPaths = pathStrings.
|
|
3254
|
+
const staticPaths = pathStrings.map((p) => globRoot(p));
|
|
3092
3255
|
validateStateDirOverlap(stateDir, staticPaths);
|
|
3093
3256
|
const normalized = normalizeWatchPaths(config.watch.paths);
|
|
3257
|
+
const initializedRoots = new Set();
|
|
3094
3258
|
for (const entry of normalized) {
|
|
3095
3259
|
const rootVcs = entry.vcs?.enabled ?? config.vcs.enabled;
|
|
3096
3260
|
if (!rootVcs)
|
|
3097
3261
|
continue;
|
|
3098
|
-
|
|
3099
|
-
|
|
3262
|
+
const root = globRoot(entry.path);
|
|
3263
|
+
// Deduplicate: skip if another glob already resolved to this root.
|
|
3264
|
+
if (initializedRoots.has(root))
|
|
3100
3265
|
continue;
|
|
3101
|
-
|
|
3102
|
-
await initRepo(
|
|
3103
|
-
await ensureGitignore(
|
|
3104
|
-
logger.info({ root
|
|
3266
|
+
initializedRoots.add(root);
|
|
3267
|
+
await initRepo(root);
|
|
3268
|
+
await ensureGitignore(root);
|
|
3269
|
+
logger.info({ root }, 'VCS initialized for watch root');
|
|
3105
3270
|
}
|
|
3106
3271
|
return true;
|
|
3107
3272
|
}
|
|
@@ -7054,104 +7219,6 @@ class SystemHealth {
|
|
|
7054
7219
|
}
|
|
7055
7220
|
}
|
|
7056
7221
|
|
|
7057
|
-
/**
|
|
7058
|
-
* @module watcher/globToDir
|
|
7059
|
-
* Adapts glob-based watch config to chokidar v4+, which removed glob support
|
|
7060
|
-
* (see paulmillr/chokidar#1350). Chokidar v4 treats glob patterns as literal
|
|
7061
|
-
* strings, silently producing zero events. This module extracts static directory
|
|
7062
|
-
* roots from glob patterns for chokidar to watch, then filters emitted events
|
|
7063
|
-
* against the original globs via picomatch.
|
|
7064
|
-
*/
|
|
7065
|
-
/**
|
|
7066
|
-
* Extract the static directory root from a glob pattern.
|
|
7067
|
-
* Stops at the first segment containing glob characters (`*`, `{`, `?`, `[`).
|
|
7068
|
-
*
|
|
7069
|
-
* @param glob - A glob pattern (e.g., `j:/domains/**\/*.json`).
|
|
7070
|
-
* @returns The static directory prefix (e.g., `j:/domains`).
|
|
7071
|
-
*/
|
|
7072
|
-
function globRoot(glob) {
|
|
7073
|
-
const normalized = normalizeSlashes(glob);
|
|
7074
|
-
const segments = normalized.split('/');
|
|
7075
|
-
const staticSegments = [];
|
|
7076
|
-
for (const seg of segments) {
|
|
7077
|
-
if (/[*?{[\]]/.test(seg))
|
|
7078
|
-
break;
|
|
7079
|
-
staticSegments.push(seg);
|
|
7080
|
-
}
|
|
7081
|
-
return staticSegments.join('/') || '.';
|
|
7082
|
-
}
|
|
7083
|
-
/**
|
|
7084
|
-
* Deduplicate directory roots, removing paths that are subdirectories of others.
|
|
7085
|
-
*
|
|
7086
|
-
* @param roots - Array of directory paths.
|
|
7087
|
-
* @returns Deduplicated array with subdirectories removed.
|
|
7088
|
-
*/
|
|
7089
|
-
function deduplicateRoots(roots) {
|
|
7090
|
-
const normalized = roots.map((r) => normalizeSlashes(r).toLowerCase());
|
|
7091
|
-
const sorted = [...new Set(normalized)].sort();
|
|
7092
|
-
return sorted.filter((root, _i, arr) => {
|
|
7093
|
-
const withSlash = root.endsWith('/') ? root : root + '/';
|
|
7094
|
-
return !arr.some((other) => other !== root && withSlash.startsWith(other + '/'));
|
|
7095
|
-
});
|
|
7096
|
-
}
|
|
7097
|
-
/**
|
|
7098
|
-
* Build a picomatch matcher from an array of glob patterns.
|
|
7099
|
-
* Normalizes Windows paths (backslash → forward slash, lowercase drive letter)
|
|
7100
|
-
* before matching.
|
|
7101
|
-
*
|
|
7102
|
-
* @param globs - Glob patterns to match against.
|
|
7103
|
-
* @returns A function that tests whether a file path matches any of the globs.
|
|
7104
|
-
*/
|
|
7105
|
-
function buildGlobMatcher(globs) {
|
|
7106
|
-
const normalizedGlobs = globs.map((g) => normalizeSlashes(g));
|
|
7107
|
-
const isMatch = picomatch(normalizedGlobs, { dot: true, nocase: true });
|
|
7108
|
-
return (filePath) => {
|
|
7109
|
-
const normalized = normalizeSlashes(filePath);
|
|
7110
|
-
return isMatch(normalized);
|
|
7111
|
-
};
|
|
7112
|
-
}
|
|
7113
|
-
/**
|
|
7114
|
-
* Convert an array of glob patterns into chokidar-compatible directory roots
|
|
7115
|
-
* and a filter function for post-hoc event filtering.
|
|
7116
|
-
*
|
|
7117
|
-
* @param globs - Glob patterns from the watch config.
|
|
7118
|
-
* @returns Object with `roots` (directories for chokidar) and `matches` (filter function).
|
|
7119
|
-
*/
|
|
7120
|
-
function resolveWatchPaths(globs) {
|
|
7121
|
-
const rawRoots = globs.map(globRoot);
|
|
7122
|
-
const roots = deduplicateRoots(rawRoots);
|
|
7123
|
-
const matches = buildGlobMatcher(globs);
|
|
7124
|
-
return { roots, matches };
|
|
7125
|
-
}
|
|
7126
|
-
/**
|
|
7127
|
-
* Convert ignored glob patterns to picomatch matcher functions.
|
|
7128
|
-
*
|
|
7129
|
-
* Chokidar v5 replaced the external `anymatch` dependency with an inline
|
|
7130
|
-
* implementation that does **exact string equality** for string matchers,
|
|
7131
|
-
* breaking glob-based `ignored` patterns. This function converts glob strings
|
|
7132
|
-
* to picomatch functions that chokidar's `createPattern` passes through
|
|
7133
|
-
* unchanged (`typeof matcher === 'function'`).
|
|
7134
|
-
*
|
|
7135
|
-
* Non-string entries (functions, RegExps) are passed through as-is.
|
|
7136
|
-
*
|
|
7137
|
-
* @param ignored - Array of ignored patterns (globs, functions, RegExps).
|
|
7138
|
-
* @returns Array with glob strings replaced by picomatch matcher functions.
|
|
7139
|
-
*/
|
|
7140
|
-
function resolveIgnored(ignored) {
|
|
7141
|
-
return ignored.map((entry) => {
|
|
7142
|
-
if (typeof entry !== 'string')
|
|
7143
|
-
return entry;
|
|
7144
|
-
// If the string contains glob characters, convert to a picomatch function.
|
|
7145
|
-
// Literal strings (exact paths) are also converted for consistent matching.
|
|
7146
|
-
const normalizedPattern = normalizeSlashes(entry);
|
|
7147
|
-
const matcher = picomatch(normalizedPattern, { dot: true, nocase: true });
|
|
7148
|
-
return (filePath) => {
|
|
7149
|
-
const normalized = normalizeSlashes(filePath);
|
|
7150
|
-
return matcher(normalized);
|
|
7151
|
-
};
|
|
7152
|
-
});
|
|
7153
|
-
}
|
|
7154
|
-
|
|
7155
7222
|
/**
|
|
7156
7223
|
* @module watcher/MoveCorrelator
|
|
7157
7224
|
* Correlates unlink+add events as file moves using content hash matching.
|
|
@@ -7693,7 +7760,7 @@ class JeevesWatcher {
|
|
|
7693
7760
|
this.watcher = watcher;
|
|
7694
7761
|
this.gitignoreFilter = gitignoreFilter;
|
|
7695
7762
|
this.server = await this.startApiServer();
|
|
7696
|
-
vcsCoordinator.start();
|
|
7763
|
+
await vcsCoordinator.start();
|
|
7697
7764
|
this.watcher.start();
|
|
7698
7765
|
this.startConfigWatch();
|
|
7699
7766
|
logger.info('jeeves-watcher started');
|
|
@@ -224,7 +224,7 @@ async function startFromConfig(configPath, descriptor) {
|
|
|
224
224
|
const config = await loadConfig(resolvedPath);
|
|
225
225
|
// Dynamic import breaks the circular dependency between this module
|
|
226
226
|
// and app/index.ts (which defines JeevesWatcher and re-exports startFromConfig).
|
|
227
|
-
const { JeevesWatcher } = await import('./index-
|
|
227
|
+
const { JeevesWatcher } = await import('./index-ChYxOGtD.js');
|
|
228
228
|
const app = new JeevesWatcher(config, resolvedPath, descriptor);
|
|
229
229
|
installShutdownHandlers(() => app.stop());
|
|
230
230
|
await app.start();
|
package/dist/index.d.ts
CHANGED
|
@@ -1291,14 +1291,21 @@ declare class VcsManager {
|
|
|
1291
1291
|
private debounceTimer;
|
|
1292
1292
|
private commitInFlight;
|
|
1293
1293
|
private started;
|
|
1294
|
+
private isBaseline;
|
|
1294
1295
|
private _lastPushTime;
|
|
1295
1296
|
constructor(rootPath: string, config: VcsConfig, logger: pino.Logger, commitMessageGenerator?: CommitMessageGenerator, remoteUrl?: string, accessToken?: string);
|
|
1296
1297
|
get lastPushTime(): string | null;
|
|
1297
1298
|
get pushErrors(): readonly PushError[];
|
|
1298
1299
|
/**
|
|
1299
1300
|
* Begin accepting file changes. Sets up the debounce mechanism.
|
|
1301
|
+
* Enqueues a baseline commit for pre-existing files if the repo has no commits.
|
|
1300
1302
|
*/
|
|
1301
|
-
start(): void
|
|
1303
|
+
start(): Promise<void>;
|
|
1304
|
+
/**
|
|
1305
|
+
* If the repo has no commits, enumerate untracked files and feed them
|
|
1306
|
+
* through fileChanged() to create a baseline commit.
|
|
1307
|
+
*/
|
|
1308
|
+
private enqueueBaselineIfNeeded;
|
|
1302
1309
|
/**
|
|
1303
1310
|
* Add a file to the pending set and reset the debounce timer.
|
|
1304
1311
|
* If the pending set exceeds maxBatchSize, flush immediately.
|
|
@@ -1379,7 +1386,7 @@ declare class VcsCoordinator {
|
|
|
1379
1386
|
/**
|
|
1380
1387
|
* Start all VcsManager instances.
|
|
1381
1388
|
*/
|
|
1382
|
-
start(): void
|
|
1389
|
+
start(): Promise<void>;
|
|
1383
1390
|
/**
|
|
1384
1391
|
* Route a file event to the correct root's VcsManager.
|
|
1385
1392
|
*
|
package/dist/index.js
CHANGED
|
@@ -3375,17 +3375,32 @@ function createSearchHandler(deps) {
|
|
|
3375
3375
|
*/
|
|
3376
3376
|
/** Promisified execFile for git commands. */
|
|
3377
3377
|
const execFileAsync = promisify(execFile);
|
|
3378
|
+
/**
|
|
3379
|
+
* Normalize a path for case-insensitive comparison on Windows.
|
|
3380
|
+
* On Windows, lowercases the entire path; on other platforms, returns as-is.
|
|
3381
|
+
*
|
|
3382
|
+
* @param p - The path string to normalize.
|
|
3383
|
+
* @param platform - The platform to check against (default: `process.platform`).
|
|
3384
|
+
* @returns The normalized path.
|
|
3385
|
+
*/
|
|
3386
|
+
function normalizePathCase(p, platform = process.platform) {
|
|
3387
|
+
return platform === 'win32' ? p.toLowerCase() : p;
|
|
3388
|
+
}
|
|
3378
3389
|
/**
|
|
3379
3390
|
* Find the longest-prefix-match root for a normalized path.
|
|
3380
3391
|
* Roots must be sorted longest-first for correct nested matching.
|
|
3381
3392
|
*
|
|
3382
3393
|
* @param roots - Root paths sorted longest-first.
|
|
3383
3394
|
* @param normalizedPath - Normalized absolute path (forward slashes).
|
|
3395
|
+
* @param platform - The platform to check against (default: `process.platform`).
|
|
3384
3396
|
* @returns The matching root, or undefined.
|
|
3385
3397
|
*/
|
|
3386
|
-
function findRootForPath(roots, normalizedPath) {
|
|
3398
|
+
function findRootForPath(roots, normalizedPath, platform = process.platform) {
|
|
3399
|
+
const comparePath = normalizePathCase(normalizedPath, platform);
|
|
3387
3400
|
for (const root of roots) {
|
|
3388
|
-
|
|
3401
|
+
const compareRoot = normalizePathCase(root, platform);
|
|
3402
|
+
if (comparePath === compareRoot ||
|
|
3403
|
+
comparePath.startsWith(compareRoot + '/')) {
|
|
3389
3404
|
return root;
|
|
3390
3405
|
}
|
|
3391
3406
|
}
|
|
@@ -3425,15 +3440,18 @@ function resolveWatchRootsForGlob(coordinator, glob) {
|
|
|
3425
3440
|
const normalizedGlob = normalizeSlashes(glob);
|
|
3426
3441
|
const roots = coordinator.getRoots();
|
|
3427
3442
|
const results = [];
|
|
3443
|
+
const compareGlob = normalizePathCase(normalizedGlob);
|
|
3428
3444
|
for (const root of roots) {
|
|
3429
|
-
|
|
3445
|
+
const compareRoot = normalizePathCase(root);
|
|
3446
|
+
if (compareGlob.startsWith(compareRoot + '/') ||
|
|
3447
|
+
compareGlob === compareRoot) {
|
|
3430
3448
|
// Glob is under this root
|
|
3431
3449
|
results.push({
|
|
3432
3450
|
root,
|
|
3433
3451
|
relativePath: normalizeSlashes(relative(root, normalizedGlob)),
|
|
3434
3452
|
});
|
|
3435
3453
|
}
|
|
3436
|
-
else if (
|
|
3454
|
+
else if (compareRoot.startsWith(compareGlob.replace(/[*?[\]{}]/g, ''))) {
|
|
3437
3455
|
// Root is under the glob's base path — include with root-relative glob
|
|
3438
3456
|
results.push({
|
|
3439
3457
|
root,
|
|
@@ -4654,6 +4672,104 @@ async function ensureGitignore(rootPath, alwaysOnEntries = []) {
|
|
|
4654
4672
|
}
|
|
4655
4673
|
}
|
|
4656
4674
|
|
|
4675
|
+
/**
|
|
4676
|
+
* @module watcher/globToDir
|
|
4677
|
+
* Adapts glob-based watch config to chokidar v4+, which removed glob support
|
|
4678
|
+
* (see paulmillr/chokidar#1350). Chokidar v4 treats glob patterns as literal
|
|
4679
|
+
* strings, silently producing zero events. This module extracts static directory
|
|
4680
|
+
* roots from glob patterns for chokidar to watch, then filters emitted events
|
|
4681
|
+
* against the original globs via picomatch.
|
|
4682
|
+
*/
|
|
4683
|
+
/**
|
|
4684
|
+
* Extract the static directory root from a glob pattern.
|
|
4685
|
+
* Stops at the first segment containing glob characters (`*`, `{`, `?`, `[`).
|
|
4686
|
+
*
|
|
4687
|
+
* @param glob - A glob pattern (e.g., `j:/domains/**\/*.json`).
|
|
4688
|
+
* @returns The static directory prefix (e.g., `j:/domains`).
|
|
4689
|
+
*/
|
|
4690
|
+
function globRoot(glob) {
|
|
4691
|
+
const normalized = normalizeSlashes(glob);
|
|
4692
|
+
const segments = normalized.split('/');
|
|
4693
|
+
const staticSegments = [];
|
|
4694
|
+
for (const seg of segments) {
|
|
4695
|
+
if (/[*?{[\]]/.test(seg))
|
|
4696
|
+
break;
|
|
4697
|
+
staticSegments.push(seg);
|
|
4698
|
+
}
|
|
4699
|
+
return staticSegments.join('/') || '.';
|
|
4700
|
+
}
|
|
4701
|
+
/**
|
|
4702
|
+
* Deduplicate directory roots, removing paths that are subdirectories of others.
|
|
4703
|
+
*
|
|
4704
|
+
* @param roots - Array of directory paths.
|
|
4705
|
+
* @returns Deduplicated array with subdirectories removed.
|
|
4706
|
+
*/
|
|
4707
|
+
function deduplicateRoots(roots) {
|
|
4708
|
+
const normalized = roots.map((r) => normalizeSlashes(r).toLowerCase());
|
|
4709
|
+
const sorted = [...new Set(normalized)].sort();
|
|
4710
|
+
return sorted.filter((root, _i, arr) => {
|
|
4711
|
+
const withSlash = root.endsWith('/') ? root : root + '/';
|
|
4712
|
+
return !arr.some((other) => other !== root && withSlash.startsWith(other + '/'));
|
|
4713
|
+
});
|
|
4714
|
+
}
|
|
4715
|
+
/**
|
|
4716
|
+
* Build a picomatch matcher from an array of glob patterns.
|
|
4717
|
+
* Normalizes Windows paths (backslash → forward slash, lowercase drive letter)
|
|
4718
|
+
* before matching.
|
|
4719
|
+
*
|
|
4720
|
+
* @param globs - Glob patterns to match against.
|
|
4721
|
+
* @returns A function that tests whether a file path matches any of the globs.
|
|
4722
|
+
*/
|
|
4723
|
+
function buildGlobMatcher(globs) {
|
|
4724
|
+
const normalizedGlobs = globs.map((g) => normalizeSlashes(g));
|
|
4725
|
+
const isMatch = picomatch(normalizedGlobs, { dot: true, nocase: true });
|
|
4726
|
+
return (filePath) => {
|
|
4727
|
+
const normalized = normalizeSlashes(filePath);
|
|
4728
|
+
return isMatch(normalized);
|
|
4729
|
+
};
|
|
4730
|
+
}
|
|
4731
|
+
/**
|
|
4732
|
+
* Convert an array of glob patterns into chokidar-compatible directory roots
|
|
4733
|
+
* and a filter function for post-hoc event filtering.
|
|
4734
|
+
*
|
|
4735
|
+
* @param globs - Glob patterns from the watch config.
|
|
4736
|
+
* @returns Object with `roots` (directories for chokidar) and `matches` (filter function).
|
|
4737
|
+
*/
|
|
4738
|
+
function resolveWatchPaths(globs) {
|
|
4739
|
+
const rawRoots = globs.map(globRoot);
|
|
4740
|
+
const roots = deduplicateRoots(rawRoots);
|
|
4741
|
+
const matches = buildGlobMatcher(globs);
|
|
4742
|
+
return { roots, matches };
|
|
4743
|
+
}
|
|
4744
|
+
/**
|
|
4745
|
+
* Convert ignored glob patterns to picomatch matcher functions.
|
|
4746
|
+
*
|
|
4747
|
+
* Chokidar v5 replaced the external `anymatch` dependency with an inline
|
|
4748
|
+
* implementation that does **exact string equality** for string matchers,
|
|
4749
|
+
* breaking glob-based `ignored` patterns. This function converts glob strings
|
|
4750
|
+
* to picomatch functions that chokidar's `createPattern` passes through
|
|
4751
|
+
* unchanged (`typeof matcher === 'function'`).
|
|
4752
|
+
*
|
|
4753
|
+
* Non-string entries (functions, RegExps) are passed through as-is.
|
|
4754
|
+
*
|
|
4755
|
+
* @param ignored - Array of ignored patterns (globs, functions, RegExps).
|
|
4756
|
+
* @returns Array with glob strings replaced by picomatch matcher functions.
|
|
4757
|
+
*/
|
|
4758
|
+
function resolveIgnored(ignored) {
|
|
4759
|
+
return ignored.map((entry) => {
|
|
4760
|
+
if (typeof entry !== 'string')
|
|
4761
|
+
return entry;
|
|
4762
|
+
// If the string contains glob characters, convert to a picomatch function.
|
|
4763
|
+
// Literal strings (exact paths) are also converted for consistent matching.
|
|
4764
|
+
const normalizedPattern = normalizeSlashes(entry);
|
|
4765
|
+
const matcher = picomatch(normalizedPattern, { dot: true, nocase: true });
|
|
4766
|
+
return (filePath) => {
|
|
4767
|
+
const normalized = normalizeSlashes(filePath);
|
|
4768
|
+
return matcher(normalized);
|
|
4769
|
+
};
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4772
|
+
|
|
4657
4773
|
/**
|
|
4658
4774
|
* @module vcs/VcsManager
|
|
4659
4775
|
* Per-root VCS manager for git-backed content versioning.
|
|
@@ -4682,6 +4798,7 @@ class VcsManager {
|
|
|
4682
4798
|
debounceTimer;
|
|
4683
4799
|
commitInFlight = Promise.resolve();
|
|
4684
4800
|
started = false;
|
|
4801
|
+
isBaseline = false;
|
|
4685
4802
|
_lastPushTime = null;
|
|
4686
4803
|
constructor(rootPath, config, logger, commitMessageGenerator, remoteUrl, accessToken) {
|
|
4687
4804
|
this.rootPath = rootPath;
|
|
@@ -4702,11 +4819,47 @@ class VcsManager {
|
|
|
4702
4819
|
}
|
|
4703
4820
|
/**
|
|
4704
4821
|
* Begin accepting file changes. Sets up the debounce mechanism.
|
|
4822
|
+
* Enqueues a baseline commit for pre-existing files if the repo has no commits.
|
|
4705
4823
|
*/
|
|
4706
|
-
start() {
|
|
4824
|
+
async start() {
|
|
4707
4825
|
this.started = true;
|
|
4708
4826
|
this.squashManager?.start();
|
|
4709
4827
|
this.logger.info({ root: this.rootPath }, 'VcsManager started');
|
|
4828
|
+
await this.enqueueBaselineIfNeeded();
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* If the repo has no commits, enumerate untracked files and feed them
|
|
4832
|
+
* through fileChanged() to create a baseline commit.
|
|
4833
|
+
*/
|
|
4834
|
+
async enqueueBaselineIfNeeded() {
|
|
4835
|
+
try {
|
|
4836
|
+
await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
4837
|
+
cwd: this.rootPath,
|
|
4838
|
+
});
|
|
4839
|
+
// HEAD exists — repo already has commits
|
|
4840
|
+
return;
|
|
4841
|
+
}
|
|
4842
|
+
catch {
|
|
4843
|
+
// No commits yet — proceed with baseline
|
|
4844
|
+
}
|
|
4845
|
+
try {
|
|
4846
|
+
const { stdout } = await execFileAsync('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd: this.rootPath });
|
|
4847
|
+
const files = stdout
|
|
4848
|
+
.split('\0')
|
|
4849
|
+
.map((line) => line.trim())
|
|
4850
|
+
.filter((line) => line.length > 0);
|
|
4851
|
+
if (files.length === 0)
|
|
4852
|
+
return;
|
|
4853
|
+
this.logger.info({ root: this.rootPath, fileCount: files.length }, 'Enqueuing baseline commit for pre-existing files');
|
|
4854
|
+
this.isBaseline = true;
|
|
4855
|
+
const absolutePaths = files.map((f) => this.rootPath + '/' + f);
|
|
4856
|
+
for (const filePath of absolutePaths) {
|
|
4857
|
+
this.fileChanged(filePath);
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
catch (error) {
|
|
4861
|
+
this.logger.warn({ root: this.rootPath, err: normalizeError(error) }, 'Failed to enumerate untracked files for baseline commit');
|
|
4862
|
+
}
|
|
4710
4863
|
}
|
|
4711
4864
|
/**
|
|
4712
4865
|
* Add a file to the pending set and reset the debounce timer.
|
|
@@ -4821,6 +4974,9 @@ class VcsManager {
|
|
|
4821
4974
|
*/
|
|
4822
4975
|
buildTemplateMessage(fileCount) {
|
|
4823
4976
|
if (this.pendingReversions.length === 0) {
|
|
4977
|
+
if (this.isBaseline) {
|
|
4978
|
+
return `baseline: batch (${String(fileCount)} files)`;
|
|
4979
|
+
}
|
|
4824
4980
|
const timestamp = new Date().toISOString();
|
|
4825
4981
|
return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
|
|
4826
4982
|
}
|
|
@@ -4878,10 +5034,15 @@ class VcsManager {
|
|
|
4878
5034
|
cwd: this.rootPath,
|
|
4879
5035
|
});
|
|
4880
5036
|
// Build message after staging so getStagedDiff can see the changes
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5037
|
+
// Skip AI for baselines and retries — use template directly
|
|
5038
|
+
const message = this.isBaseline || attempt > 1
|
|
5039
|
+
? this.buildTemplateMessage(files.length)
|
|
5040
|
+
: await this.buildCommitMessage(files);
|
|
4884
5041
|
this.pendingReversions.length = 0;
|
|
5042
|
+
// Only clear baseline flag when all baseline files have been committed
|
|
5043
|
+
if (this.pending.size === 0) {
|
|
5044
|
+
this.isBaseline = false;
|
|
5045
|
+
}
|
|
4885
5046
|
await execFileAsync('git', ['commit', '-m', message], {
|
|
4886
5047
|
cwd: this.rootPath,
|
|
4887
5048
|
});
|
|
@@ -4957,11 +5118,11 @@ class VcsCoordinator {
|
|
|
4957
5118
|
const rootVcs = entry.vcs?.enabled ?? config.vcs.enabled;
|
|
4958
5119
|
if (!rootVcs)
|
|
4959
5120
|
continue;
|
|
4960
|
-
|
|
4961
|
-
|
|
5121
|
+
const resolvedRoot = normalizeSlashes(resolve(globRoot(entry.path)));
|
|
5122
|
+
const rootKey = normalizePathCase(resolvedRoot);
|
|
5123
|
+
// Deduplicate: skip if another glob already resolved to this root.
|
|
5124
|
+
if (this.managers.has(rootKey))
|
|
4962
5125
|
continue;
|
|
4963
|
-
}
|
|
4964
|
-
const resolvedRoot = normalizeSlashes(resolve(entry.path));
|
|
4965
5126
|
const mergedConfig = {
|
|
4966
5127
|
...config.vcs,
|
|
4967
5128
|
...entry.vcs,
|
|
@@ -4980,7 +5141,7 @@ class VcsCoordinator {
|
|
|
4980
5141
|
const remoteUrl = entry.vcs?.remote;
|
|
4981
5142
|
const accessToken = entry.vcs?.accessToken ?? config.vcs.defaultAccessToken;
|
|
4982
5143
|
const manager = new VcsManager(resolvedRoot, mergedConfig, logger.child({ vcsRoot: resolvedRoot }), generator, remoteUrl, accessToken);
|
|
4983
|
-
this.managers.set(
|
|
5144
|
+
this.managers.set(rootKey, manager);
|
|
4984
5145
|
this.roots.push(resolvedRoot);
|
|
4985
5146
|
}
|
|
4986
5147
|
// Sort roots longest-first so nested paths match before parents.
|
|
@@ -4989,10 +5150,12 @@ class VcsCoordinator {
|
|
|
4989
5150
|
/**
|
|
4990
5151
|
* Start all VcsManager instances.
|
|
4991
5152
|
*/
|
|
4992
|
-
start() {
|
|
5153
|
+
async start() {
|
|
5154
|
+
const startPromises = [];
|
|
4993
5155
|
this.managers.forEach((manager) => {
|
|
4994
|
-
manager.start();
|
|
5156
|
+
startPromises.push(manager.start());
|
|
4995
5157
|
});
|
|
5158
|
+
await Promise.all(startPromises);
|
|
4996
5159
|
this.logger.info({ rootCount: this.managers.size }, 'VcsCoordinator started');
|
|
4997
5160
|
}
|
|
4998
5161
|
/**
|
|
@@ -5034,7 +5197,7 @@ class VcsCoordinator {
|
|
|
5034
5197
|
* Get the VcsManager for a specific root path.
|
|
5035
5198
|
*/
|
|
5036
5199
|
getManager(root) {
|
|
5037
|
-
return this.managers.get(root);
|
|
5200
|
+
return this.managers.get(normalizePathCase(root));
|
|
5038
5201
|
}
|
|
5039
5202
|
/**
|
|
5040
5203
|
* Get all managers as [root, manager] pairs.
|
|
@@ -5051,7 +5214,7 @@ class VcsCoordinator {
|
|
|
5051
5214
|
*/
|
|
5052
5215
|
findManagerForPath(normalizedPath) {
|
|
5053
5216
|
const root = findRootForPath(this.roots, normalizedPath);
|
|
5054
|
-
return root ? this.managers.get(root) : undefined;
|
|
5217
|
+
return root ? this.managers.get(normalizePathCase(root)) : undefined;
|
|
5055
5218
|
}
|
|
5056
5219
|
}
|
|
5057
5220
|
|
|
@@ -5224,20 +5387,22 @@ async function initVcs(config, logger) {
|
|
|
5224
5387
|
}
|
|
5225
5388
|
const stateDir = config.stateDir ?? '.jeeves-metadata';
|
|
5226
5389
|
const pathStrings = extractWatchPathStrings(config.watch.paths);
|
|
5227
|
-
const staticPaths = pathStrings.
|
|
5390
|
+
const staticPaths = pathStrings.map((p) => globRoot(p));
|
|
5228
5391
|
validateStateDirOverlap(stateDir, staticPaths);
|
|
5229
5392
|
const normalized = normalizeWatchPaths(config.watch.paths);
|
|
5393
|
+
const initializedRoots = new Set();
|
|
5230
5394
|
for (const entry of normalized) {
|
|
5231
5395
|
const rootVcs = entry.vcs?.enabled ?? config.vcs.enabled;
|
|
5232
5396
|
if (!rootVcs)
|
|
5233
5397
|
continue;
|
|
5234
|
-
|
|
5235
|
-
|
|
5398
|
+
const root = globRoot(entry.path);
|
|
5399
|
+
// Deduplicate: skip if another glob already resolved to this root.
|
|
5400
|
+
if (initializedRoots.has(root))
|
|
5236
5401
|
continue;
|
|
5237
|
-
|
|
5238
|
-
await initRepo(
|
|
5239
|
-
await ensureGitignore(
|
|
5240
|
-
logger.info({ root
|
|
5402
|
+
initializedRoots.add(root);
|
|
5403
|
+
await initRepo(root);
|
|
5404
|
+
await ensureGitignore(root);
|
|
5405
|
+
logger.info({ root }, 'VCS initialized for watch root');
|
|
5241
5406
|
}
|
|
5242
5407
|
return true;
|
|
5243
5408
|
}
|
|
@@ -8093,104 +8258,6 @@ class SystemHealth {
|
|
|
8093
8258
|
}
|
|
8094
8259
|
}
|
|
8095
8260
|
|
|
8096
|
-
/**
|
|
8097
|
-
* @module watcher/globToDir
|
|
8098
|
-
* Adapts glob-based watch config to chokidar v4+, which removed glob support
|
|
8099
|
-
* (see paulmillr/chokidar#1350). Chokidar v4 treats glob patterns as literal
|
|
8100
|
-
* strings, silently producing zero events. This module extracts static directory
|
|
8101
|
-
* roots from glob patterns for chokidar to watch, then filters emitted events
|
|
8102
|
-
* against the original globs via picomatch.
|
|
8103
|
-
*/
|
|
8104
|
-
/**
|
|
8105
|
-
* Extract the static directory root from a glob pattern.
|
|
8106
|
-
* Stops at the first segment containing glob characters (`*`, `{`, `?`, `[`).
|
|
8107
|
-
*
|
|
8108
|
-
* @param glob - A glob pattern (e.g., `j:/domains/**\/*.json`).
|
|
8109
|
-
* @returns The static directory prefix (e.g., `j:/domains`).
|
|
8110
|
-
*/
|
|
8111
|
-
function globRoot(glob) {
|
|
8112
|
-
const normalized = normalizeSlashes(glob);
|
|
8113
|
-
const segments = normalized.split('/');
|
|
8114
|
-
const staticSegments = [];
|
|
8115
|
-
for (const seg of segments) {
|
|
8116
|
-
if (/[*?{[\]]/.test(seg))
|
|
8117
|
-
break;
|
|
8118
|
-
staticSegments.push(seg);
|
|
8119
|
-
}
|
|
8120
|
-
return staticSegments.join('/') || '.';
|
|
8121
|
-
}
|
|
8122
|
-
/**
|
|
8123
|
-
* Deduplicate directory roots, removing paths that are subdirectories of others.
|
|
8124
|
-
*
|
|
8125
|
-
* @param roots - Array of directory paths.
|
|
8126
|
-
* @returns Deduplicated array with subdirectories removed.
|
|
8127
|
-
*/
|
|
8128
|
-
function deduplicateRoots(roots) {
|
|
8129
|
-
const normalized = roots.map((r) => normalizeSlashes(r).toLowerCase());
|
|
8130
|
-
const sorted = [...new Set(normalized)].sort();
|
|
8131
|
-
return sorted.filter((root, _i, arr) => {
|
|
8132
|
-
const withSlash = root.endsWith('/') ? root : root + '/';
|
|
8133
|
-
return !arr.some((other) => other !== root && withSlash.startsWith(other + '/'));
|
|
8134
|
-
});
|
|
8135
|
-
}
|
|
8136
|
-
/**
|
|
8137
|
-
* Build a picomatch matcher from an array of glob patterns.
|
|
8138
|
-
* Normalizes Windows paths (backslash → forward slash, lowercase drive letter)
|
|
8139
|
-
* before matching.
|
|
8140
|
-
*
|
|
8141
|
-
* @param globs - Glob patterns to match against.
|
|
8142
|
-
* @returns A function that tests whether a file path matches any of the globs.
|
|
8143
|
-
*/
|
|
8144
|
-
function buildGlobMatcher(globs) {
|
|
8145
|
-
const normalizedGlobs = globs.map((g) => normalizeSlashes(g));
|
|
8146
|
-
const isMatch = picomatch(normalizedGlobs, { dot: true, nocase: true });
|
|
8147
|
-
return (filePath) => {
|
|
8148
|
-
const normalized = normalizeSlashes(filePath);
|
|
8149
|
-
return isMatch(normalized);
|
|
8150
|
-
};
|
|
8151
|
-
}
|
|
8152
|
-
/**
|
|
8153
|
-
* Convert an array of glob patterns into chokidar-compatible directory roots
|
|
8154
|
-
* and a filter function for post-hoc event filtering.
|
|
8155
|
-
*
|
|
8156
|
-
* @param globs - Glob patterns from the watch config.
|
|
8157
|
-
* @returns Object with `roots` (directories for chokidar) and `matches` (filter function).
|
|
8158
|
-
*/
|
|
8159
|
-
function resolveWatchPaths(globs) {
|
|
8160
|
-
const rawRoots = globs.map(globRoot);
|
|
8161
|
-
const roots = deduplicateRoots(rawRoots);
|
|
8162
|
-
const matches = buildGlobMatcher(globs);
|
|
8163
|
-
return { roots, matches };
|
|
8164
|
-
}
|
|
8165
|
-
/**
|
|
8166
|
-
* Convert ignored glob patterns to picomatch matcher functions.
|
|
8167
|
-
*
|
|
8168
|
-
* Chokidar v5 replaced the external `anymatch` dependency with an inline
|
|
8169
|
-
* implementation that does **exact string equality** for string matchers,
|
|
8170
|
-
* breaking glob-based `ignored` patterns. This function converts glob strings
|
|
8171
|
-
* to picomatch functions that chokidar's `createPattern` passes through
|
|
8172
|
-
* unchanged (`typeof matcher === 'function'`).
|
|
8173
|
-
*
|
|
8174
|
-
* Non-string entries (functions, RegExps) are passed through as-is.
|
|
8175
|
-
*
|
|
8176
|
-
* @param ignored - Array of ignored patterns (globs, functions, RegExps).
|
|
8177
|
-
* @returns Array with glob strings replaced by picomatch matcher functions.
|
|
8178
|
-
*/
|
|
8179
|
-
function resolveIgnored(ignored) {
|
|
8180
|
-
return ignored.map((entry) => {
|
|
8181
|
-
if (typeof entry !== 'string')
|
|
8182
|
-
return entry;
|
|
8183
|
-
// If the string contains glob characters, convert to a picomatch function.
|
|
8184
|
-
// Literal strings (exact paths) are also converted for consistent matching.
|
|
8185
|
-
const normalizedPattern = normalizeSlashes(entry);
|
|
8186
|
-
const matcher = picomatch(normalizedPattern, { dot: true, nocase: true });
|
|
8187
|
-
return (filePath) => {
|
|
8188
|
-
const normalized = normalizeSlashes(filePath);
|
|
8189
|
-
return matcher(normalized);
|
|
8190
|
-
};
|
|
8191
|
-
});
|
|
8192
|
-
}
|
|
8193
|
-
|
|
8194
8261
|
/**
|
|
8195
8262
|
* @module watcher/MoveCorrelator
|
|
8196
8263
|
* Correlates unlink+add events as file moves using content hash matching.
|
|
@@ -8844,7 +8911,7 @@ class JeevesWatcher {
|
|
|
8844
8911
|
this.watcher = watcher;
|
|
8845
8912
|
this.gitignoreFilter = gitignoreFilter;
|
|
8846
8913
|
this.server = await this.startApiServer();
|
|
8847
|
-
vcsCoordinator.start();
|
|
8914
|
+
await vcsCoordinator.start();
|
|
8848
8915
|
this.watcher.start();
|
|
8849
8916
|
this.startConfigWatch();
|
|
8850
8917
|
logger.info('jeeves-watcher started');
|
package/package.json
CHANGED