@hexclave/shared-backend 1.0.36 → 1.0.37
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/config-agent.d.ts +22 -1
- package/dist/config-agent.d.ts.map +1 -1
- package/dist/config-agent.js +54 -2
- package/dist/config-agent.js.map +1 -1
- package/dist/config-file.d.ts +15 -0
- package/dist/config-file.d.ts.map +1 -0
- package/dist/config-file.js +90 -0
- package/dist/config-file.js.map +1 -0
- package/dist/config-updater.d.ts +7 -0
- package/dist/config-updater.d.ts.map +1 -0
- package/dist/config-updater.js +330 -0
- package/dist/config-updater.js.map +1 -0
- package/dist/esm/config-agent.d.ts +22 -1
- package/dist/esm/config-agent.d.ts.map +1 -1
- package/dist/esm/config-agent.js +51 -3
- package/dist/esm/config-agent.js.map +1 -1
- package/dist/esm/config-file.d.ts +15 -0
- package/dist/esm/config-file.d.ts.map +1 -0
- package/dist/esm/config-file.js +82 -0
- package/dist/esm/config-file.js.map +1 -0
- package/dist/esm/config-updater.d.ts +7 -0
- package/dist/esm/config-updater.d.ts.map +1 -0
- package/dist/esm/config-updater.js +327 -0
- package/dist/esm/config-updater.js.map +1 -0
- package/dist/esm/index.d.ts +2 -16
- package/dist/esm/index.js +3 -446
- package/dist/index.d.ts +3 -16
- package/dist/index.js +15 -455
- package/package.json +2 -2
- package/dist/esm/index.d.ts.map +0 -1
- package/dist/esm/index.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/esm/index.js
CHANGED
|
@@ -1,448 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "fs";
|
|
3
|
-
import { showOnboardingHexclaveConfigValue } from "@hexclave/shared/dist/config-authoring";
|
|
4
|
-
import { detectImportPackageFromDir, parseHexclaveConfigFileContent, renderConfigFileContent } from "@hexclave/shared/dist/config-rendering";
|
|
5
|
-
import { isValidConfig, normalize, override } from "@hexclave/shared/dist/config/format";
|
|
6
|
-
import { captureError } from "@hexclave/shared/dist/utils/errors";
|
|
7
|
-
import { createHash } from "crypto";
|
|
8
|
-
import { createJiti } from "jiti";
|
|
9
|
-
import { ClaudeAgentFailureError, ClaudeAgentTimeoutError, getToolWriteTargetPath, isPathInsideDir, runHeadlessClaudeAgent } from "./config-agent.js";
|
|
1
|
+
export * from "./config-file.js"
|
|
10
2
|
|
|
11
|
-
|
|
12
|
-
const jiti = createJiti(import.meta.url, { moduleCache: false });
|
|
13
|
-
const LOG_PREFIX = "[Stack config updater]";
|
|
14
|
-
const DEFAULT_AGENT_TIMEOUT_MS = 12e4;
|
|
15
|
-
const CONFIG_UPDATE_LOG_PATH_LIMIT = 40;
|
|
16
|
-
const AGENT_OUTPUT_LOG_MAX_LENGTH = 2e4;
|
|
17
|
-
function formatConfigUpdaterErrorForLog(error) {
|
|
18
|
-
if (error instanceof Error) return {
|
|
19
|
-
errorName: error.name,
|
|
20
|
-
errorMessage: error.message,
|
|
21
|
-
errorStack: error.stack
|
|
22
|
-
};
|
|
23
|
-
return { errorMessage: String(error) };
|
|
24
|
-
}
|
|
25
|
-
function configUpdatePathDetailsForLog(changes) {
|
|
26
|
-
const paths = changes.map(({ path: configPath }) => configPath).sort();
|
|
27
|
-
return {
|
|
28
|
-
configUpdatePathCount: paths.length,
|
|
29
|
-
configUpdatePaths: paths.slice(0, CONFIG_UPDATE_LOG_PATH_LIMIT),
|
|
30
|
-
configUpdatePathsTruncated: paths.length > CONFIG_UPDATE_LOG_PATH_LIMIT
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
function appendBoundedAgentOutput(current, chunk) {
|
|
34
|
-
const next = `${current}${chunk}`;
|
|
35
|
-
if (next.length <= AGENT_OUTPUT_LOG_MAX_LENGTH) return next;
|
|
36
|
-
return next.slice(next.length - AGENT_OUTPUT_LOG_MAX_LENGTH);
|
|
37
|
-
}
|
|
38
|
-
function stringifyAgentMessageForLog(message) {
|
|
39
|
-
try {
|
|
40
|
-
return `${JSON.stringify(message)}\n`;
|
|
41
|
-
} catch {
|
|
42
|
-
return `${String(message)}\n`;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function agentOutputDetailsForLog(agentStdout, agentStderr) {
|
|
46
|
-
return {
|
|
47
|
-
agentStdout,
|
|
48
|
-
agentStdoutTruncated: agentStdout.length >= AGENT_OUTPUT_LOG_MAX_LENGTH,
|
|
49
|
-
agentStderr,
|
|
50
|
-
agentStderrTruncated: agentStderr.length >= AGENT_OUTPUT_LOG_MAX_LENGTH
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function isConfigModule(value) {
|
|
54
|
-
return value !== null && typeof value === "object";
|
|
55
|
-
}
|
|
56
|
-
function sha256String(value) {
|
|
57
|
-
return createHash("sha256").update(value).digest("hex");
|
|
58
|
-
}
|
|
59
|
-
function resolveConfigFilePath(inputPath) {
|
|
60
|
-
const resolved = path.resolve(inputPath);
|
|
61
|
-
if (/\.(ts|js|mjs|cjs)$/i.test(resolved)) return resolved;
|
|
62
|
-
const hexclaveCandidate = path.join(resolved, "hexclave.config.ts");
|
|
63
|
-
const legacyCandidate = path.join(resolved, "stack.config.ts");
|
|
64
|
-
if (existsSync(hexclaveCandidate)) return hexclaveCandidate;
|
|
65
|
-
if (existsSync(legacyCandidate)) return legacyCandidate;
|
|
66
|
-
return hexclaveCandidate;
|
|
67
|
-
}
|
|
68
|
-
function ensureConfigFileExists(configFilePath) {
|
|
69
|
-
if (existsSync(configFilePath)) return;
|
|
70
|
-
mkdirSync(path.dirname(configFilePath), { recursive: true });
|
|
71
|
-
renderConfigObjectToFile(configFilePath, {});
|
|
72
|
-
}
|
|
73
|
-
async function readConfigObject(configFilePath) {
|
|
74
|
-
return (await readConfigFile(configFilePath)).config;
|
|
75
|
-
}
|
|
76
|
-
async function readConfigFile(configFilePath) {
|
|
77
|
-
ensureConfigFileExists(configFilePath);
|
|
78
|
-
if (readFileSync(configFilePath, "utf-8").trim() === "") return {
|
|
79
|
-
config: {},
|
|
80
|
-
showOnboarding: false
|
|
81
|
-
};
|
|
82
|
-
let configModule;
|
|
83
|
-
try {
|
|
84
|
-
configModule = await jiti.import(configFilePath);
|
|
85
|
-
} catch (error) {
|
|
86
|
-
captureError("shared-backend/readConfigFile", error);
|
|
87
|
-
throw new Error(`Failed to load config file ${configFilePath}.`);
|
|
88
|
-
}
|
|
89
|
-
if (!isConfigModule(configModule)) throw new Error(`Invalid config in ${configFilePath}. The file must export a plain \`config\` object or "show-onboarding".`);
|
|
90
|
-
const config = configModule.config;
|
|
91
|
-
if (config === showOnboardingHexclaveConfigValue) return {
|
|
92
|
-
config: {},
|
|
93
|
-
showOnboarding: true
|
|
94
|
-
};
|
|
95
|
-
if (!isValidConfig(config)) throw new Error(`Invalid config in ${configFilePath}.`);
|
|
96
|
-
return {
|
|
97
|
-
config,
|
|
98
|
-
showOnboarding: false
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
function renderConfigObjectToString(configFilePath, config) {
|
|
102
|
-
return renderConfigFileContent(config, detectImportPackageFromDir(path.dirname(configFilePath)));
|
|
103
|
-
}
|
|
104
|
-
function writeFileAtomic(configFilePath, content) {
|
|
105
|
-
const dir = path.dirname(configFilePath);
|
|
106
|
-
mkdirSync(dir, { recursive: true });
|
|
107
|
-
const tempPath = path.join(dir, `.stack.config.${Math.random().toString(36).slice(2)}.tmp`);
|
|
108
|
-
writeFileSync(tempPath, content, "utf-8");
|
|
109
|
-
try {
|
|
110
|
-
renameSync(tempPath, configFilePath);
|
|
111
|
-
} catch (error) {
|
|
112
|
-
try {
|
|
113
|
-
rmSync(tempPath);
|
|
114
|
-
} catch {}
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
function renderConfigObjectToFile(configFilePath, config) {
|
|
119
|
-
writeFileAtomic(configFilePath, renderConfigObjectToString(configFilePath, config));
|
|
120
|
-
}
|
|
121
|
-
async function updateConfigObject(configFilePath, configUpdate) {
|
|
122
|
-
const startedAtMs = performance.now();
|
|
123
|
-
ensureConfigFileExists(configFilePath);
|
|
124
|
-
const changes = flattenConfigUpdate(configUpdate);
|
|
125
|
-
if (changes.length === 0) {
|
|
126
|
-
console.log(`${LOG_PREFIX} Skipping config update because it contains no changes`, { configFilePath });
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const updateLogDetails = {
|
|
130
|
-
configFilePath,
|
|
131
|
-
...configUpdatePathDetailsForLog(changes)
|
|
132
|
-
};
|
|
133
|
-
console.log(`${LOG_PREFIX} Starting config file update`, updateLogDetails);
|
|
134
|
-
const content = readFileSync(configFilePath, "utf-8");
|
|
135
|
-
const staticConfig = tryParseStaticConfigFileContent(content, configFilePath);
|
|
136
|
-
if (staticConfig != null && isValidConfig(staticConfig)) {
|
|
137
|
-
console.log(`${LOG_PREFIX} Applying config update with static config rewrite`, updateLogDetails);
|
|
138
|
-
const merged = override(staticConfig, configUpdate);
|
|
139
|
-
if (!isValidConfig(merged)) throw new Error(`${LOG_PREFIX} Merged config is invalid after applying update to ${configFilePath}`);
|
|
140
|
-
renderConfigObjectToFile(configFilePath, merged);
|
|
141
|
-
console.log(`${LOG_PREFIX} Finished config update with static config rewrite`, {
|
|
142
|
-
...updateLogDetails,
|
|
143
|
-
elapsedMs: Math.round(performance.now() - startedAtMs)
|
|
144
|
-
});
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
console.log(`${LOG_PREFIX} Applying config update with agent-assisted rewrite`, {
|
|
148
|
-
...updateLogDetails,
|
|
149
|
-
configDirectory: path.dirname(configFilePath)
|
|
150
|
-
});
|
|
151
|
-
const baselineConfig = await tryReadConfigForValidation(configFilePath);
|
|
152
|
-
const { snapshots, seen } = snapshotConfigFiles(configFilePath, content);
|
|
153
|
-
try {
|
|
154
|
-
await runConfigUpdateAgent({
|
|
155
|
-
prompt: buildConfigUpdatePrompt(path.basename(configFilePath), configUpdate, baselineConfig),
|
|
156
|
-
cwd: path.dirname(configFilePath),
|
|
157
|
-
onFileWillChange: (filePath) => captureSnapshotIfAbsent(snapshots, filePath, seen)
|
|
158
|
-
});
|
|
159
|
-
await validateAgentUpdate(configFilePath, baselineConfig, configUpdate, snapshots);
|
|
160
|
-
} catch (error) {
|
|
161
|
-
console.warn(`${LOG_PREFIX} Config update failed; restoring files from snapshots`, {
|
|
162
|
-
...updateLogDetails,
|
|
163
|
-
snapshotCount: snapshots.length,
|
|
164
|
-
elapsedMs: Math.round(performance.now() - startedAtMs),
|
|
165
|
-
...formatConfigUpdaterErrorForLog(error)
|
|
166
|
-
});
|
|
167
|
-
try {
|
|
168
|
-
restoreConfigFiles(snapshots);
|
|
169
|
-
console.warn(`${LOG_PREFIX} Restored files after failed config update`, {
|
|
170
|
-
...updateLogDetails,
|
|
171
|
-
snapshotCount: snapshots.length
|
|
172
|
-
});
|
|
173
|
-
} catch (restoreError) {
|
|
174
|
-
console.error(`${LOG_PREFIX} Failed to fully roll back config files after a failed update of ${configFilePath}; some files may be left in a partially-restored state`, {
|
|
175
|
-
configFilePath,
|
|
176
|
-
...formatConfigUpdaterErrorForLog(restoreError)
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
throw error;
|
|
180
|
-
}
|
|
181
|
-
console.log(`${LOG_PREFIX} Finished config update with agent-assisted rewrite`, {
|
|
182
|
-
...updateLogDetails,
|
|
183
|
-
elapsedMs: Math.round(performance.now() - startedAtMs),
|
|
184
|
-
snapshotCount: snapshots.length
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
async function replaceConfigObject(configFilePath, config) {
|
|
188
|
-
renderConfigObjectToFile(configFilePath, config);
|
|
189
|
-
}
|
|
190
|
-
async function runConfigUpdateAgent(options) {
|
|
191
|
-
const timeoutMs = parseAgentTimeoutMs();
|
|
192
|
-
const deniedOutOfBoundsWrites = /* @__PURE__ */ new Set();
|
|
193
|
-
const startedAtMs = performance.now();
|
|
194
|
-
let agentStdout = "";
|
|
195
|
-
let agentStderr = "";
|
|
196
|
-
console.log(`${LOG_PREFIX} Starting config update agent`, {
|
|
197
|
-
cwd: options.cwd,
|
|
198
|
-
timeoutMs
|
|
199
|
-
});
|
|
200
|
-
try {
|
|
201
|
-
await runHeadlessClaudeAgent({
|
|
202
|
-
prompt: options.prompt,
|
|
203
|
-
cwd: options.cwd,
|
|
204
|
-
allowedTools: [
|
|
205
|
-
"Read",
|
|
206
|
-
"Write",
|
|
207
|
-
"Edit",
|
|
208
|
-
"Glob",
|
|
209
|
-
"Grep"
|
|
210
|
-
],
|
|
211
|
-
strictIsolation: true,
|
|
212
|
-
timeoutMs,
|
|
213
|
-
stderr: (data) => {
|
|
214
|
-
agentStderr = appendBoundedAgentOutput(agentStderr, data);
|
|
215
|
-
console.warn(`${LOG_PREFIX} [agent] ${data}`);
|
|
216
|
-
},
|
|
217
|
-
onMessage: (message) => {
|
|
218
|
-
agentStdout = appendBoundedAgentOutput(agentStdout, stringifyAgentMessageForLog(message));
|
|
219
|
-
},
|
|
220
|
-
onPreToolUse: (input) => {
|
|
221
|
-
const target = getToolWriteTargetPath(input.tool_name, input.tool_input, options.cwd);
|
|
222
|
-
if (target == null) return { continue: true };
|
|
223
|
-
if (!isPathInsideDir(options.cwd, target)) {
|
|
224
|
-
deniedOutOfBoundsWrites.add(target);
|
|
225
|
-
return { hookSpecificOutput: {
|
|
226
|
-
hookEventName: "PreToolUse",
|
|
227
|
-
permissionDecision: "deny",
|
|
228
|
-
permissionDecisionReason: `Refusing to modify ${target}: config updates may only change files inside the config directory.`
|
|
229
|
-
} };
|
|
230
|
-
}
|
|
231
|
-
options.onFileWillChange?.(target);
|
|
232
|
-
return { continue: true };
|
|
233
|
-
}
|
|
234
|
-
});
|
|
235
|
-
} catch (error) {
|
|
236
|
-
if (error instanceof ClaudeAgentTimeoutError) {
|
|
237
|
-
console.warn(`${LOG_PREFIX} Config update agent timed out`, {
|
|
238
|
-
cwd: options.cwd,
|
|
239
|
-
timeoutMs,
|
|
240
|
-
elapsedMs: Math.round(performance.now() - startedAtMs),
|
|
241
|
-
...formatConfigUpdaterErrorForLog(error),
|
|
242
|
-
...agentOutputDetailsForLog(agentStdout, agentStderr)
|
|
243
|
-
});
|
|
244
|
-
throw new Error(`Config update agent timed out after ${timeoutMs}ms. It was unable to apply the config changes to the file.`);
|
|
245
|
-
}
|
|
246
|
-
if (error instanceof ClaudeAgentFailureError) {
|
|
247
|
-
console.warn(`${LOG_PREFIX} Config update agent failed`, {
|
|
248
|
-
cwd: options.cwd,
|
|
249
|
-
timeoutMs,
|
|
250
|
-
elapsedMs: Math.round(performance.now() - startedAtMs),
|
|
251
|
-
...formatConfigUpdaterErrorForLog(error),
|
|
252
|
-
...agentOutputDetailsForLog(agentStdout, agentStderr)
|
|
253
|
-
});
|
|
254
|
-
throw new Error(`${error.message} It was unable to apply the config changes to the file.`);
|
|
255
|
-
}
|
|
256
|
-
console.warn(`${LOG_PREFIX} Config update agent failed unexpectedly`, {
|
|
257
|
-
cwd: options.cwd,
|
|
258
|
-
timeoutMs,
|
|
259
|
-
elapsedMs: Math.round(performance.now() - startedAtMs),
|
|
260
|
-
...formatConfigUpdaterErrorForLog(error),
|
|
261
|
-
...agentOutputDetailsForLog(agentStdout, agentStderr)
|
|
262
|
-
});
|
|
263
|
-
throw error;
|
|
264
|
-
}
|
|
265
|
-
console.log(`${LOG_PREFIX} Finished config update agent`, {
|
|
266
|
-
cwd: options.cwd,
|
|
267
|
-
timeoutMs,
|
|
268
|
-
elapsedMs: Math.round(performance.now() - startedAtMs),
|
|
269
|
-
deniedOutOfBoundsWriteCount: deniedOutOfBoundsWrites.size
|
|
270
|
-
});
|
|
271
|
-
if (deniedOutOfBoundsWrites.size > 0) {
|
|
272
|
-
console.warn(`${LOG_PREFIX} Config update agent attempted out-of-bounds writes`, {
|
|
273
|
-
cwd: options.cwd,
|
|
274
|
-
deniedOutOfBoundsWriteCount: deniedOutOfBoundsWrites.size,
|
|
275
|
-
deniedOutOfBoundsWrites: [...deniedOutOfBoundsWrites]
|
|
276
|
-
});
|
|
277
|
-
throw new Error(`Config update agent tried to modify ${deniedOutOfBoundsWrites.size} file(s) outside the config directory, which is not allowed: ${[...deniedOutOfBoundsWrites].join(", ")}. The config was not updated.`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
function parseAgentTimeoutMs() {
|
|
281
|
-
const raw = process.env.STACK_CONFIG_UPDATE_AGENT_TIMEOUT_MS;
|
|
282
|
-
if (raw == null || raw.trim() === "") return DEFAULT_AGENT_TIMEOUT_MS;
|
|
283
|
-
const parsed = Number(raw);
|
|
284
|
-
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`Invalid STACK_CONFIG_UPDATE_AGENT_TIMEOUT_MS: ${JSON.stringify(raw)}. Expected a positive number of milliseconds.`);
|
|
285
|
-
return parsed;
|
|
286
|
-
}
|
|
287
|
-
function captureSnapshotIfAbsent(snapshots, filePath, seen) {
|
|
288
|
-
const resolved = path.resolve(filePath);
|
|
289
|
-
if (seen.has(resolved)) return;
|
|
290
|
-
seen.add(resolved);
|
|
291
|
-
snapshots.push({
|
|
292
|
-
path: resolved,
|
|
293
|
-
content: existsSync(resolved) ? readFileSync(resolved, "utf-8") : null
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
function snapshotConfigFiles(configFilePath, configContent) {
|
|
297
|
-
const dir = path.dirname(configFilePath);
|
|
298
|
-
const resolvedConfig = path.resolve(configFilePath);
|
|
299
|
-
const snapshots = [{
|
|
300
|
-
path: resolvedConfig,
|
|
301
|
-
content: configContent
|
|
302
|
-
}];
|
|
303
|
-
const seen = new Set([resolvedConfig]);
|
|
304
|
-
for (const specifier of getRelativeImportSpecifiers(configContent)) {
|
|
305
|
-
const resolved = path.resolve(dir, specifier);
|
|
306
|
-
if (!isPathInsideDir(dir, resolved)) continue;
|
|
307
|
-
captureSnapshotIfAbsent(snapshots, resolved, seen);
|
|
308
|
-
}
|
|
309
|
-
return {
|
|
310
|
-
snapshots,
|
|
311
|
-
seen
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
function restoreConfigFiles(snapshots) {
|
|
315
|
-
const failures = [];
|
|
316
|
-
for (const { path: filePath, content } of snapshots) try {
|
|
317
|
-
if (content === null) {
|
|
318
|
-
if (existsSync(filePath)) rmSync(filePath);
|
|
319
|
-
} else writeFileSync(filePath, content, "utf-8");
|
|
320
|
-
} catch (error) {
|
|
321
|
-
failures.push(`${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
322
|
-
}
|
|
323
|
-
if (failures.length > 0) throw new Error(`Failed to restore ${failures.length} file(s) during rollback: ${failures.join("; ")}`);
|
|
324
|
-
}
|
|
325
|
-
async function tryReadConfigForValidation(configFilePath) {
|
|
326
|
-
try {
|
|
327
|
-
return (await readConfigFile(configFilePath)).config;
|
|
328
|
-
} catch (error) {
|
|
329
|
-
console.warn(`${LOG_PREFIX} Could not evaluate config for validation baseline; will fall back to a structural check`, {
|
|
330
|
-
configFilePath,
|
|
331
|
-
error: error instanceof Error ? error.message : String(error)
|
|
332
|
-
});
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
async function validateAgentUpdate(configFilePath, baselineConfig, configUpdate, snapshots) {
|
|
337
|
-
if (baselineConfig != null) {
|
|
338
|
-
const target = canonicalizeConfig(override(baselineConfig, configUpdate));
|
|
339
|
-
if (!configsEqual(canonicalizeConfig((await readConfigFile(configFilePath)).config), target)) throw new Error(`Config update validation failed for ${configFilePath}: the updated file does not evaluate to the expected configuration.`);
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
if (flattenConfigUpdate(configUpdate).length > 0 && !snapshotsChangedOnDisk(snapshots)) console.warn(`${LOG_PREFIX} Agent did not modify any file for ${configFilePath}; assuming values are already up to date.`);
|
|
343
|
-
if (!configFileExportsConfig(readFileSync(configFilePath, "utf-8"), configFilePath)) throw new Error(`Config update validation failed for ${configFilePath}: the updated file no longer exports a valid \`config\`.`);
|
|
344
|
-
}
|
|
345
|
-
function tryParseStaticConfigFileContent(content, configFilePath) {
|
|
346
|
-
try {
|
|
347
|
-
const parsed = parseHexclaveConfigFileContent(content, configFilePath);
|
|
348
|
-
return isValidConfig(parsed) ? parsed : null;
|
|
349
|
-
} catch {
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
function configFileExportsConfig(content, configFilePath) {
|
|
354
|
-
try {
|
|
355
|
-
parseHexclaveConfigFileContent(content, configFilePath);
|
|
356
|
-
return true;
|
|
357
|
-
} catch {
|
|
358
|
-
return /\bexport\s+const\s+config\b/.test(content);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
function getRelativeImportSpecifiers(content) {
|
|
362
|
-
const specifiers = [];
|
|
363
|
-
const importPattern = /\bimport\b(?:[^'"]*?\bfrom\s*)?["'](\.{1,2}\/[^"']+)["']/g;
|
|
364
|
-
let match;
|
|
365
|
-
while ((match = importPattern.exec(content)) !== null) specifiers.push(match[1]);
|
|
366
|
-
return specifiers;
|
|
367
|
-
}
|
|
368
|
-
function snapshotsChangedOnDisk(snapshots) {
|
|
369
|
-
return snapshots.some(({ path: filePath, content }) => {
|
|
370
|
-
return (existsSync(filePath) ? readFileSync(filePath, "utf-8") : null) !== content;
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
function flattenConfigUpdate(update) {
|
|
374
|
-
const changes = [];
|
|
375
|
-
const walk = (prefix, obj) => {
|
|
376
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
377
|
-
const fullPath = prefix === "" ? key : `${prefix}.${key}`;
|
|
378
|
-
if (value === void 0) continue;
|
|
379
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0) walk(fullPath, value);
|
|
380
|
-
else changes.push({
|
|
381
|
-
path: fullPath,
|
|
382
|
-
value
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
};
|
|
386
|
-
walk("", update);
|
|
387
|
-
return changes;
|
|
388
|
-
}
|
|
389
|
-
function buildConfigUpdatePrompt(configFileName, configUpdate, baselineConfig) {
|
|
390
|
-
const changeLines = flattenConfigUpdate(configUpdate).map(({ path: configPath, value }) => {
|
|
391
|
-
return `- ${JSON.stringify(configPath)}: set to ${JSON.stringify(value)}`;
|
|
392
|
-
}).join("\n");
|
|
393
|
-
const expectedConfig = baselineConfig == null ? null : canonicalizeConfig(override(baselineConfig, configUpdate));
|
|
394
|
-
const expectedConfigSection = expectedConfig == null ? "" : `
|
|
395
|
-
After the edit, evaluating the exported \`config\` must produce this exact JSON value:
|
|
3
|
+
export * from "./config-updater.js"
|
|
396
4
|
|
|
397
|
-
|
|
398
|
-
`;
|
|
399
|
-
return `You are editing a Hexclave / Stack Auth configuration file in place. Apply a set of configuration changes WITHOUT changing how the file is written.
|
|
400
|
-
|
|
401
|
-
Config file: ${JSON.stringify(configFileName)} (in the current working directory).
|
|
402
|
-
|
|
403
|
-
The file exports a \`config\` object (it may be wrapped in a helper such as \`defineStackConfig(...)\`). Some config values may be sourced from other files via imports, for example:
|
|
404
|
-
|
|
405
|
-
import welcomeEmail from "./welcome-email.tsx" with { type: "text" };
|
|
406
|
-
export const config = { emails: { templates: { welcome: welcomeEmail } } };
|
|
407
|
-
|
|
408
|
-
Apply EXACTLY these changes. Paths use dot notation, so \`a.b.c\` refers to \`config.a.b.c\`:
|
|
409
|
-
|
|
410
|
-
${changeLines}
|
|
411
|
-
${expectedConfigSection}
|
|
412
|
-
|
|
413
|
-
Rules:
|
|
414
|
-
- Change ONLY the config paths listed above. Leave every other part of the file byte-for-byte unchanged: imports, comments, formatting, helper wrappers, and any config fields not listed.
|
|
415
|
-
- If a listed path's value is currently provided by an imported external file (like the \`import ... with { type: "text" }\` example above), DO NOT inline the new value into the config file. Instead, overwrite that external file with the new value and keep the import statement intact.
|
|
416
|
-
- If a listed path's value is a plain inline literal, edit it inline.
|
|
417
|
-
- Keep the file valid: it must still export a \`config\` that, once evaluated, reflects the new values exactly.
|
|
418
|
-
- Do not run any shell commands and do not create files other than what is required to apply these changes.`;
|
|
419
|
-
}
|
|
420
|
-
function canonicalizeConfig(config) {
|
|
421
|
-
const droppedKeys = [];
|
|
422
|
-
const normalized = normalize(config, {
|
|
423
|
-
onDotIntoNonObject: "ignore",
|
|
424
|
-
onDotIntoNull: "empty-object",
|
|
425
|
-
droppedKeys
|
|
426
|
-
});
|
|
427
|
-
if (droppedKeys.length > 0) throw new Error(`Config update has conflicting keys that would be dropped during normalization: ${droppedKeys.map((key) => JSON.stringify(key)).join(", ")}`);
|
|
428
|
-
return normalized;
|
|
429
|
-
}
|
|
430
|
-
function configsEqual(a, b) {
|
|
431
|
-
if (a === b) return true;
|
|
432
|
-
if (a === null || b === null) return a === b;
|
|
433
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
434
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
435
|
-
return a.every((value, index) => configsEqual(value, b[index]));
|
|
436
|
-
}
|
|
437
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
438
|
-
const aEntries = Object.entries(a);
|
|
439
|
-
const bMap = new Map(Object.entries(b));
|
|
440
|
-
if (aEntries.length !== bMap.size) return false;
|
|
441
|
-
return aEntries.every(([key, value]) => bMap.has(key) && configsEqual(value, bMap.get(key)));
|
|
442
|
-
}
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
//#endregion
|
|
447
|
-
export { ensureConfigFileExists, readConfigFile, readConfigObject, replaceConfigObject, resolveConfigFilePath, sha256String, updateConfigObject };
|
|
448
|
-
//# sourceMappingURL=index.js.map
|
|
5
|
+
export { };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
declare function sha256String(value: string): string;
|
|
5
|
-
declare function resolveConfigFilePath(inputPath: string): string;
|
|
6
|
-
declare function ensureConfigFileExists(configFilePath: string): void;
|
|
7
|
-
declare function readConfigObject(configFilePath: string): Promise<Config>;
|
|
8
|
-
declare function readConfigFile(configFilePath: string): Promise<{
|
|
9
|
-
config: Config;
|
|
10
|
-
showOnboarding: boolean;
|
|
11
|
-
}>;
|
|
12
|
-
declare function updateConfigObject(configFilePath: string, configUpdate: Config): Promise<void>;
|
|
13
|
-
declare function replaceConfigObject(configFilePath: string, config: Config): Promise<void>;
|
|
14
|
-
//#endregion
|
|
15
|
-
export { ensureConfigFileExists, readConfigFile, readConfigObject, replaceConfigObject, resolveConfigFilePath, sha256String, updateConfigObject };
|
|
16
|
-
//# sourceMappingURL=index.d.ts.map
|
|
1
|
+
import { ensureConfigFileExists, readConfigFile, readConfigObject, replaceConfigObject, resolveConfigFilePath, sha256String } from "./config-file.js";
|
|
2
|
+
import { updateConfigObject } from "./config-updater.js";
|
|
3
|
+
export { ensureConfigFileExists, readConfigFile, readConfigObject, replaceConfigObject, resolveConfigFilePath, sha256String, updateConfigObject };
|