@karmaniverous/jeeves-meta 0.18.0-1 → 0.18.0-3
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-meta/index.js +58 -68
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { sleepAsync, fetchWithTimeout, createConfigQueryHandler,
|
|
2
|
+
import { sleepAsync, fetchWithTimeout, createConfigQueryHandler, createStatusHandler, getBindAddress, fetchJson, postJson, getServiceUrl, jeevesComponentDescriptorSchema, createConfigApplyHandler, createServiceCli } from '@karmaniverous/jeeves';
|
|
3
3
|
import { normalizePath, metaConfigSchema, getEndpoint, META_COMPONENT } from '@karmaniverous/jeeves-meta-core';
|
|
4
4
|
import fs, { unlinkSync, writeFileSync, existsSync, readFileSync, statSync, mkdirSync, readdirSync, watchFile } from 'node:fs';
|
|
5
5
|
import path, { join, dirname, relative, posix, sep, resolve } from 'node:path';
|
|
@@ -4018,24 +4018,53 @@ function registerConfigRoute(app, deps) {
|
|
|
4018
4018
|
}
|
|
4019
4019
|
|
|
4020
4020
|
/**
|
|
4021
|
-
* POST /config/apply — apply a config patch
|
|
4021
|
+
* POST /config/apply — apply a config patch to the runtime config file.
|
|
4022
4022
|
*
|
|
4023
|
-
*
|
|
4024
|
-
* `
|
|
4025
|
-
*
|
|
4026
|
-
*
|
|
4023
|
+
* Delegates the read/merge/validate/write cycle to the core SDK's
|
|
4024
|
+
* `createConfigApplyHandler` (bound to the runtime config path), which
|
|
4025
|
+
* merges into the raw file, validates without writing the schema's parsed
|
|
4026
|
+
* output, and keeps the file mode (0600 for a new file). This route adds
|
|
4027
|
+
* request validation, `restartRequired` reporting, and keeps the resolved
|
|
4028
|
+
* config (which may hold secrets) out of the response.
|
|
4027
4029
|
*
|
|
4028
4030
|
* @module routes/configApply
|
|
4029
4031
|
*/
|
|
4030
|
-
/**
|
|
4031
|
-
|
|
4032
|
+
/**
|
|
4033
|
+
* Snapshot the effective (schema-resolved) config currently on disk.
|
|
4034
|
+
*
|
|
4035
|
+
* @param configPath - Runtime config file path.
|
|
4036
|
+
* @returns The resolved config, the raw object when it does not validate,
|
|
4037
|
+
* or `{}` when the file is missing or unreadable.
|
|
4038
|
+
*/
|
|
4039
|
+
function readEffectiveConfig(configPath) {
|
|
4040
|
+
let raw;
|
|
4041
|
+
try {
|
|
4042
|
+
raw = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
4043
|
+
}
|
|
4044
|
+
catch {
|
|
4045
|
+
return {};
|
|
4046
|
+
}
|
|
4047
|
+
const parsed = serviceConfigSchema.safeParse(raw);
|
|
4048
|
+
if (parsed.success)
|
|
4049
|
+
return parsed.data;
|
|
4050
|
+
return typeof raw === 'object' && raw !== null && !Array.isArray(raw)
|
|
4051
|
+
? raw
|
|
4052
|
+
: {};
|
|
4053
|
+
}
|
|
4054
|
+
/**
|
|
4055
|
+
* Register the POST /config/apply route.
|
|
4056
|
+
*
|
|
4057
|
+
* @param app - Fastify instance.
|
|
4058
|
+
* @param applyConfig - Core config apply handler bound to the runtime path.
|
|
4059
|
+
* @param configPath - Runtime config file path (for restart detection).
|
|
4060
|
+
*/
|
|
4061
|
+
function registerConfigApplyRoute(app, applyConfig, configPath) {
|
|
4032
4062
|
app.post(getEndpoint('configApply').path, async (request, reply) => {
|
|
4033
|
-
if (!configPath) {
|
|
4063
|
+
if (!applyConfig || !configPath) {
|
|
4034
4064
|
return reply
|
|
4035
4065
|
.status(500)
|
|
4036
4066
|
.send({ error: 'No runtime config path available' });
|
|
4037
4067
|
}
|
|
4038
|
-
// Validate request body
|
|
4039
4068
|
const body = request.body;
|
|
4040
4069
|
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
4041
4070
|
return reply
|
|
@@ -4056,64 +4085,20 @@ function registerConfigApplyRoute(app, configPath) {
|
|
|
4056
4085
|
.status(400)
|
|
4057
4086
|
.send({ error: '`replace` must be a boolean if provided' });
|
|
4058
4087
|
}
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
}
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
(err instanceof Error && err.message.includes('JSON'))) {
|
|
4067
|
-
return reply.status(400).send({
|
|
4068
|
-
error: `Existing config file contains invalid JSON: ${err.message}`,
|
|
4069
|
-
});
|
|
4070
|
-
}
|
|
4071
|
-
// File missing — start from empty
|
|
4072
|
-
}
|
|
4073
|
-
// Merge or replace
|
|
4074
|
-
const merged = replace
|
|
4075
|
-
? { ...patch }
|
|
4076
|
-
: { ...existing, ...patch };
|
|
4077
|
-
// Validate against schema
|
|
4078
|
-
const parseResult = serviceConfigSchema.safeParse(merged);
|
|
4079
|
-
if (!parseResult.success) {
|
|
4080
|
-
return reply.status(400).send({
|
|
4081
|
-
error: 'Config validation failed',
|
|
4082
|
-
issues: parseResult.error.issues,
|
|
4083
|
-
});
|
|
4084
|
-
}
|
|
4085
|
-
const validatedConfig = parseResult.data;
|
|
4086
|
-
// Write atomically — temp file lands next to the runtime config file
|
|
4087
|
-
try {
|
|
4088
|
-
const json = JSON.stringify(validatedConfig, null, 2) + '\n';
|
|
4089
|
-
atomicWrite(configPath, json);
|
|
4090
|
-
}
|
|
4091
|
-
catch (err) {
|
|
4092
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4093
|
-
return reply
|
|
4094
|
-
.status(500)
|
|
4095
|
-
.send({ error: `Failed to write config: ${message}` });
|
|
4096
|
-
}
|
|
4097
|
-
// Compute whether any restart-required fields actually changed
|
|
4098
|
-
// (compares validated output against previous config, catching both
|
|
4099
|
-
// explicit patch keys and implicit changes via replace + schema defaults)
|
|
4100
|
-
const validatedRecord = validatedConfig;
|
|
4101
|
-
const restartRequired = RESTART_REQUIRED_FIELDS.some((field) => JSON.stringify(existing[field]) !==
|
|
4102
|
-
JSON.stringify(validatedRecord[field]));
|
|
4103
|
-
// Apply hot-reload callback
|
|
4104
|
-
try {
|
|
4105
|
-
applyHotReloadedConfig(validatedConfig);
|
|
4106
|
-
}
|
|
4107
|
-
catch (err) {
|
|
4108
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4109
|
-
return reply.status(200).send({
|
|
4110
|
-
applied: true,
|
|
4111
|
-
warning: `Config written but callback failed: ${message}`,
|
|
4112
|
-
restartRequired,
|
|
4113
|
-
});
|
|
4088
|
+
const previous = readEffectiveConfig(configPath);
|
|
4089
|
+
const result = await applyConfig({
|
|
4090
|
+
patch: patch,
|
|
4091
|
+
replace,
|
|
4092
|
+
});
|
|
4093
|
+
if (result.status !== 200) {
|
|
4094
|
+
return reply.status(result.status).send(result.body);
|
|
4114
4095
|
}
|
|
4096
|
+
// Never echo the resolved config: it may hold secrets (gatewayApiKey).
|
|
4097
|
+
const { config: next = {}, warning } = result.body;
|
|
4098
|
+
const restartRequired = RESTART_REQUIRED_FIELDS.some((field) => JSON.stringify(previous[field]) !== JSON.stringify(next[field]));
|
|
4115
4099
|
return reply.status(200).send({
|
|
4116
4100
|
applied: true,
|
|
4101
|
+
...(warning ? { warning } : {}),
|
|
4117
4102
|
restartRequired,
|
|
4118
4103
|
});
|
|
4119
4104
|
});
|
|
@@ -5070,7 +5055,7 @@ function registerRoutes(app, deps) {
|
|
|
5070
5055
|
registerSeedRoute(app, deps);
|
|
5071
5056
|
registerUnlockRoute(app, deps);
|
|
5072
5057
|
registerConfigRoute(app, deps);
|
|
5073
|
-
registerConfigApplyRoute(app, deps.configPath);
|
|
5058
|
+
registerConfigApplyRoute(app, deps.configApply, deps.configPath);
|
|
5074
5059
|
registerQueueRoutes(app, deps);
|
|
5075
5060
|
}
|
|
5076
5061
|
|
|
@@ -5254,8 +5239,10 @@ class HttpWatcherClient {
|
|
|
5254
5239
|
*
|
|
5255
5240
|
* @param config - Validated service configuration.
|
|
5256
5241
|
* @param configPath - Optional path to config file for hot-reload.
|
|
5242
|
+
* @param configApply - Core config apply handler bound to `configPath`,
|
|
5243
|
+
* backing POST /config/apply.
|
|
5257
5244
|
*/
|
|
5258
|
-
async function startService(config, configPath) {
|
|
5245
|
+
async function startService(config, configPath, configApply) {
|
|
5259
5246
|
const logger = createLogger({
|
|
5260
5247
|
level: config.logging.level,
|
|
5261
5248
|
file: config.logging.file,
|
|
@@ -5292,6 +5279,7 @@ async function startService(config, configPath) {
|
|
|
5292
5279
|
ready: false,
|
|
5293
5280
|
executor,
|
|
5294
5281
|
configPath,
|
|
5282
|
+
configApply,
|
|
5295
5283
|
};
|
|
5296
5284
|
registerConfigHotReloadRuntime({
|
|
5297
5285
|
config,
|
|
@@ -5620,7 +5608,9 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
|
|
|
5620
5608
|
},
|
|
5621
5609
|
run: async (configPath) => {
|
|
5622
5610
|
const config = loadServiceConfig(configPath);
|
|
5623
|
-
|
|
5611
|
+
// Route POST /config/apply through core's handler so the raw file is
|
|
5612
|
+
// merged (unknown keys kept, no defaults written, mode preserved).
|
|
5613
|
+
await startService(config, configPath, createConfigApplyHandler(metaDescriptor, configPath));
|
|
5624
5614
|
},
|
|
5625
5615
|
startCommand: (configPath) => [
|
|
5626
5616
|
'node',
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"url": "https://github.com/karmaniverous/jeeves-meta/issues"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@karmaniverous/jeeves": "0.6.0-
|
|
10
|
+
"@karmaniverous/jeeves": "0.6.0-9",
|
|
11
11
|
"@karmaniverous/jeeves-meta-core": "^0.2.2",
|
|
12
12
|
"commander": "^15",
|
|
13
13
|
"croner": "^10",
|
|
@@ -106,5 +106,5 @@
|
|
|
106
106
|
"typecheck": "tsc"
|
|
107
107
|
},
|
|
108
108
|
"type": "module",
|
|
109
|
-
"version": "0.18.0-
|
|
109
|
+
"version": "0.18.0-3"
|
|
110
110
|
}
|