@karmaniverous/jeeves-watcher 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/jeeves-watcher/index.js +3589 -3483
- package/dist/index.d.ts +4 -4
- package/dist/index.js +191 -85
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1637,8 +1637,10 @@ interface ApiServerOptions {
|
|
|
1637
1637
|
embeddingProvider: EmbeddingProvider;
|
|
1638
1638
|
/** The event queue. */
|
|
1639
1639
|
queue: EventQueue;
|
|
1640
|
-
/** The application configuration. */
|
|
1640
|
+
/** The application configuration (used as initial/fallback value). */
|
|
1641
1641
|
config: JeevesWatcherConfig;
|
|
1642
|
+
/** Config getter for live config access after hot-reload. */
|
|
1643
|
+
getConfig?: () => JeevesWatcherConfig;
|
|
1642
1644
|
/** The logger instance. */
|
|
1643
1645
|
logger: pino.Logger;
|
|
1644
1646
|
/** The issues manager. */
|
|
@@ -1734,9 +1736,7 @@ declare function startFromConfig(configPath?: string): Promise<JeevesWatcher>;
|
|
|
1734
1736
|
* Main application orchestrator. Wires components, manages lifecycle (start/stop/reload).
|
|
1735
1737
|
*/
|
|
1736
1738
|
|
|
1737
|
-
/**
|
|
1738
|
-
* Runtime options for {@link JeevesWatcher} that aren't serializable in config.
|
|
1739
|
-
*/
|
|
1739
|
+
/** Runtime options for {@link JeevesWatcher} that aren't serializable in config. */
|
|
1740
1740
|
interface JeevesWatcherRuntimeOptions {
|
|
1741
1741
|
/** Callback invoked on unrecoverable system error. If not set, throws. */
|
|
1742
1742
|
onFatalError?: (error: unknown) => void;
|
package/dist/index.js
CHANGED
|
@@ -1561,12 +1561,13 @@ function wrapHandler(fn, logger, label) {
|
|
|
1561
1561
|
function createConfigApplyHandler(deps) {
|
|
1562
1562
|
return wrapHandler(async (request, reply) => {
|
|
1563
1563
|
const { config: submittedConfig } = request.body;
|
|
1564
|
-
const
|
|
1564
|
+
const config = deps.getConfig();
|
|
1565
|
+
const { candidateRaw, errors } = mergeAndValidateConfig(config, submittedConfig);
|
|
1565
1566
|
if (errors.length > 0) {
|
|
1566
1567
|
return await reply.status(400).send({ valid: false, errors });
|
|
1567
1568
|
}
|
|
1568
1569
|
await writeFile(deps.configPath, JSON.stringify(candidateRaw, null, 2), 'utf-8');
|
|
1569
|
-
const reindexScope =
|
|
1570
|
+
const reindexScope = config.configWatch?.reindex ?? 'issues';
|
|
1570
1571
|
if (deps.triggerReindex) {
|
|
1571
1572
|
deps.triggerReindex(reindexScope);
|
|
1572
1573
|
}
|
|
@@ -1706,32 +1707,38 @@ function compileRules(rules) {
|
|
|
1706
1707
|
* @returns The handler function.
|
|
1707
1708
|
*/
|
|
1708
1709
|
function createConfigMatchHandler(options) {
|
|
1709
|
-
const {
|
|
1710
|
-
//
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
? picomatch(config.watch.ignored, { dot: true })
|
|
1716
|
-
: null;
|
|
1710
|
+
const { getConfig, logger } = options;
|
|
1711
|
+
// Cache compiled artifacts per config reference — recompute only on hot-reload.
|
|
1712
|
+
let cachedConfig;
|
|
1713
|
+
let compiledRules = [];
|
|
1714
|
+
let watchMatcher;
|
|
1715
|
+
let ignoreMatcher;
|
|
1717
1716
|
const handler = async (req, res) => {
|
|
1718
1717
|
const body = req.body;
|
|
1719
1718
|
if (!Array.isArray(body.paths)) {
|
|
1720
1719
|
res.code(400).send({ error: 'Request body must include "paths" array' });
|
|
1721
1720
|
return;
|
|
1722
1721
|
}
|
|
1722
|
+
const config = getConfig();
|
|
1723
|
+
if (config !== cachedConfig) {
|
|
1724
|
+
compiledRules = compileRules(config.inferenceRules ?? []);
|
|
1725
|
+
watchMatcher = picomatch(config.watch.paths, { dot: true });
|
|
1726
|
+
ignoreMatcher = config.watch.ignored?.length
|
|
1727
|
+
? picomatch(config.watch.ignored, { dot: true })
|
|
1728
|
+
: undefined;
|
|
1729
|
+
cachedConfig = config;
|
|
1730
|
+
}
|
|
1723
1731
|
const matches = body.paths.map((path) => {
|
|
1724
1732
|
const attrs = buildSyntheticAttributes(path);
|
|
1725
|
-
// Find matching rules
|
|
1726
1733
|
const matchingRules = [];
|
|
1727
1734
|
for (const compiled of compiledRules) {
|
|
1728
1735
|
if (compiled.validate(attrs)) {
|
|
1729
1736
|
matchingRules.push(compiled.rule.name);
|
|
1730
1737
|
}
|
|
1731
1738
|
}
|
|
1732
|
-
// Check watch scope: matches watch paths and not in ignored
|
|
1733
1739
|
const normalised = attrs.file.path;
|
|
1734
|
-
const watched = watchMatcher(normalised)
|
|
1740
|
+
const watched = (watchMatcher?.(normalised) ?? false) &&
|
|
1741
|
+
!(ignoreMatcher?.(normalised) ?? false);
|
|
1735
1742
|
return { rules: matchingRules, watched };
|
|
1736
1743
|
});
|
|
1737
1744
|
const response = { matches };
|
|
@@ -1910,7 +1917,7 @@ function resolveReferences(doc, resolveTypes) {
|
|
|
1910
1917
|
*/
|
|
1911
1918
|
function createConfigQueryHandler(deps) {
|
|
1912
1919
|
const coreHandler = createConfigQueryHandler$1(() => buildMergedDocument({
|
|
1913
|
-
config: deps.
|
|
1920
|
+
config: deps.getConfig(),
|
|
1914
1921
|
valuesManager: deps.valuesManager,
|
|
1915
1922
|
issuesManager: deps.issuesManager,
|
|
1916
1923
|
helperIntrospection: deps.helperIntrospection,
|
|
@@ -1971,7 +1978,7 @@ function createConfigReindexHandler(deps) {
|
|
|
1971
1978
|
});
|
|
1972
1979
|
}
|
|
1973
1980
|
const reindexDeps = {
|
|
1974
|
-
config: deps.
|
|
1981
|
+
config: deps.getConfig(),
|
|
1975
1982
|
processor: deps.processor,
|
|
1976
1983
|
logger: deps.logger,
|
|
1977
1984
|
reindexTracker: deps.reindexTracker,
|
|
@@ -3189,7 +3196,7 @@ function validateInferenceRuleSchemas(parsed) {
|
|
|
3189
3196
|
function createConfigValidateHandler(deps) {
|
|
3190
3197
|
return wrapHandler(async (request) => {
|
|
3191
3198
|
const { config: submittedConfig, testPaths } = request.body;
|
|
3192
|
-
const { candidateRaw, parsed, errors } = mergeAndValidateConfig(deps.
|
|
3199
|
+
const { candidateRaw, parsed, errors } = mergeAndValidateConfig(deps.getConfig(), submittedConfig);
|
|
3193
3200
|
if (errors.length > 0) {
|
|
3194
3201
|
return { valid: false, errors };
|
|
3195
3202
|
}
|
|
@@ -3309,13 +3316,14 @@ function buildFacetSchema(rules, mergeOptions) {
|
|
|
3309
3316
|
* @returns Fastify route handler (plain return, compatible with `withCache`).
|
|
3310
3317
|
*/
|
|
3311
3318
|
function createFacetsHandler(deps) {
|
|
3312
|
-
const {
|
|
3319
|
+
const { getConfig, valuesManager, configDir } = deps;
|
|
3313
3320
|
let cached;
|
|
3314
|
-
const mergeOptions = {
|
|
3315
|
-
globalSchemas: config.schemas,
|
|
3316
|
-
configDir,
|
|
3317
|
-
};
|
|
3318
3321
|
return () => {
|
|
3322
|
+
const config = getConfig();
|
|
3323
|
+
const mergeOptions = {
|
|
3324
|
+
globalSchemas: config.schemas,
|
|
3325
|
+
configDir,
|
|
3326
|
+
};
|
|
3319
3327
|
// Rebuild schema cache if rules changed
|
|
3320
3328
|
const currentHash = computeRulesHash(config.inferenceRules);
|
|
3321
3329
|
if (!cached || cached.rulesHash !== currentHash) {
|
|
@@ -3420,7 +3428,7 @@ function getValueType(value) {
|
|
|
3420
3428
|
*
|
|
3421
3429
|
* Unknown properties are allowed.
|
|
3422
3430
|
*/
|
|
3423
|
-
function validateMetadataPayload(config, path, metadata) {
|
|
3431
|
+
function validateMetadataPayload(config, path, metadata, configDir) {
|
|
3424
3432
|
const compiled = compileRules(config.inferenceRules ?? []);
|
|
3425
3433
|
const attrs = buildSyntheticAttributes(path);
|
|
3426
3434
|
const matched = compiled.filter((r) => r.validate(attrs));
|
|
@@ -3431,6 +3439,7 @@ function validateMetadataPayload(config, path, metadata) {
|
|
|
3431
3439
|
}
|
|
3432
3440
|
const merged = mergeSchemas(schemaRefs, {
|
|
3433
3441
|
globalSchemas: config.schemas ?? {},
|
|
3442
|
+
configDir,
|
|
3434
3443
|
});
|
|
3435
3444
|
const details = [];
|
|
3436
3445
|
for (const [key, value] of Object.entries(metadata)) {
|
|
@@ -3451,6 +3460,7 @@ function validateMetadataPayload(config, path, metadata) {
|
|
|
3451
3460
|
return false;
|
|
3452
3461
|
const ruleSchema = mergeSchemas(m.rule.schema, {
|
|
3453
3462
|
globalSchemas: config.schemas ?? {},
|
|
3463
|
+
configDir,
|
|
3454
3464
|
});
|
|
3455
3465
|
return (Object.hasOwn(ruleSchema.properties, key) &&
|
|
3456
3466
|
ruleSchema.properties[key].type === expectedType);
|
|
@@ -3490,7 +3500,7 @@ function validateMetadataPayload(config, path, metadata) {
|
|
|
3490
3500
|
function createMetadataHandler(deps) {
|
|
3491
3501
|
return wrapHandler(async (request, reply) => {
|
|
3492
3502
|
const { path, metadata } = request.body;
|
|
3493
|
-
const validation = validateMetadataPayload(deps.
|
|
3503
|
+
const validation = validateMetadataPayload(deps.getConfig(), path, metadata, deps.configDir);
|
|
3494
3504
|
if (!validation.ok) {
|
|
3495
3505
|
return reply
|
|
3496
3506
|
.code(400)
|
|
@@ -3594,13 +3604,14 @@ function createRebuildMetadataHandler(deps) {
|
|
|
3594
3604
|
* @returns Fastify route handler.
|
|
3595
3605
|
*/
|
|
3596
3606
|
function createRenderHandler(deps) {
|
|
3597
|
-
const { processor,
|
|
3607
|
+
const { processor, getWatch, logger } = deps;
|
|
3598
3608
|
return async (request, reply) => {
|
|
3599
3609
|
const { path: filePath } = request.body;
|
|
3600
3610
|
if (!filePath || typeof filePath !== 'string') {
|
|
3601
3611
|
return reply.status(400).send({ error: 'Missing required field: path' });
|
|
3602
3612
|
}
|
|
3603
|
-
// Validate path is within watched scope
|
|
3613
|
+
// Validate path is within watched scope (live config)
|
|
3614
|
+
const watch = getWatch();
|
|
3604
3615
|
if (!isPathWatched(filePath, watch.paths, watch.ignored)) {
|
|
3605
3616
|
return reply.status(403).send({ error: 'Path is outside watched scope' });
|
|
3606
3617
|
}
|
|
@@ -3786,8 +3797,9 @@ function createSearchHandler(deps) {
|
|
|
3786
3797
|
return wrapHandler(async (request) => {
|
|
3787
3798
|
const { query, limit = 10, offset, filter } = request.body;
|
|
3788
3799
|
const vectors = await deps.embeddingProvider.embed([query]);
|
|
3789
|
-
|
|
3790
|
-
|
|
3800
|
+
const hybridConfig = deps.getHybridConfig?.();
|
|
3801
|
+
if (hybridConfig?.enabled) {
|
|
3802
|
+
return deps.vectorStore.hybridSearch(vectors[0], query, limit, hybridConfig.textWeight, filter);
|
|
3791
3803
|
}
|
|
3792
3804
|
return deps.vectorStore.search(vectors[0], limit, filter, offset);
|
|
3793
3805
|
}, deps.logger, 'Search');
|
|
@@ -3810,7 +3822,7 @@ function createStatusHandler(deps) {
|
|
|
3810
3822
|
version: deps.version,
|
|
3811
3823
|
uptime: process.uptime(),
|
|
3812
3824
|
collection: {
|
|
3813
|
-
name: deps.
|
|
3825
|
+
name: deps.getCollectionName(),
|
|
3814
3826
|
pointCount: collectionInfo.pointCount,
|
|
3815
3827
|
dimensions: collectionInfo.dimensions,
|
|
3816
3828
|
},
|
|
@@ -3857,7 +3869,7 @@ function createWalkHandler(deps) {
|
|
|
3857
3869
|
const normGlobs = globs.map((g) => normalizeSlashes(g));
|
|
3858
3870
|
const matchGlobs = picomatch(normGlobs, { dot: true, nocase: true });
|
|
3859
3871
|
const paths = watchedFiles.filter((f) => matchGlobs(normalizeSlashes(f)));
|
|
3860
|
-
const scannedRoots = getWatchRootBases(deps.
|
|
3872
|
+
const scannedRoots = getWatchRootBases(deps.getWatchPaths());
|
|
3861
3873
|
return {
|
|
3862
3874
|
paths,
|
|
3863
3875
|
matchedCount: paths.length,
|
|
@@ -4118,6 +4130,59 @@ async function introspectHelpers(config, configDir) {
|
|
|
4118
4130
|
function getConfigDir(configPath) {
|
|
4119
4131
|
return configPath ? dirname(configPath) : '.';
|
|
4120
4132
|
}
|
|
4133
|
+
/**
|
|
4134
|
+
* Tear down and rebuild the filesystem watcher with new config.
|
|
4135
|
+
* Falls back to the old watcher if the new one fails to start.
|
|
4136
|
+
*/
|
|
4137
|
+
async function rebuildWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, oldState, initialScanTracker, contentHashCache) {
|
|
4138
|
+
try {
|
|
4139
|
+
await oldState.watcher.stop();
|
|
4140
|
+
const { watcher: newWatcher, gitignoreFilter: newGitignoreFilter } = createWatcher(newConfig, factories, queue, processor, logger, runtimeOptions, initialScanTracker, contentHashCache);
|
|
4141
|
+
newWatcher.start();
|
|
4142
|
+
logger.info('Filesystem watcher rebuilt successfully');
|
|
4143
|
+
return { watcher: newWatcher, gitignoreFilter: newGitignoreFilter };
|
|
4144
|
+
}
|
|
4145
|
+
catch (error) {
|
|
4146
|
+
logger.error({ err: normalizeError(error) }, 'Failed to rebuild watcher, restoring previous');
|
|
4147
|
+
try {
|
|
4148
|
+
oldState.watcher.start();
|
|
4149
|
+
}
|
|
4150
|
+
catch (restartError) {
|
|
4151
|
+
logger.error({ err: normalizeError(restartError) }, 'Failed to restart previous watcher');
|
|
4152
|
+
}
|
|
4153
|
+
return oldState;
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
/**
|
|
4157
|
+
* Check whether watch-relevant config fields changed between old and new config.
|
|
4158
|
+
*/
|
|
4159
|
+
function watchConfigChanged(oldConfig, newConfig) {
|
|
4160
|
+
return (JSON.stringify(oldConfig.watch.paths) !==
|
|
4161
|
+
JSON.stringify(newConfig.watch.paths) ||
|
|
4162
|
+
JSON.stringify(oldConfig.watch.ignored) !==
|
|
4163
|
+
JSON.stringify(newConfig.watch.ignored) ||
|
|
4164
|
+
JSON.stringify(oldConfig.watch.moveDetection) !==
|
|
4165
|
+
JSON.stringify(newConfig.watch.moveDetection) ||
|
|
4166
|
+
(oldConfig.watch.respectGitignore ?? true) !==
|
|
4167
|
+
(newConfig.watch.respectGitignore ?? true));
|
|
4168
|
+
}
|
|
4169
|
+
/**
|
|
4170
|
+
* Resolve package version from nearest package.json.
|
|
4171
|
+
*/
|
|
4172
|
+
function resolveVersion(referenceUrl) {
|
|
4173
|
+
try {
|
|
4174
|
+
const pkgDir = packageDirectorySync({
|
|
4175
|
+
cwd: dirname(fileURLToPath(referenceUrl)),
|
|
4176
|
+
});
|
|
4177
|
+
const pkg = pkgDir
|
|
4178
|
+
? JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'))
|
|
4179
|
+
: undefined;
|
|
4180
|
+
return pkg?.version ?? 'unknown';
|
|
4181
|
+
}
|
|
4182
|
+
catch {
|
|
4183
|
+
return 'unknown';
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4121
4186
|
|
|
4122
4187
|
/**
|
|
4123
4188
|
* @module rules/virtualRules
|
|
@@ -4223,8 +4288,9 @@ function extractMatchGlobs(rules) {
|
|
|
4223
4288
|
* @returns A zero-argument callback suitable for passing to rule registration handlers.
|
|
4224
4289
|
*/
|
|
4225
4290
|
function createOnRulesChanged(deps) {
|
|
4226
|
-
const {
|
|
4291
|
+
const { getConfig, configPath, processor, logger, virtualRuleStore, reindexTracker, valuesManager, issuesManager, gitignoreFilter, vectorStore, } = deps;
|
|
4227
4292
|
return () => {
|
|
4293
|
+
const config = getConfig();
|
|
4228
4294
|
const configRules = config.inferenceRules ?? [];
|
|
4229
4295
|
const virtualRulesBySource = virtualRuleStore.getAll();
|
|
4230
4296
|
const allVirtualRules = Object.values(virtualRulesBySource).flat();
|
|
@@ -4381,6 +4447,7 @@ class InitialScanTracker {
|
|
|
4381
4447
|
*/
|
|
4382
4448
|
function createApiServer(options) {
|
|
4383
4449
|
const { processor, vectorStore, embeddingProvider, logger, config, issuesManager, valuesManager, configPath, helperIntrospection, virtualRuleStore, gitignoreFilter, version, initialScanTracker, } = options;
|
|
4450
|
+
const getConfig = options.getConfig ?? (() => config);
|
|
4384
4451
|
const reindexTracker = options.reindexTracker ?? new ReindexTracker();
|
|
4385
4452
|
const app = Fastify({ logger: false });
|
|
4386
4453
|
const triggerReindex = (scope) => {
|
|
@@ -4389,7 +4456,7 @@ function createApiServer(options) {
|
|
|
4389
4456
|
return;
|
|
4390
4457
|
}
|
|
4391
4458
|
void executeReindex({
|
|
4392
|
-
config,
|
|
4459
|
+
config: getConfig(),
|
|
4393
4460
|
processor,
|
|
4394
4461
|
logger,
|
|
4395
4462
|
reindexTracker,
|
|
@@ -4402,30 +4469,33 @@ function createApiServer(options) {
|
|
|
4402
4469
|
const cacheTtlMs = config.api?.cacheTtlMs ?? 30000;
|
|
4403
4470
|
app.get('/status', withCache(cacheTtlMs, createStatusHandler({
|
|
4404
4471
|
vectorStore,
|
|
4405
|
-
|
|
4472
|
+
getCollectionName: () => getConfig().vectorStore.collectionName,
|
|
4406
4473
|
reindexTracker,
|
|
4407
4474
|
version: version ?? 'unknown',
|
|
4408
4475
|
initialScanTracker,
|
|
4409
4476
|
})));
|
|
4410
|
-
app.post('/metadata', createMetadataHandler({
|
|
4411
|
-
|
|
4477
|
+
app.post('/metadata', createMetadataHandler({
|
|
4478
|
+
processor,
|
|
4479
|
+
getConfig,
|
|
4480
|
+
logger,
|
|
4481
|
+
configDir: dirname(configPath),
|
|
4482
|
+
}));
|
|
4483
|
+
app.post('/render', withCache(cacheTtlMs, createRenderHandler({
|
|
4484
|
+
processor,
|
|
4485
|
+
getWatch: () => getConfig().watch,
|
|
4486
|
+
logger,
|
|
4487
|
+
})));
|
|
4412
4488
|
app.get('/search/facets', createFacetsHandler({
|
|
4413
|
-
|
|
4489
|
+
getConfig,
|
|
4414
4490
|
valuesManager,
|
|
4415
4491
|
configDir: dirname(configPath),
|
|
4416
4492
|
}));
|
|
4417
|
-
const hybridConfig = config.search?.hybrid
|
|
4418
|
-
? {
|
|
4419
|
-
enabled: config.search.hybrid.enabled,
|
|
4420
|
-
textWeight: config.search.hybrid.textWeight,
|
|
4421
|
-
}
|
|
4422
|
-
: undefined;
|
|
4423
4493
|
app.post('/scan', createScanHandler({
|
|
4424
4494
|
vectorStore,
|
|
4425
4495
|
logger,
|
|
4426
4496
|
}));
|
|
4427
4497
|
app.post('/walk', createWalkHandler({
|
|
4428
|
-
|
|
4498
|
+
getWatchPaths: () => getConfig().watch.paths,
|
|
4429
4499
|
fileSystemWatcher: options.fileSystemWatcher,
|
|
4430
4500
|
logger,
|
|
4431
4501
|
}));
|
|
@@ -4433,7 +4503,12 @@ function createApiServer(options) {
|
|
|
4433
4503
|
embeddingProvider,
|
|
4434
4504
|
vectorStore,
|
|
4435
4505
|
logger,
|
|
4436
|
-
|
|
4506
|
+
getHybridConfig: () => {
|
|
4507
|
+
const hybrid = getConfig().search?.hybrid;
|
|
4508
|
+
return hybrid
|
|
4509
|
+
? { enabled: hybrid.enabled, textWeight: hybrid.textWeight }
|
|
4510
|
+
: undefined;
|
|
4511
|
+
},
|
|
4437
4512
|
}));
|
|
4438
4513
|
app.post('/rebuild-metadata', createRebuildMetadataHandler({
|
|
4439
4514
|
enrichmentStore: options.enrichmentStore,
|
|
@@ -4441,7 +4516,7 @@ function createApiServer(options) {
|
|
|
4441
4516
|
logger,
|
|
4442
4517
|
}));
|
|
4443
4518
|
app.post('/reindex', createConfigReindexHandler({
|
|
4444
|
-
|
|
4519
|
+
getConfig,
|
|
4445
4520
|
processor,
|
|
4446
4521
|
logger,
|
|
4447
4522
|
reindexTracker,
|
|
@@ -4452,9 +4527,9 @@ function createApiServer(options) {
|
|
|
4452
4527
|
}));
|
|
4453
4528
|
app.get('/issues', withCache(cacheTtlMs, createIssuesHandler({ issuesManager })));
|
|
4454
4529
|
app.get('/config/schema', withCache(cacheTtlMs, createConfigSchemaHandler()));
|
|
4455
|
-
app.post('/config/match', createConfigMatchHandler({
|
|
4530
|
+
app.post('/config/match', createConfigMatchHandler({ getConfig, logger }));
|
|
4456
4531
|
app.get('/config', withCache(cacheTtlMs, createConfigQueryHandler({
|
|
4457
|
-
|
|
4532
|
+
getConfig,
|
|
4458
4533
|
valuesManager,
|
|
4459
4534
|
issuesManager,
|
|
4460
4535
|
logger,
|
|
@@ -4464,12 +4539,12 @@ function createApiServer(options) {
|
|
|
4464
4539
|
: undefined,
|
|
4465
4540
|
})));
|
|
4466
4541
|
app.post('/config/validate', createConfigValidateHandler({
|
|
4467
|
-
|
|
4542
|
+
getConfig,
|
|
4468
4543
|
logger,
|
|
4469
4544
|
configDir: dirname(configPath),
|
|
4470
4545
|
}));
|
|
4471
4546
|
app.post('/config/apply', createConfigApplyHandler({
|
|
4472
|
-
|
|
4547
|
+
getConfig,
|
|
4473
4548
|
configPath,
|
|
4474
4549
|
reindexTracker,
|
|
4475
4550
|
logger,
|
|
@@ -4478,7 +4553,7 @@ function createApiServer(options) {
|
|
|
4478
4553
|
// Virtual rules and points deletion routes
|
|
4479
4554
|
if (virtualRuleStore) {
|
|
4480
4555
|
const onRulesChanged = createOnRulesChanged({
|
|
4481
|
-
|
|
4556
|
+
getConfig,
|
|
4482
4557
|
configPath,
|
|
4483
4558
|
processor,
|
|
4484
4559
|
logger,
|
|
@@ -4914,6 +4989,50 @@ class ValuesManager extends JsonFileStore {
|
|
|
4914
4989
|
}
|
|
4915
4990
|
}
|
|
4916
4991
|
|
|
4992
|
+
/**
|
|
4993
|
+
* @module app/configReload
|
|
4994
|
+
* Config-reload orchestration extracted from JeevesWatcher to follow SRP.
|
|
4995
|
+
*/
|
|
4996
|
+
/**
|
|
4997
|
+
* Reload config, update rules/templates, optionally rebuild watcher, and trigger reindex.
|
|
4998
|
+
* Mutates `state.config`, `state.watcher`, and `state.gitignoreFilter` in place.
|
|
4999
|
+
*/
|
|
5000
|
+
async function reloadConfig(state, deps) {
|
|
5001
|
+
const { logger, processor, configPath } = deps;
|
|
5002
|
+
logger.info({ configPath }, 'Config change detected, reloading...');
|
|
5003
|
+
try {
|
|
5004
|
+
const oldConfig = state.config;
|
|
5005
|
+
const newConfig = await deps.factories.loadConfig(configPath);
|
|
5006
|
+
state.config = newConfig;
|
|
5007
|
+
const compiledRules = deps.factories.compileRules(newConfig.inferenceRules ?? []);
|
|
5008
|
+
const reloadConfigDir = getConfigDir(configPath);
|
|
5009
|
+
const { templateEngine: newTemplateEngine, customMapLib: newCustomMapLib } = await buildTemplateEngineAndCustomMapLib(newConfig, reloadConfigDir);
|
|
5010
|
+
processor.updateRules(compiledRules, newTemplateEngine, newCustomMapLib);
|
|
5011
|
+
logger.info({ configPath, rules: compiledRules.length }, 'Config reloaded');
|
|
5012
|
+
// Rebuild filesystem watcher if watch config changed
|
|
5013
|
+
if (watchConfigChanged(oldConfig, newConfig)) {
|
|
5014
|
+
logger.info('Watch config changed, rebuilding filesystem watcher...');
|
|
5015
|
+
const newState = await rebuildWatcher(newConfig, deps.factories, deps.queue, processor, logger, deps.runtimeOptions, { watcher: state.watcher, gitignoreFilter: state.gitignoreFilter }, deps.initialScanTracker, deps.contentHashCache);
|
|
5016
|
+
state.watcher = newState.watcher;
|
|
5017
|
+
state.gitignoreFilter = newState.gitignoreFilter;
|
|
5018
|
+
}
|
|
5019
|
+
// Trigger reindex based on configWatch.reindex setting
|
|
5020
|
+
const reindexScope = newConfig.configWatch?.reindex ?? 'issues';
|
|
5021
|
+
logger.info({ scope: reindexScope }, `Config watch triggering ${reindexScope} reindex`);
|
|
5022
|
+
await executeReindex({
|
|
5023
|
+
config: newConfig,
|
|
5024
|
+
processor,
|
|
5025
|
+
logger,
|
|
5026
|
+
valuesManager: deps.valuesManager,
|
|
5027
|
+
issuesManager: deps.issuesManager,
|
|
5028
|
+
gitignoreFilter: state.gitignoreFilter,
|
|
5029
|
+
}, reindexScope);
|
|
5030
|
+
}
|
|
5031
|
+
catch (error) {
|
|
5032
|
+
logger.error({ err: normalizeError(error) }, 'Failed to reload config');
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
|
|
4917
5036
|
/**
|
|
4918
5037
|
* @module app/configWatcher
|
|
4919
5038
|
* Watches the config file for changes and triggers debounced reload. Isolated I/O wrapper around chokidar.
|
|
@@ -7383,18 +7502,7 @@ class JeevesWatcher {
|
|
|
7383
7502
|
this.runtimeOptions = runtimeOptions;
|
|
7384
7503
|
this.virtualRuleStore = new VirtualRuleStore();
|
|
7385
7504
|
this.initialScanTracker = new InitialScanTracker();
|
|
7386
|
-
|
|
7387
|
-
const pkgDir = packageDirectorySync({
|
|
7388
|
-
cwd: dirname(fileURLToPath(import.meta.url)),
|
|
7389
|
-
});
|
|
7390
|
-
const pkg = pkgDir
|
|
7391
|
-
? JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'))
|
|
7392
|
-
: undefined;
|
|
7393
|
-
this.version = pkg?.version ?? 'unknown';
|
|
7394
|
-
}
|
|
7395
|
-
catch {
|
|
7396
|
-
this.version = 'unknown';
|
|
7397
|
-
}
|
|
7505
|
+
this.version = resolveVersion(import.meta.url);
|
|
7398
7506
|
}
|
|
7399
7507
|
/**
|
|
7400
7508
|
* Start the watcher, API server, and all components.
|
|
@@ -7468,6 +7576,7 @@ class JeevesWatcher {
|
|
|
7468
7576
|
this.logger?.warn({ timeoutMs: timeout }, 'Queue drain timeout hit, forcing shutdown');
|
|
7469
7577
|
}
|
|
7470
7578
|
}
|
|
7579
|
+
this.enrichmentStore?.close();
|
|
7471
7580
|
if (this.server) {
|
|
7472
7581
|
await this.server.close();
|
|
7473
7582
|
}
|
|
@@ -7480,6 +7589,7 @@ class JeevesWatcher {
|
|
|
7480
7589
|
embeddingProvider: this.embeddingProvider,
|
|
7481
7590
|
queue: this.queue,
|
|
7482
7591
|
config: this.config,
|
|
7592
|
+
getConfig: () => this.config,
|
|
7483
7593
|
logger: this.logger,
|
|
7484
7594
|
issuesManager: this.issuesManager,
|
|
7485
7595
|
valuesManager: this.valuesManager,
|
|
@@ -7529,30 +7639,26 @@ class JeevesWatcher {
|
|
|
7529
7639
|
const processor = this.processor;
|
|
7530
7640
|
if (!logger || !processor || !this.configPath)
|
|
7531
7641
|
return;
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
this.
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
}
|
|
7553
|
-
catch (error) {
|
|
7554
|
-
logger.error({ err: normalizeError(error) }, 'Failed to reload config');
|
|
7555
|
-
}
|
|
7642
|
+
const state = {
|
|
7643
|
+
config: this.config,
|
|
7644
|
+
watcher: this.watcher,
|
|
7645
|
+
gitignoreFilter: this.gitignoreFilter,
|
|
7646
|
+
};
|
|
7647
|
+
await reloadConfig(state, {
|
|
7648
|
+
configPath: this.configPath,
|
|
7649
|
+
factories: this.factories,
|
|
7650
|
+
queue: this.queue,
|
|
7651
|
+
processor,
|
|
7652
|
+
logger,
|
|
7653
|
+
runtimeOptions: this.runtimeOptions,
|
|
7654
|
+
initialScanTracker: this.initialScanTracker,
|
|
7655
|
+
contentHashCache: this.contentHashCache,
|
|
7656
|
+
valuesManager: this.valuesManager,
|
|
7657
|
+
issuesManager: this.issuesManager,
|
|
7658
|
+
});
|
|
7659
|
+
this.config = state.config;
|
|
7660
|
+
this.watcher = state.watcher;
|
|
7661
|
+
this.gitignoreFilter = state.gitignoreFilter;
|
|
7556
7662
|
}
|
|
7557
7663
|
}
|
|
7558
7664
|
|
package/package.json
CHANGED