zouroboros-core 2.0.0
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/LICENSE +21 -0
- package/README.md +85 -0
- package/dist/backup.d.ts +65 -0
- package/dist/backup.js +203 -0
- package/dist/commands.d.ts +69 -0
- package/dist/commands.js +307 -0
- package/dist/config/loader.d.ts +48 -0
- package/dist/config/loader.js +145 -0
- package/dist/config/schema.d.ts +597 -0
- package/dist/config/schema.js +151 -0
- package/dist/constants.d.ts +57 -0
- package/dist/constants.js +225 -0
- package/dist/errors.d.ts +79 -0
- package/dist/errors.js +171 -0
- package/dist/hooks.d.ts +81 -0
- package/dist/hooks.js +231 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +21 -0
- package/dist/instincts.d.ts +117 -0
- package/dist/instincts.js +428 -0
- package/dist/migrations.d.ts +56 -0
- package/dist/migrations.js +123 -0
- package/dist/sessions.d.ts +88 -0
- package/dist/sessions.js +275 -0
- package/dist/token-budget.d.ts +76 -0
- package/dist/token-budget.js +196 -0
- package/dist/types.d.ts +406 -0
- package/dist/types.js +6 -0
- package/dist/utils/index.d.ts +55 -0
- package/dist/utils/index.js +132 -0
- package/package.json +50 -0
package/dist/commands.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECC-002: Slash Commands Hub
|
|
3
|
+
*
|
|
4
|
+
* First-class CLI-style commands callable from any persona conversation.
|
|
5
|
+
* Zero-dependency command parser with unified help, registration, and routing.
|
|
6
|
+
* Supports subcommands: /memory search "query" → routes to memory.search handler.
|
|
7
|
+
*/
|
|
8
|
+
export class CommandHub {
|
|
9
|
+
commands = new Map();
|
|
10
|
+
aliases = new Map();
|
|
11
|
+
hooks = null;
|
|
12
|
+
/** Wire to hook system for command.execute events */
|
|
13
|
+
wireHooks(hooks) {
|
|
14
|
+
this.hooks = hooks;
|
|
15
|
+
}
|
|
16
|
+
register(definition) {
|
|
17
|
+
if (!definition.name || !definition.name.startsWith('/')) {
|
|
18
|
+
throw new Error(`Command name must start with '/': ${definition.name}`);
|
|
19
|
+
}
|
|
20
|
+
// Check for alias collisions
|
|
21
|
+
for (const alias of definition.aliases) {
|
|
22
|
+
const existing = this.aliases.get(alias);
|
|
23
|
+
if (existing && existing !== definition.name) {
|
|
24
|
+
throw new Error(`Alias '${alias}' already registered to '${existing}', cannot register for '${definition.name}'`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
this.commands.set(definition.name, definition);
|
|
28
|
+
for (const alias of definition.aliases) {
|
|
29
|
+
this.aliases.set(alias, definition.name);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Register a subcommand under an existing command */
|
|
33
|
+
registerSubcommand(commandName, sub) {
|
|
34
|
+
const cmd = this.commands.get(commandName);
|
|
35
|
+
if (!cmd)
|
|
36
|
+
throw new Error(`Command '${commandName}' not found`);
|
|
37
|
+
if (!cmd.subcommands)
|
|
38
|
+
cmd.subcommands = new Map();
|
|
39
|
+
cmd.subcommands.set(sub.name, sub);
|
|
40
|
+
}
|
|
41
|
+
unregister(name) {
|
|
42
|
+
const cmd = this.commands.get(name);
|
|
43
|
+
if (!cmd)
|
|
44
|
+
return false;
|
|
45
|
+
for (const alias of cmd.aliases) {
|
|
46
|
+
this.aliases.delete(alias);
|
|
47
|
+
}
|
|
48
|
+
this.commands.delete(name);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
resolve(nameOrAlias) {
|
|
52
|
+
const cmd = this.commands.get(nameOrAlias);
|
|
53
|
+
if (cmd)
|
|
54
|
+
return cmd;
|
|
55
|
+
const canonical = this.aliases.get(nameOrAlias);
|
|
56
|
+
if (canonical)
|
|
57
|
+
return this.commands.get(canonical) || null;
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
parse(input) {
|
|
61
|
+
const trimmed = input.trim();
|
|
62
|
+
if (!trimmed.startsWith('/'))
|
|
63
|
+
return null;
|
|
64
|
+
const parts = this.tokenize(trimmed);
|
|
65
|
+
if (parts.length === 0)
|
|
66
|
+
return null;
|
|
67
|
+
const name = parts[0];
|
|
68
|
+
const args = {};
|
|
69
|
+
const flags = new Set();
|
|
70
|
+
let subcommand;
|
|
71
|
+
const def = this.resolve(name);
|
|
72
|
+
// Check for subcommand: if part[1] exists and matches a registered subcommand
|
|
73
|
+
let argDefs = def?.args || [];
|
|
74
|
+
let partStart = 1;
|
|
75
|
+
if (def?.subcommands && parts.length > 1 && def.subcommands.has(parts[1])) {
|
|
76
|
+
subcommand = parts[1];
|
|
77
|
+
argDefs = def.subcommands.get(parts[1]).args;
|
|
78
|
+
partStart = 2;
|
|
79
|
+
}
|
|
80
|
+
let positionalIndex = 0;
|
|
81
|
+
for (let i = partStart; i < parts.length; i++) {
|
|
82
|
+
const part = parts[i];
|
|
83
|
+
if (part.startsWith('--')) {
|
|
84
|
+
const key = part.slice(2);
|
|
85
|
+
const eqIdx = key.indexOf('=');
|
|
86
|
+
if (eqIdx >= 0) {
|
|
87
|
+
args[key.slice(0, eqIdx)] = key.slice(eqIdx + 1);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const argDef = argDefs.find(a => a.name === key);
|
|
91
|
+
if (argDef?.type === 'flag' || argDef?.type === 'boolean') {
|
|
92
|
+
flags.add(key);
|
|
93
|
+
args[key] = true;
|
|
94
|
+
}
|
|
95
|
+
else if (i + 1 < parts.length && !parts[i + 1].startsWith('--')) {
|
|
96
|
+
args[key] = this.coerceArg(parts[++i], argDef?.type);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
flags.add(key);
|
|
100
|
+
args[key] = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const positionalDefs = argDefs.filter(a => a.type !== 'flag');
|
|
106
|
+
if (positionalIndex < positionalDefs.length) {
|
|
107
|
+
const argDef = positionalDefs[positionalIndex];
|
|
108
|
+
args[argDef.name] = this.coerceArg(part, argDef.type);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
args[`_${positionalIndex}`] = part;
|
|
112
|
+
}
|
|
113
|
+
positionalIndex++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Apply defaults
|
|
117
|
+
for (const argDef of argDefs) {
|
|
118
|
+
if (argDef.default !== undefined && args[argDef.name] === undefined) {
|
|
119
|
+
args[argDef.name] = argDef.default;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { name, subcommand, args, raw: trimmed, flags };
|
|
123
|
+
}
|
|
124
|
+
async execute(input) {
|
|
125
|
+
const parsed = this.parse(input);
|
|
126
|
+
if (!parsed) {
|
|
127
|
+
return { success: false, output: '', error: 'Invalid command format. Commands must start with /' };
|
|
128
|
+
}
|
|
129
|
+
const cmd = this.resolve(parsed.name);
|
|
130
|
+
if (!cmd) {
|
|
131
|
+
const suggestions = this.suggest(parsed.name);
|
|
132
|
+
const suggestionText = suggestions.length > 0
|
|
133
|
+
? ` Did you mean: ${suggestions.join(', ')}?`
|
|
134
|
+
: '';
|
|
135
|
+
return { success: false, output: '', error: `Unknown command: ${parsed.name}.${suggestionText}` };
|
|
136
|
+
}
|
|
137
|
+
// Route to subcommand handler if applicable
|
|
138
|
+
let handler = cmd.handler;
|
|
139
|
+
let requiredArgs = cmd.args;
|
|
140
|
+
if (parsed.subcommand && cmd.subcommands?.has(parsed.subcommand)) {
|
|
141
|
+
const sub = cmd.subcommands.get(parsed.subcommand);
|
|
142
|
+
handler = sub.handler;
|
|
143
|
+
requiredArgs = sub.args;
|
|
144
|
+
}
|
|
145
|
+
// Validate required args
|
|
146
|
+
for (const argDef of requiredArgs) {
|
|
147
|
+
if (argDef.required && parsed.args[argDef.name] === undefined) {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
output: '',
|
|
151
|
+
error: `Missing required argument: ${argDef.name}\nUsage: ${cmd.usage}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const result = await handler(parsed);
|
|
157
|
+
// Emit command.execute hook event
|
|
158
|
+
if (this.hooks) {
|
|
159
|
+
this.hooks.emit('command.execute', {
|
|
160
|
+
command: parsed.name,
|
|
161
|
+
subcommand: parsed.subcommand,
|
|
162
|
+
success: result.success,
|
|
163
|
+
}, 'command-hub').catch(() => { });
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
output: '',
|
|
171
|
+
error: `Command failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
help(commandName) {
|
|
176
|
+
if (commandName) {
|
|
177
|
+
const cmd = this.resolve(commandName);
|
|
178
|
+
if (!cmd)
|
|
179
|
+
return `Unknown command: ${commandName}`;
|
|
180
|
+
const lines = [
|
|
181
|
+
`${cmd.name} — ${cmd.description}`,
|
|
182
|
+
`Usage: ${cmd.usage}`,
|
|
183
|
+
];
|
|
184
|
+
if (cmd.aliases.length > 0) {
|
|
185
|
+
lines.push(`Aliases: ${cmd.aliases.join(', ')}`);
|
|
186
|
+
}
|
|
187
|
+
if (cmd.subcommands && cmd.subcommands.size > 0) {
|
|
188
|
+
lines.push('Subcommands:');
|
|
189
|
+
for (const [subName, sub] of cmd.subcommands) {
|
|
190
|
+
lines.push(` ${subName} — ${sub.description}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (cmd.args.length > 0) {
|
|
194
|
+
lines.push('Arguments:');
|
|
195
|
+
for (const arg of cmd.args) {
|
|
196
|
+
const req = arg.required ? '(required)' : `(default: ${arg.default ?? 'none'})`;
|
|
197
|
+
lines.push(` --${arg.name} ${arg.description} ${req}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
202
|
+
// List all commands by category
|
|
203
|
+
const byCategory = new Map();
|
|
204
|
+
for (const cmd of this.commands.values()) {
|
|
205
|
+
if (cmd.hidden)
|
|
206
|
+
continue;
|
|
207
|
+
const list = byCategory.get(cmd.category) || [];
|
|
208
|
+
list.push(cmd);
|
|
209
|
+
byCategory.set(cmd.category, list);
|
|
210
|
+
}
|
|
211
|
+
const lines = ['Available Commands:', ''];
|
|
212
|
+
for (const [category, cmds] of byCategory) {
|
|
213
|
+
lines.push(`[${category}]`);
|
|
214
|
+
for (const cmd of cmds.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
215
|
+
const aliasStr = cmd.aliases.length > 0 ? ` (${cmd.aliases.join(', ')})` : '';
|
|
216
|
+
lines.push(` ${cmd.name}${aliasStr} — ${cmd.description}`);
|
|
217
|
+
}
|
|
218
|
+
lines.push('');
|
|
219
|
+
}
|
|
220
|
+
return lines.join('\n');
|
|
221
|
+
}
|
|
222
|
+
suggest(partial) {
|
|
223
|
+
const lower = partial.toLowerCase();
|
|
224
|
+
const matches = [];
|
|
225
|
+
for (const name of this.commands.keys()) {
|
|
226
|
+
if (name.toLowerCase().includes(lower) || this.levenshtein(name.toLowerCase(), lower) <= 2) {
|
|
227
|
+
matches.push(name);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const [alias, canonical] of this.aliases) {
|
|
231
|
+
if (alias.toLowerCase().includes(lower)) {
|
|
232
|
+
matches.push(`${alias} → ${canonical}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return matches.slice(0, 5);
|
|
236
|
+
}
|
|
237
|
+
list(category) {
|
|
238
|
+
const all = [...this.commands.values()];
|
|
239
|
+
if (category)
|
|
240
|
+
return all.filter(c => c.category === category);
|
|
241
|
+
return all;
|
|
242
|
+
}
|
|
243
|
+
getCategories() {
|
|
244
|
+
const counts = {};
|
|
245
|
+
for (const cmd of this.commands.values()) {
|
|
246
|
+
counts[cmd.category] = (counts[cmd.category] || 0) + 1;
|
|
247
|
+
}
|
|
248
|
+
return counts;
|
|
249
|
+
}
|
|
250
|
+
tokenize(input) {
|
|
251
|
+
const tokens = [];
|
|
252
|
+
let current = '';
|
|
253
|
+
let inQuote = false;
|
|
254
|
+
let quoteChar = '';
|
|
255
|
+
for (const ch of input) {
|
|
256
|
+
if (inQuote) {
|
|
257
|
+
if (ch === quoteChar) {
|
|
258
|
+
inQuote = false;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
current += ch;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (ch === '"' || ch === "'") {
|
|
265
|
+
inQuote = true;
|
|
266
|
+
quoteChar = ch;
|
|
267
|
+
}
|
|
268
|
+
else if (ch === ' ' || ch === '\t') {
|
|
269
|
+
if (current.length > 0) {
|
|
270
|
+
tokens.push(current);
|
|
271
|
+
current = '';
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
current += ch;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (current.length > 0)
|
|
279
|
+
tokens.push(current);
|
|
280
|
+
return tokens;
|
|
281
|
+
}
|
|
282
|
+
coerceArg(value, type) {
|
|
283
|
+
if (type === 'number') {
|
|
284
|
+
const n = Number(value);
|
|
285
|
+
return isNaN(n) ? value : n;
|
|
286
|
+
}
|
|
287
|
+
if (type === 'boolean') {
|
|
288
|
+
return value === 'true' || value === '1' || value === 'yes';
|
|
289
|
+
}
|
|
290
|
+
return value;
|
|
291
|
+
}
|
|
292
|
+
levenshtein(a, b) {
|
|
293
|
+
const m = a.length, n = b.length;
|
|
294
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
|
295
|
+
for (let i = 1; i <= m; i++) {
|
|
296
|
+
for (let j = 1; j <= n; j++) {
|
|
297
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
298
|
+
? dp[i - 1][j - 1]
|
|
299
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return dp[m][n];
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
export function createCommandHub() {
|
|
306
|
+
return new CommandHub();
|
|
307
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Zouroboros
|
|
3
|
+
*/
|
|
4
|
+
import type { ZouroborosConfig } from '../types.js';
|
|
5
|
+
import { DEFAULT_CONFIG, DEFAULT_CONFIG_PATH } from '../constants.js';
|
|
6
|
+
export { DEFAULT_CONFIG, DEFAULT_CONFIG_PATH };
|
|
7
|
+
export { validateConfigSchema, formatValidationErrors } from './schema.js';
|
|
8
|
+
export type { ConfigValidationIssue } from './schema.js';
|
|
9
|
+
/**
|
|
10
|
+
* Configuration validation error
|
|
11
|
+
*/
|
|
12
|
+
export declare class ConfigValidationError extends Error {
|
|
13
|
+
path: string;
|
|
14
|
+
constructor(message: string, path: string);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Load configuration from file or return defaults
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadConfig(configPath?: string): ZouroborosConfig;
|
|
20
|
+
/**
|
|
21
|
+
* Save configuration to file
|
|
22
|
+
*/
|
|
23
|
+
export declare function saveConfig(config: ZouroborosConfig, configPath?: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Merge partial config with defaults
|
|
26
|
+
*/
|
|
27
|
+
export declare function mergeConfig(partial: Partial<ZouroborosConfig>): ZouroborosConfig;
|
|
28
|
+
/**
|
|
29
|
+
* Validate configuration structure using Zod schemas.
|
|
30
|
+
* Throws ConfigValidationError with actionable messages on failure.
|
|
31
|
+
*/
|
|
32
|
+
export declare function validateConfig(config: unknown): ZouroborosConfig;
|
|
33
|
+
/**
|
|
34
|
+
* Get a nested config value by path
|
|
35
|
+
*/
|
|
36
|
+
export declare function getConfigValue<T>(config: ZouroborosConfig, path: string): T | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Set a nested config value by path
|
|
39
|
+
*/
|
|
40
|
+
export declare function setConfigValue<T>(config: ZouroborosConfig, path: string, value: T): ZouroborosConfig;
|
|
41
|
+
/**
|
|
42
|
+
* Initialize configuration with interactive prompts
|
|
43
|
+
*/
|
|
44
|
+
export declare function initConfig(options?: {
|
|
45
|
+
force?: boolean;
|
|
46
|
+
workspaceRoot?: string;
|
|
47
|
+
dataDir?: string;
|
|
48
|
+
}): Promise<ZouroborosConfig>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Zouroboros
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import { DEFAULT_CONFIG, DEFAULT_CONFIG_PATH } from '../constants.js';
|
|
7
|
+
import { validateConfigSchema, formatValidationErrors } from './schema.js';
|
|
8
|
+
export { DEFAULT_CONFIG, DEFAULT_CONFIG_PATH };
|
|
9
|
+
export { validateConfigSchema, formatValidationErrors } from './schema.js';
|
|
10
|
+
/**
|
|
11
|
+
* Configuration validation error
|
|
12
|
+
*/
|
|
13
|
+
export class ConfigValidationError extends Error {
|
|
14
|
+
path;
|
|
15
|
+
constructor(message, path) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.path = path;
|
|
18
|
+
this.name = 'ConfigValidationError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Load configuration from file or return defaults
|
|
23
|
+
*/
|
|
24
|
+
export function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
|
|
25
|
+
if (!existsSync(configPath)) {
|
|
26
|
+
return { ...DEFAULT_CONFIG };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
30
|
+
const parsed = JSON.parse(content);
|
|
31
|
+
return mergeConfig(parsed);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new ConfigValidationError(`Failed to parse config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`, configPath);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Save configuration to file
|
|
39
|
+
*/
|
|
40
|
+
export function saveConfig(config, configPath = DEFAULT_CONFIG_PATH) {
|
|
41
|
+
const validated = validateConfig(config);
|
|
42
|
+
validated.updatedAt = new Date().toISOString();
|
|
43
|
+
const dir = dirname(configPath);
|
|
44
|
+
if (!existsSync(dir)) {
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
writeFileSync(configPath, JSON.stringify(validated, null, 2), 'utf-8');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Merge partial config with defaults
|
|
51
|
+
*/
|
|
52
|
+
export function mergeConfig(partial) {
|
|
53
|
+
return {
|
|
54
|
+
...DEFAULT_CONFIG,
|
|
55
|
+
...partial,
|
|
56
|
+
core: { ...DEFAULT_CONFIG.core, ...partial.core },
|
|
57
|
+
memory: { ...DEFAULT_CONFIG.memory, ...partial.memory },
|
|
58
|
+
swarm: {
|
|
59
|
+
...DEFAULT_CONFIG.swarm,
|
|
60
|
+
...partial.swarm,
|
|
61
|
+
circuitBreaker: { ...DEFAULT_CONFIG.swarm.circuitBreaker, ...partial.swarm?.circuitBreaker },
|
|
62
|
+
retryConfig: { ...DEFAULT_CONFIG.swarm.retryConfig, ...partial.swarm?.retryConfig },
|
|
63
|
+
},
|
|
64
|
+
personas: { ...DEFAULT_CONFIG.personas, ...partial.personas },
|
|
65
|
+
selfheal: {
|
|
66
|
+
...DEFAULT_CONFIG.selfheal,
|
|
67
|
+
...partial.selfheal,
|
|
68
|
+
metrics: { ...DEFAULT_CONFIG.selfheal.metrics, ...partial.selfheal?.metrics },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Validate configuration structure using Zod schemas.
|
|
74
|
+
* Throws ConfigValidationError with actionable messages on failure.
|
|
75
|
+
*/
|
|
76
|
+
export function validateConfig(config) {
|
|
77
|
+
if (typeof config !== 'object' || config === null) {
|
|
78
|
+
throw new ConfigValidationError('Config must be an object', '');
|
|
79
|
+
}
|
|
80
|
+
const issues = validateConfigSchema(config);
|
|
81
|
+
if (issues) {
|
|
82
|
+
throw new ConfigValidationError(formatValidationErrors(issues), issues[0]?.path ?? '');
|
|
83
|
+
}
|
|
84
|
+
return config;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get a nested config value by path
|
|
88
|
+
*/
|
|
89
|
+
export function getConfigValue(config, path) {
|
|
90
|
+
const parts = path.split('.');
|
|
91
|
+
let current = config;
|
|
92
|
+
for (const part of parts) {
|
|
93
|
+
if (typeof current !== 'object' || current === null) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
current = current[part];
|
|
97
|
+
}
|
|
98
|
+
return current;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Set a nested config value by path
|
|
102
|
+
*/
|
|
103
|
+
export function setConfigValue(config, path, value) {
|
|
104
|
+
const parts = path.split('.');
|
|
105
|
+
const newConfig = structuredClone(config);
|
|
106
|
+
let current = newConfig;
|
|
107
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
108
|
+
const part = parts[i];
|
|
109
|
+
if (!(part in current) || typeof current[part] !== 'object' || current[part] === null) {
|
|
110
|
+
current[part] = {};
|
|
111
|
+
}
|
|
112
|
+
current = current[part];
|
|
113
|
+
}
|
|
114
|
+
current[parts[parts.length - 1]] = value;
|
|
115
|
+
newConfig.updatedAt = new Date().toISOString();
|
|
116
|
+
return validateConfig(newConfig);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Initialize configuration with interactive prompts
|
|
120
|
+
*/
|
|
121
|
+
export async function initConfig(options = {}) {
|
|
122
|
+
const configPath = DEFAULT_CONFIG_PATH;
|
|
123
|
+
if (existsSync(configPath) && !options.force) {
|
|
124
|
+
throw new Error(`Config already exists at ${configPath}. Use --force to overwrite.`);
|
|
125
|
+
}
|
|
126
|
+
const config = {
|
|
127
|
+
...DEFAULT_CONFIG,
|
|
128
|
+
createdAt: new Date().toISOString(),
|
|
129
|
+
updatedAt: new Date().toISOString(),
|
|
130
|
+
};
|
|
131
|
+
if (options.workspaceRoot) {
|
|
132
|
+
config.core.workspaceRoot = options.workspaceRoot;
|
|
133
|
+
}
|
|
134
|
+
if (options.dataDir) {
|
|
135
|
+
config.core.dataDir = options.dataDir;
|
|
136
|
+
config.memory.dbPath = join(options.dataDir, 'memory.db');
|
|
137
|
+
config.swarm.registryPath = join(options.dataDir, 'executor-registry.json');
|
|
138
|
+
}
|
|
139
|
+
// Ensure data directory exists
|
|
140
|
+
if (!existsSync(config.core.dataDir)) {
|
|
141
|
+
mkdirSync(config.core.dataDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
saveConfig(config, configPath);
|
|
144
|
+
return config;
|
|
145
|
+
}
|