@karmaniverous/jeeves-watcher 0.18.3 → 0.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.schema.json +268 -219
- package/dist/cli/jeeves-watcher/{index-Dk_WrV0T.js → index-CaibsSrT.js} +106 -16
- package/dist/cli/jeeves-watcher/index.js +1 -1
- package/dist/index.d.ts +9 -2
- package/dist/index.js +106 -16
- package/package.json +2 -2
|
@@ -10,7 +10,7 @@ import Ajv from 'ajv';
|
|
|
10
10
|
import addFormats from 'ajv-formats';
|
|
11
11
|
import picomatch from 'picomatch';
|
|
12
12
|
import { deserialize, serialize } from 'node:v8';
|
|
13
|
-
import { normalizeWatchPaths, extractWatchPathStrings, inferenceRuleSchema, jeevesWatcherConfigSchema } from '@karmaniverous/jeeves-watcher-core';
|
|
13
|
+
import { normalizeWatchPaths, extractWatchPathStrings, vcsAuthorConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema } from '@karmaniverous/jeeves-watcher-core';
|
|
14
14
|
import { execFile } from 'node:child_process';
|
|
15
15
|
import { promisify } from 'node:util';
|
|
16
16
|
import { homedir } from 'node:os';
|
|
@@ -934,17 +934,32 @@ class CommitMessageGenerator {
|
|
|
934
934
|
*/
|
|
935
935
|
/** Promisified execFile for git commands. */
|
|
936
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
|
+
}
|
|
937
948
|
/**
|
|
938
949
|
* Find the longest-prefix-match root for a normalized path.
|
|
939
950
|
* Roots must be sorted longest-first for correct nested matching.
|
|
940
951
|
*
|
|
941
952
|
* @param roots - Root paths sorted longest-first.
|
|
942
953
|
* @param normalizedPath - Normalized absolute path (forward slashes).
|
|
954
|
+
* @param platform - The platform to check against (default: `process.platform`).
|
|
943
955
|
* @returns The matching root, or undefined.
|
|
944
956
|
*/
|
|
945
|
-
function findRootForPath(roots, normalizedPath) {
|
|
957
|
+
function findRootForPath(roots, normalizedPath, platform = process.platform) {
|
|
958
|
+
const comparePath = normalizePathCase(normalizedPath, platform);
|
|
946
959
|
for (const root of roots) {
|
|
947
|
-
|
|
960
|
+
const compareRoot = normalizePathCase(root, platform);
|
|
961
|
+
if (comparePath === compareRoot ||
|
|
962
|
+
comparePath.startsWith(compareRoot + '/')) {
|
|
948
963
|
return root;
|
|
949
964
|
}
|
|
950
965
|
}
|
|
@@ -1303,6 +1318,7 @@ class VcsManager {
|
|
|
1303
1318
|
debounceTimer;
|
|
1304
1319
|
commitInFlight = Promise.resolve();
|
|
1305
1320
|
started = false;
|
|
1321
|
+
isBaseline = false;
|
|
1306
1322
|
_lastPushTime = null;
|
|
1307
1323
|
constructor(rootPath, config, logger, commitMessageGenerator, remoteUrl, accessToken) {
|
|
1308
1324
|
this.rootPath = rootPath;
|
|
@@ -1323,11 +1339,47 @@ class VcsManager {
|
|
|
1323
1339
|
}
|
|
1324
1340
|
/**
|
|
1325
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.
|
|
1326
1343
|
*/
|
|
1327
|
-
start() {
|
|
1344
|
+
async start() {
|
|
1328
1345
|
this.started = true;
|
|
1329
1346
|
this.squashManager?.start();
|
|
1330
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
|
+
}
|
|
1331
1383
|
}
|
|
1332
1384
|
/**
|
|
1333
1385
|
* Add a file to the pending set and reset the debounce timer.
|
|
@@ -1442,6 +1494,9 @@ class VcsManager {
|
|
|
1442
1494
|
*/
|
|
1443
1495
|
buildTemplateMessage(fileCount) {
|
|
1444
1496
|
if (this.pendingReversions.length === 0) {
|
|
1497
|
+
if (this.isBaseline) {
|
|
1498
|
+
return `baseline: batch (${String(fileCount)} files)`;
|
|
1499
|
+
}
|
|
1445
1500
|
const timestamp = new Date().toISOString();
|
|
1446
1501
|
return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
|
|
1447
1502
|
}
|
|
@@ -1499,10 +1554,15 @@ class VcsManager {
|
|
|
1499
1554
|
cwd: this.rootPath,
|
|
1500
1555
|
});
|
|
1501
1556
|
// Build message after staging so getStagedDiff can see the changes
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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);
|
|
1505
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
|
+
}
|
|
1506
1566
|
await execFileAsync('git', ['commit', '-m', message], {
|
|
1507
1567
|
cwd: this.rootPath,
|
|
1508
1568
|
});
|
|
@@ -1579,8 +1639,9 @@ class VcsCoordinator {
|
|
|
1579
1639
|
if (!rootVcs)
|
|
1580
1640
|
continue;
|
|
1581
1641
|
const resolvedRoot = normalizeSlashes(resolve(globRoot(entry.path)));
|
|
1642
|
+
const rootKey = normalizePathCase(resolvedRoot);
|
|
1582
1643
|
// Deduplicate: skip if another glob already resolved to this root.
|
|
1583
|
-
if (this.managers.has(
|
|
1644
|
+
if (this.managers.has(rootKey))
|
|
1584
1645
|
continue;
|
|
1585
1646
|
const mergedConfig = {
|
|
1586
1647
|
...config.vcs,
|
|
@@ -1600,7 +1661,7 @@ class VcsCoordinator {
|
|
|
1600
1661
|
const remoteUrl = entry.vcs?.remote;
|
|
1601
1662
|
const accessToken = entry.vcs?.accessToken ?? config.vcs.defaultAccessToken;
|
|
1602
1663
|
const manager = new VcsManager(resolvedRoot, mergedConfig, logger.child({ vcsRoot: resolvedRoot }), generator, remoteUrl, accessToken);
|
|
1603
|
-
this.managers.set(
|
|
1664
|
+
this.managers.set(rootKey, manager);
|
|
1604
1665
|
this.roots.push(resolvedRoot);
|
|
1605
1666
|
}
|
|
1606
1667
|
// Sort roots longest-first so nested paths match before parents.
|
|
@@ -1609,10 +1670,12 @@ class VcsCoordinator {
|
|
|
1609
1670
|
/**
|
|
1610
1671
|
* Start all VcsManager instances.
|
|
1611
1672
|
*/
|
|
1612
|
-
start() {
|
|
1673
|
+
async start() {
|
|
1674
|
+
const startPromises = [];
|
|
1613
1675
|
this.managers.forEach((manager) => {
|
|
1614
|
-
manager.start();
|
|
1676
|
+
startPromises.push(manager.start());
|
|
1615
1677
|
});
|
|
1678
|
+
await Promise.all(startPromises);
|
|
1616
1679
|
this.logger.info({ rootCount: this.managers.size }, 'VcsCoordinator started');
|
|
1617
1680
|
}
|
|
1618
1681
|
/**
|
|
@@ -1654,7 +1717,7 @@ class VcsCoordinator {
|
|
|
1654
1717
|
* Get the VcsManager for a specific root path.
|
|
1655
1718
|
*/
|
|
1656
1719
|
getManager(root) {
|
|
1657
|
-
return this.managers.get(root);
|
|
1720
|
+
return this.managers.get(normalizePathCase(root));
|
|
1658
1721
|
}
|
|
1659
1722
|
/**
|
|
1660
1723
|
* Get all managers as [root, manager] pairs.
|
|
@@ -1671,7 +1734,7 @@ class VcsCoordinator {
|
|
|
1671
1734
|
*/
|
|
1672
1735
|
findManagerForPath(normalizedPath) {
|
|
1673
1736
|
const root = findRootForPath(this.roots, normalizedPath);
|
|
1674
|
-
return root ? this.managers.get(root) : undefined;
|
|
1737
|
+
return root ? this.managers.get(normalizePathCase(root)) : undefined;
|
|
1675
1738
|
}
|
|
1676
1739
|
}
|
|
1677
1740
|
|
|
@@ -2892,15 +2955,18 @@ function resolveWatchRootsForGlob(coordinator, glob) {
|
|
|
2892
2955
|
const normalizedGlob = normalizeSlashes(glob);
|
|
2893
2956
|
const roots = coordinator.getRoots();
|
|
2894
2957
|
const results = [];
|
|
2958
|
+
const compareGlob = normalizePathCase(normalizedGlob);
|
|
2895
2959
|
for (const root of roots) {
|
|
2896
|
-
|
|
2960
|
+
const compareRoot = normalizePathCase(root);
|
|
2961
|
+
if (compareGlob.startsWith(compareRoot + '/') ||
|
|
2962
|
+
compareGlob === compareRoot) {
|
|
2897
2963
|
// Glob is under this root
|
|
2898
2964
|
results.push({
|
|
2899
2965
|
root,
|
|
2900
2966
|
relativePath: normalizeSlashes(relative(root, normalizedGlob)),
|
|
2901
2967
|
});
|
|
2902
2968
|
}
|
|
2903
|
-
else if (
|
|
2969
|
+
else if (compareRoot.startsWith(compareGlob.replace(/[*?[\]{}]/g, ''))) {
|
|
2904
2970
|
// Root is under the glob's base path — include with root-relative glob
|
|
2905
2971
|
results.push({
|
|
2906
2972
|
root,
|
|
@@ -2986,6 +3052,22 @@ async function initRepo(rootPath) {
|
|
|
2986
3052
|
await execFileAsync('git', ['init'], { cwd: rootPath });
|
|
2987
3053
|
}
|
|
2988
3054
|
}
|
|
3055
|
+
/**
|
|
3056
|
+
* Configure local git identity for a repository.
|
|
3057
|
+
* Sets repo-level (not global) user.name and user.email.
|
|
3058
|
+
*
|
|
3059
|
+
* @param rootPath - Directory of the git repository.
|
|
3060
|
+
* @param name - Author name for commits.
|
|
3061
|
+
* @param email - Author email for commits.
|
|
3062
|
+
*/
|
|
3063
|
+
async function configureRepoIdentity(rootPath, name, email) {
|
|
3064
|
+
await execFileAsync('git', ['config', '--local', 'user.name', name], {
|
|
3065
|
+
cwd: rootPath,
|
|
3066
|
+
});
|
|
3067
|
+
await execFileAsync('git', ['config', '--local', 'user.email', email], {
|
|
3068
|
+
cwd: rootPath,
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
2989
3071
|
/**
|
|
2990
3072
|
* Ensure a .gitignore file exists at rootPath with all always-on entries.
|
|
2991
3073
|
* Creates the file if missing. Appends only entries not already present.
|
|
@@ -3200,7 +3282,15 @@ async function initVcs(config, logger) {
|
|
|
3200
3282
|
initializedRoots.add(root);
|
|
3201
3283
|
await initRepo(root);
|
|
3202
3284
|
await ensureGitignore(root);
|
|
3285
|
+
// Resolve author identity: per-path → root-level → schema defaults.
|
|
3286
|
+
const defaults = vcsAuthorConfigSchema.parse({});
|
|
3287
|
+
const pathAuthor = entry.vcs?.author;
|
|
3288
|
+
const rootAuthor = config.vcs.author;
|
|
3289
|
+
const authorName = pathAuthor?.name ?? rootAuthor?.name ?? defaults.name;
|
|
3290
|
+
const authorEmail = pathAuthor?.email ?? rootAuthor?.email ?? defaults.email;
|
|
3291
|
+
await configureRepoIdentity(root, authorName, authorEmail);
|
|
3203
3292
|
logger.info({ root }, 'VCS initialized for watch root');
|
|
3293
|
+
logger.debug({ root, authorName, authorEmail }, 'VCS author identity configured');
|
|
3204
3294
|
}
|
|
3205
3295
|
return true;
|
|
3206
3296
|
}
|
|
@@ -7694,7 +7784,7 @@ class JeevesWatcher {
|
|
|
7694
7784
|
this.watcher = watcher;
|
|
7695
7785
|
this.gitignoreFilter = gitignoreFilter;
|
|
7696
7786
|
this.server = await this.startApiServer();
|
|
7697
|
-
vcsCoordinator.start();
|
|
7787
|
+
await vcsCoordinator.start();
|
|
7698
7788
|
this.watcher.start();
|
|
7699
7789
|
this.startConfigWatch();
|
|
7700
7790
|
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-CaibsSrT.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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join, dirname, resolve, relative, extname, basename, isAbsolute, sep } from 'node:path';
|
|
2
2
|
import { createConfigQueryHandler as createConfigQueryHandler$1, createStatusHandler, createConfigApplyHandler, substituteEnvVars, DEFAULT_PORTS, getBindAddress, postJson, fetchJson, getPackageVersion } from '@karmaniverous/jeeves';
|
|
3
|
-
import { extractWatchPathStrings, inferenceRuleSchema, jeevesWatcherConfigSchema, normalizeWatchPaths, LOGGING_DEFAULTS, API_DEFAULTS, EMBEDDING_DEFAULTS, CONFIG_WATCH_DEFAULTS, WATCH_DEFAULTS, ROOT_DEFAULTS, INIT_CONFIG_TEMPLATE, DEFAULT_PORT as DEFAULT_PORT$1, PLUGIN_PACKAGE, SERVICE_PACKAGE, COMPONENT_NAME } from '@karmaniverous/jeeves-watcher-core';
|
|
3
|
+
import { extractWatchPathStrings, inferenceRuleSchema, jeevesWatcherConfigSchema, normalizeWatchPaths, vcsAuthorConfigSchema, LOGGING_DEFAULTS, API_DEFAULTS, EMBEDDING_DEFAULTS, CONFIG_WATCH_DEFAULTS, WATCH_DEFAULTS, ROOT_DEFAULTS, INIT_CONFIG_TEMPLATE, DEFAULT_PORT as DEFAULT_PORT$1, PLUGIN_PACKAGE, SERVICE_PACKAGE, COMPONENT_NAME } from '@karmaniverous/jeeves-watcher-core';
|
|
4
4
|
export { apiConfigSchema, configWatchConfigSchema, embeddingConfigSchema, inferenceRuleSchema, jeevesWatcherConfigSchema, loggingConfigSchema, vectorStoreConfigSchema, watchConfigSchema } from '@karmaniverous/jeeves-watcher-core';
|
|
5
5
|
import Fastify from 'fastify';
|
|
6
6
|
import { readdir, stat, readFile, writeFile, mkdir, appendFile, access, rm } from 'node:fs/promises';
|
|
@@ -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,
|
|
@@ -4624,6 +4642,22 @@ async function initRepo(rootPath) {
|
|
|
4624
4642
|
await execFileAsync('git', ['init'], { cwd: rootPath });
|
|
4625
4643
|
}
|
|
4626
4644
|
}
|
|
4645
|
+
/**
|
|
4646
|
+
* Configure local git identity for a repository.
|
|
4647
|
+
* Sets repo-level (not global) user.name and user.email.
|
|
4648
|
+
*
|
|
4649
|
+
* @param rootPath - Directory of the git repository.
|
|
4650
|
+
* @param name - Author name for commits.
|
|
4651
|
+
* @param email - Author email for commits.
|
|
4652
|
+
*/
|
|
4653
|
+
async function configureRepoIdentity(rootPath, name, email) {
|
|
4654
|
+
await execFileAsync('git', ['config', '--local', 'user.name', name], {
|
|
4655
|
+
cwd: rootPath,
|
|
4656
|
+
});
|
|
4657
|
+
await execFileAsync('git', ['config', '--local', 'user.email', email], {
|
|
4658
|
+
cwd: rootPath,
|
|
4659
|
+
});
|
|
4660
|
+
}
|
|
4627
4661
|
/**
|
|
4628
4662
|
* Ensure a .gitignore file exists at rootPath with all always-on entries.
|
|
4629
4663
|
* Creates the file if missing. Appends only entries not already present.
|
|
@@ -4780,6 +4814,7 @@ class VcsManager {
|
|
|
4780
4814
|
debounceTimer;
|
|
4781
4815
|
commitInFlight = Promise.resolve();
|
|
4782
4816
|
started = false;
|
|
4817
|
+
isBaseline = false;
|
|
4783
4818
|
_lastPushTime = null;
|
|
4784
4819
|
constructor(rootPath, config, logger, commitMessageGenerator, remoteUrl, accessToken) {
|
|
4785
4820
|
this.rootPath = rootPath;
|
|
@@ -4800,11 +4835,47 @@ class VcsManager {
|
|
|
4800
4835
|
}
|
|
4801
4836
|
/**
|
|
4802
4837
|
* Begin accepting file changes. Sets up the debounce mechanism.
|
|
4838
|
+
* Enqueues a baseline commit for pre-existing files if the repo has no commits.
|
|
4803
4839
|
*/
|
|
4804
|
-
start() {
|
|
4840
|
+
async start() {
|
|
4805
4841
|
this.started = true;
|
|
4806
4842
|
this.squashManager?.start();
|
|
4807
4843
|
this.logger.info({ root: this.rootPath }, 'VcsManager started');
|
|
4844
|
+
await this.enqueueBaselineIfNeeded();
|
|
4845
|
+
}
|
|
4846
|
+
/**
|
|
4847
|
+
* If the repo has no commits, enumerate untracked files and feed them
|
|
4848
|
+
* through fileChanged() to create a baseline commit.
|
|
4849
|
+
*/
|
|
4850
|
+
async enqueueBaselineIfNeeded() {
|
|
4851
|
+
try {
|
|
4852
|
+
await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
4853
|
+
cwd: this.rootPath,
|
|
4854
|
+
});
|
|
4855
|
+
// HEAD exists — repo already has commits
|
|
4856
|
+
return;
|
|
4857
|
+
}
|
|
4858
|
+
catch {
|
|
4859
|
+
// No commits yet — proceed with baseline
|
|
4860
|
+
}
|
|
4861
|
+
try {
|
|
4862
|
+
const { stdout } = await execFileAsync('git', ['ls-files', '--others', '--exclude-standard', '-z'], { cwd: this.rootPath });
|
|
4863
|
+
const files = stdout
|
|
4864
|
+
.split('\0')
|
|
4865
|
+
.map((line) => line.trim())
|
|
4866
|
+
.filter((line) => line.length > 0);
|
|
4867
|
+
if (files.length === 0)
|
|
4868
|
+
return;
|
|
4869
|
+
this.logger.info({ root: this.rootPath, fileCount: files.length }, 'Enqueuing baseline commit for pre-existing files');
|
|
4870
|
+
this.isBaseline = true;
|
|
4871
|
+
const absolutePaths = files.map((f) => this.rootPath + '/' + f);
|
|
4872
|
+
for (const filePath of absolutePaths) {
|
|
4873
|
+
this.fileChanged(filePath);
|
|
4874
|
+
}
|
|
4875
|
+
}
|
|
4876
|
+
catch (error) {
|
|
4877
|
+
this.logger.warn({ root: this.rootPath, err: normalizeError(error) }, 'Failed to enumerate untracked files for baseline commit');
|
|
4878
|
+
}
|
|
4808
4879
|
}
|
|
4809
4880
|
/**
|
|
4810
4881
|
* Add a file to the pending set and reset the debounce timer.
|
|
@@ -4919,6 +4990,9 @@ class VcsManager {
|
|
|
4919
4990
|
*/
|
|
4920
4991
|
buildTemplateMessage(fileCount) {
|
|
4921
4992
|
if (this.pendingReversions.length === 0) {
|
|
4993
|
+
if (this.isBaseline) {
|
|
4994
|
+
return `baseline: batch (${String(fileCount)} files)`;
|
|
4995
|
+
}
|
|
4922
4996
|
const timestamp = new Date().toISOString();
|
|
4923
4997
|
return `watcher: batch ${timestamp} (${String(fileCount)} files)`;
|
|
4924
4998
|
}
|
|
@@ -4976,10 +5050,15 @@ class VcsManager {
|
|
|
4976
5050
|
cwd: this.rootPath,
|
|
4977
5051
|
});
|
|
4978
5052
|
// Build message after staging so getStagedDiff can see the changes
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
5053
|
+
// Skip AI for baselines and retries — use template directly
|
|
5054
|
+
const message = this.isBaseline || attempt > 1
|
|
5055
|
+
? this.buildTemplateMessage(files.length)
|
|
5056
|
+
: await this.buildCommitMessage(files);
|
|
4982
5057
|
this.pendingReversions.length = 0;
|
|
5058
|
+
// Only clear baseline flag when all baseline files have been committed
|
|
5059
|
+
if (this.pending.size === 0) {
|
|
5060
|
+
this.isBaseline = false;
|
|
5061
|
+
}
|
|
4983
5062
|
await execFileAsync('git', ['commit', '-m', message], {
|
|
4984
5063
|
cwd: this.rootPath,
|
|
4985
5064
|
});
|
|
@@ -5056,8 +5135,9 @@ class VcsCoordinator {
|
|
|
5056
5135
|
if (!rootVcs)
|
|
5057
5136
|
continue;
|
|
5058
5137
|
const resolvedRoot = normalizeSlashes(resolve(globRoot(entry.path)));
|
|
5138
|
+
const rootKey = normalizePathCase(resolvedRoot);
|
|
5059
5139
|
// Deduplicate: skip if another glob already resolved to this root.
|
|
5060
|
-
if (this.managers.has(
|
|
5140
|
+
if (this.managers.has(rootKey))
|
|
5061
5141
|
continue;
|
|
5062
5142
|
const mergedConfig = {
|
|
5063
5143
|
...config.vcs,
|
|
@@ -5077,7 +5157,7 @@ class VcsCoordinator {
|
|
|
5077
5157
|
const remoteUrl = entry.vcs?.remote;
|
|
5078
5158
|
const accessToken = entry.vcs?.accessToken ?? config.vcs.defaultAccessToken;
|
|
5079
5159
|
const manager = new VcsManager(resolvedRoot, mergedConfig, logger.child({ vcsRoot: resolvedRoot }), generator, remoteUrl, accessToken);
|
|
5080
|
-
this.managers.set(
|
|
5160
|
+
this.managers.set(rootKey, manager);
|
|
5081
5161
|
this.roots.push(resolvedRoot);
|
|
5082
5162
|
}
|
|
5083
5163
|
// Sort roots longest-first so nested paths match before parents.
|
|
@@ -5086,10 +5166,12 @@ class VcsCoordinator {
|
|
|
5086
5166
|
/**
|
|
5087
5167
|
* Start all VcsManager instances.
|
|
5088
5168
|
*/
|
|
5089
|
-
start() {
|
|
5169
|
+
async start() {
|
|
5170
|
+
const startPromises = [];
|
|
5090
5171
|
this.managers.forEach((manager) => {
|
|
5091
|
-
manager.start();
|
|
5172
|
+
startPromises.push(manager.start());
|
|
5092
5173
|
});
|
|
5174
|
+
await Promise.all(startPromises);
|
|
5093
5175
|
this.logger.info({ rootCount: this.managers.size }, 'VcsCoordinator started');
|
|
5094
5176
|
}
|
|
5095
5177
|
/**
|
|
@@ -5131,7 +5213,7 @@ class VcsCoordinator {
|
|
|
5131
5213
|
* Get the VcsManager for a specific root path.
|
|
5132
5214
|
*/
|
|
5133
5215
|
getManager(root) {
|
|
5134
|
-
return this.managers.get(root);
|
|
5216
|
+
return this.managers.get(normalizePathCase(root));
|
|
5135
5217
|
}
|
|
5136
5218
|
/**
|
|
5137
5219
|
* Get all managers as [root, manager] pairs.
|
|
@@ -5148,7 +5230,7 @@ class VcsCoordinator {
|
|
|
5148
5230
|
*/
|
|
5149
5231
|
findManagerForPath(normalizedPath) {
|
|
5150
5232
|
const root = findRootForPath(this.roots, normalizedPath);
|
|
5151
|
-
return root ? this.managers.get(root) : undefined;
|
|
5233
|
+
return root ? this.managers.get(normalizePathCase(root)) : undefined;
|
|
5152
5234
|
}
|
|
5153
5235
|
}
|
|
5154
5236
|
|
|
@@ -5336,7 +5418,15 @@ async function initVcs(config, logger) {
|
|
|
5336
5418
|
initializedRoots.add(root);
|
|
5337
5419
|
await initRepo(root);
|
|
5338
5420
|
await ensureGitignore(root);
|
|
5421
|
+
// Resolve author identity: per-path → root-level → schema defaults.
|
|
5422
|
+
const defaults = vcsAuthorConfigSchema.parse({});
|
|
5423
|
+
const pathAuthor = entry.vcs?.author;
|
|
5424
|
+
const rootAuthor = config.vcs.author;
|
|
5425
|
+
const authorName = pathAuthor?.name ?? rootAuthor?.name ?? defaults.name;
|
|
5426
|
+
const authorEmail = pathAuthor?.email ?? rootAuthor?.email ?? defaults.email;
|
|
5427
|
+
await configureRepoIdentity(root, authorName, authorEmail);
|
|
5339
5428
|
logger.info({ root }, 'VCS initialized for watch root');
|
|
5429
|
+
logger.debug({ root, authorName, authorEmail }, 'VCS author identity configured');
|
|
5340
5430
|
}
|
|
5341
5431
|
return true;
|
|
5342
5432
|
}
|
|
@@ -8845,7 +8935,7 @@ class JeevesWatcher {
|
|
|
8845
8935
|
this.watcher = watcher;
|
|
8846
8936
|
this.gitignoreFilter = gitignoreFilter;
|
|
8847
8937
|
this.server = await this.startApiServer();
|
|
8848
|
-
vcsCoordinator.start();
|
|
8938
|
+
await vcsCoordinator.start();
|
|
8849
8939
|
this.watcher.start();
|
|
8850
8940
|
this.startConfigWatch();
|
|
8851
8941
|
logger.info('jeeves-watcher started');
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@commander-js/extra-typings": "^14.0.0",
|
|
11
11
|
"@karmaniverous/jeeves": "^0.5.12",
|
|
12
|
-
"@karmaniverous/jeeves-watcher-core": "^0.2.
|
|
12
|
+
"@karmaniverous/jeeves-watcher-core": "^0.2.2",
|
|
13
13
|
"@karmaniverous/jsonmap": "^2.1.1",
|
|
14
14
|
"@langchain/textsplitters": "*",
|
|
15
15
|
"@qdrant/js-client-rest": "*",
|
|
@@ -149,5 +149,5 @@
|
|
|
149
149
|
},
|
|
150
150
|
"type": "module",
|
|
151
151
|
"types": "dist/index.d.ts",
|
|
152
|
-
"version": "0.18.
|
|
152
|
+
"version": "0.18.5"
|
|
153
153
|
}
|