sumulige-claude 1.1.0 → 1.1.2
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/.claude/hooks/pre-commit.cjs +86 -0
- package/.claude/hooks/pre-push.cjs +103 -0
- package/.claude/quality-gate.json +61 -0
- package/.claude/settings.local.json +4 -1
- package/AGENTS.md +416 -177
- package/Q&A.md +230 -213
- package/README.md +256 -230
- package/cli.js +28 -0
- package/config/quality-gate.json +61 -0
- package/docs/DEVELOPMENT.md +329 -291
- package/lib/commands.js +208 -0
- package/lib/config-manager.js +441 -0
- package/lib/config-schema.js +408 -0
- package/lib/config-validator.js +330 -0
- package/lib/config.js +52 -1
- package/lib/errors.js +305 -0
- package/lib/quality-gate.js +431 -0
- package/lib/quality-rules.js +373 -0
- package/package.json +5 -1
package/lib/commands.js
CHANGED
|
@@ -2321,6 +2321,214 @@ All notable changes to this project will be documented in this file.
|
|
|
2321
2321
|
|
|
2322
2322
|
log('=====================================', 'gray');
|
|
2323
2323
|
log('', 'gray');
|
|
2324
|
+
},
|
|
2325
|
+
|
|
2326
|
+
// ==========================================================================
|
|
2327
|
+
// Quality Gate Commands
|
|
2328
|
+
// ==========================================================================
|
|
2329
|
+
|
|
2330
|
+
'qg:check': async (severity = 'warn') => {
|
|
2331
|
+
const { QualityGate } = require('./quality-gate');
|
|
2332
|
+
const gate = new QualityGate({ projectDir: process.cwd() });
|
|
2333
|
+
const result = await gate.check({ severity });
|
|
2334
|
+
process.exit(result.passed ? 0 : 1);
|
|
2335
|
+
},
|
|
2336
|
+
|
|
2337
|
+
'qg:rules': () => {
|
|
2338
|
+
const registry = require('./quality-rules').registry;
|
|
2339
|
+
const rules = registry.getAll();
|
|
2340
|
+
|
|
2341
|
+
console.log('📋 Available Quality Rules');
|
|
2342
|
+
console.log('');
|
|
2343
|
+
console.log('Rules are checked when running quality gate:');
|
|
2344
|
+
console.log('');
|
|
2345
|
+
|
|
2346
|
+
const byCategory = {};
|
|
2347
|
+
for (const rule of rules) {
|
|
2348
|
+
if (!byCategory[rule.category]) byCategory[rule.category] = [];
|
|
2349
|
+
byCategory[rule.category].push(rule);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
for (const [category, rules] of Object.entries(byCategory)) {
|
|
2353
|
+
console.log(`${category.toUpperCase()}:`);
|
|
2354
|
+
for (const rule of rules) {
|
|
2355
|
+
const status = rule.enabled ? '✅' : '⊝';
|
|
2356
|
+
const sev = { info: 'I', warn: 'W', error: 'E', critical: 'X' }[rule.severity];
|
|
2357
|
+
console.log(` ${status} ${rule.id} [${sev}] - ${rule.name}`);
|
|
2358
|
+
}
|
|
2359
|
+
console.log('');
|
|
2360
|
+
}
|
|
2361
|
+
},
|
|
2362
|
+
|
|
2363
|
+
'qg:init': () => {
|
|
2364
|
+
const projectDir = process.cwd();
|
|
2365
|
+
const configDir = path.join(projectDir, '.claude');
|
|
2366
|
+
const targetPath = path.join(configDir, 'quality-gate.json');
|
|
2367
|
+
const sourcePath = path.join(__dirname, '../config/quality-gate.json');
|
|
2368
|
+
|
|
2369
|
+
if (fs.existsSync(targetPath)) {
|
|
2370
|
+
console.log('⚠️ quality-gate.json already exists');
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
ensureDir(configDir);
|
|
2375
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
2376
|
+
console.log('✅ Created .claude/quality-gate.json');
|
|
2377
|
+
console.log('');
|
|
2378
|
+
console.log('To enable Git hooks:');
|
|
2379
|
+
console.log(' ln -s .claude/hooks/pre-commit.cjs .git/hooks/pre-commit');
|
|
2380
|
+
console.log(' ln -s .claude/hooks/pre-push.cjs .git/hooks/pre-push');
|
|
2381
|
+
},
|
|
2382
|
+
|
|
2383
|
+
// ==========================================================================
|
|
2384
|
+
// Config Commands
|
|
2385
|
+
// ==========================================================================
|
|
2386
|
+
|
|
2387
|
+
'config:validate': () => {
|
|
2388
|
+
const { ConfigValidator } = require('./config-validator');
|
|
2389
|
+
const validator = new ConfigValidator();
|
|
2390
|
+
|
|
2391
|
+
console.log('🔍 Validating configuration...');
|
|
2392
|
+
console.log('');
|
|
2393
|
+
|
|
2394
|
+
let hasErrors = false;
|
|
2395
|
+
let hasWarnings = false;
|
|
2396
|
+
|
|
2397
|
+
// Check global config
|
|
2398
|
+
const globalConfigPath = path.join(process.env.HOME, '.claude', 'config.json');
|
|
2399
|
+
console.log(`Global: ${globalConfigPath}`);
|
|
2400
|
+
const globalResult = validator.validateFile(globalConfigPath);
|
|
2401
|
+
|
|
2402
|
+
if (globalResult.valid) {
|
|
2403
|
+
console.log(' ✅ Valid');
|
|
2404
|
+
} else {
|
|
2405
|
+
if (globalResult.errors.length > 0) {
|
|
2406
|
+
hasErrors = true;
|
|
2407
|
+
console.log(` ❌ ${globalResult.errors.length} error(s)`);
|
|
2408
|
+
globalResult.errors.forEach(e => {
|
|
2409
|
+
console.log(` [${e.severity}] ${e.path}: ${e.message}`);
|
|
2410
|
+
});
|
|
2411
|
+
}
|
|
2412
|
+
if (globalResult.warnings.length > 0) {
|
|
2413
|
+
hasWarnings = true;
|
|
2414
|
+
console.log(` ⚠️ ${globalResult.warnings.length} warning(s)`);
|
|
2415
|
+
globalResult.warnings.forEach(e => {
|
|
2416
|
+
console.log(` [${e.severity}] ${e.path}: ${e.message}`);
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
if (globalResult.errors.length === 0 && globalResult.warnings.length === 0) {
|
|
2420
|
+
console.log(' ❌ Validation failed');
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
console.log('');
|
|
2424
|
+
|
|
2425
|
+
// Check project config if in project
|
|
2426
|
+
const projectDir = process.cwd();
|
|
2427
|
+
const projectConfigPath = path.join(projectDir, '.claude', 'settings.json');
|
|
2428
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
2429
|
+
console.log(`Project: ${projectConfigPath}`);
|
|
2430
|
+
const projectResult = validator.validateFile(projectConfigPath, 'settings');
|
|
2431
|
+
if (projectResult.valid) {
|
|
2432
|
+
console.log(' ✅ Valid');
|
|
2433
|
+
} else {
|
|
2434
|
+
if (projectResult.errors.length > 0) {
|
|
2435
|
+
hasErrors = true;
|
|
2436
|
+
console.log(` ❌ ${projectResult.errors.length} error(s)`);
|
|
2437
|
+
projectResult.errors.forEach(e => {
|
|
2438
|
+
console.log(` [${e.severity}] ${e.path}: ${e.message}`);
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
if (projectResult.warnings.length > 0) {
|
|
2442
|
+
hasWarnings = true;
|
|
2443
|
+
console.log(` ⚠️ ${projectResult.warnings.length} warning(s)`);
|
|
2444
|
+
projectResult.warnings.forEach(e => {
|
|
2445
|
+
console.log(` [${e.severity}] ${e.path}: ${e.message}`);
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
if (projectResult.errors.length === 0 && projectResult.warnings.length === 0) {
|
|
2449
|
+
console.log(' ❌ Validation failed');
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
console.log('');
|
|
2454
|
+
|
|
2455
|
+
if (hasErrors) {
|
|
2456
|
+
console.log('❌ Configuration validation failed');
|
|
2457
|
+
process.exit(1);
|
|
2458
|
+
} else if (hasWarnings) {
|
|
2459
|
+
console.log('⚠️ Configuration has warnings (but is valid)');
|
|
2460
|
+
} else {
|
|
2461
|
+
console.log('✅ All configurations are valid');
|
|
2462
|
+
}
|
|
2463
|
+
},
|
|
2464
|
+
|
|
2465
|
+
'config:backup': () => {
|
|
2466
|
+
const { ConfigManager } = require('./config-manager');
|
|
2467
|
+
const manager = new ConfigManager();
|
|
2468
|
+
const backupPath = manager._createBackup();
|
|
2469
|
+
console.log('✅ Config backed up to:', backupPath);
|
|
2470
|
+
},
|
|
2471
|
+
|
|
2472
|
+
'config:rollback': (version) => {
|
|
2473
|
+
const { ConfigManager } = require('./config-manager');
|
|
2474
|
+
const manager = new ConfigManager();
|
|
2475
|
+
|
|
2476
|
+
const backups = manager.listBackups();
|
|
2477
|
+
if (backups.length === 0) {
|
|
2478
|
+
console.log('❌ No backups available');
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
if (!version) {
|
|
2483
|
+
console.log('Available backups:');
|
|
2484
|
+
backups.forEach((b, i) => {
|
|
2485
|
+
console.log(` ${i + 1}. ${b.version} (${new Date(b.timestamp).toLocaleString()})`);
|
|
2486
|
+
});
|
|
2487
|
+
console.log('');
|
|
2488
|
+
console.log('Usage: smc config:rollback <version>');
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
try {
|
|
2493
|
+
const result = manager.rollback(version);
|
|
2494
|
+
console.log('✅ Rolled back to:', result.restoredFrom);
|
|
2495
|
+
} catch (e) {
|
|
2496
|
+
console.log('❌', e.message);
|
|
2497
|
+
}
|
|
2498
|
+
},
|
|
2499
|
+
|
|
2500
|
+
'config:diff': (file1, file2) => {
|
|
2501
|
+
const { ConfigManager } = require('./config-manager');
|
|
2502
|
+
const manager = new ConfigManager();
|
|
2503
|
+
|
|
2504
|
+
if (!file1) {
|
|
2505
|
+
const backups = manager.listBackups();
|
|
2506
|
+
if (backups.length === 0) {
|
|
2507
|
+
console.log('❌ No backups available');
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
file1 = path.join(manager.backupDir, backups[0].file);
|
|
2511
|
+
file2 = null; // Current config
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
const changes = manager.diff(file1, file2);
|
|
2515
|
+
if (changes.length === 0) {
|
|
2516
|
+
console.log('✅ No differences found');
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
console.log('📊 Config Diff:');
|
|
2521
|
+
console.log('');
|
|
2522
|
+
for (const change of changes) {
|
|
2523
|
+
const icon = { added: '+', removed: '-', changed: '~' }[change.type];
|
|
2524
|
+
console.log(` ${icon} ${change.path}`);
|
|
2525
|
+
if (change.type !== 'removed') {
|
|
2526
|
+
console.log(` from: ${JSON.stringify(change.from)}`);
|
|
2527
|
+
}
|
|
2528
|
+
if (change.type !== 'added') {
|
|
2529
|
+
console.log(` to: ${JSON.stringify(change.to)}`);
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2324
2532
|
}
|
|
2325
2533
|
};
|
|
2326
2534
|
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Manager
|
|
3
|
+
*
|
|
4
|
+
* Advanced configuration management with:
|
|
5
|
+
* - Automatic backup before changes
|
|
6
|
+
* - Environment variable expansion
|
|
7
|
+
* - Rollback capability
|
|
8
|
+
* - Change history tracking
|
|
9
|
+
*
|
|
10
|
+
* @module lib/config-manager
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
const { ConfigValidator } = require('./config-validator');
|
|
17
|
+
const { ConfigError } = require('./errors');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Configuration Manager class
|
|
21
|
+
*/
|
|
22
|
+
class ConfigManager {
|
|
23
|
+
/**
|
|
24
|
+
* @param {Object} options - Manager options
|
|
25
|
+
* @param {string} options.configDir - Configuration directory
|
|
26
|
+
* @param {string} options.configFile - Configuration file path
|
|
27
|
+
* @param {string} options.backupDir - Backup directory
|
|
28
|
+
* @param {number} options.maxBackups - Maximum number of backups to keep
|
|
29
|
+
* @param {boolean} options.strict - Strict validation mode
|
|
30
|
+
*/
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.configDir = options.configDir || path.join(process.env.HOME, '.claude');
|
|
33
|
+
this.configFile = options.configFile || path.join(this.configDir, 'config.json');
|
|
34
|
+
this.backupDir = options.backupDir || path.join(this.configDir, 'backups');
|
|
35
|
+
this.maxBackups = options.maxBackups || 10;
|
|
36
|
+
|
|
37
|
+
this.validator = new ConfigValidator({
|
|
38
|
+
strict: options.strict !== false
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this._ensureDirectories();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load configuration with validation
|
|
46
|
+
* @param {Object} options - Load options
|
|
47
|
+
* @returns {Object} Configuration object
|
|
48
|
+
*/
|
|
49
|
+
load(options = {}) {
|
|
50
|
+
const {
|
|
51
|
+
useDefaults = true,
|
|
52
|
+
expandEnv = true,
|
|
53
|
+
strict = null
|
|
54
|
+
} = options;
|
|
55
|
+
|
|
56
|
+
if (!fs.existsSync(this.configFile)) {
|
|
57
|
+
if (useDefaults) {
|
|
58
|
+
return this._getDefaults();
|
|
59
|
+
}
|
|
60
|
+
throw new ConfigError('Configuration file not found', [], [
|
|
61
|
+
`Create file at: ${this.configFile}`,
|
|
62
|
+
'Or run: smc init'
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const content = fs.readFileSync(this.configFile, 'utf-8');
|
|
67
|
+
let config;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
config = JSON.parse(content);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
const validation = this.validator.validateFile(this.configFile);
|
|
73
|
+
throw new ConfigError(
|
|
74
|
+
'Invalid JSON in configuration file',
|
|
75
|
+
validation.errors,
|
|
76
|
+
validation.fixes
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Expand environment variables
|
|
81
|
+
if (expandEnv) {
|
|
82
|
+
config = this._expandEnvVars(config);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Validate against schema
|
|
86
|
+
const effectiveStrict = strict !== null ? strict : this.validator.strict;
|
|
87
|
+
if (effectiveStrict) {
|
|
88
|
+
const validation = this.validator.validate(config);
|
|
89
|
+
if (!validation.valid) {
|
|
90
|
+
throw new ConfigError(
|
|
91
|
+
'Configuration validation failed',
|
|
92
|
+
validation.errors,
|
|
93
|
+
validation.fixes
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return config;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Save configuration with backup
|
|
103
|
+
* @param {Object} config - Configuration to save
|
|
104
|
+
* @param {Object} options - Save options
|
|
105
|
+
* @returns {Object} Save result
|
|
106
|
+
*/
|
|
107
|
+
save(config, options = {}) {
|
|
108
|
+
const { backup = true, validate = true } = options;
|
|
109
|
+
|
|
110
|
+
// Validate before saving
|
|
111
|
+
if (validate) {
|
|
112
|
+
const validation = this.validator.validate(config);
|
|
113
|
+
if (!validation.valid) {
|
|
114
|
+
throw new ConfigError(
|
|
115
|
+
'Cannot save invalid configuration',
|
|
116
|
+
validation.errors,
|
|
117
|
+
validation.fixes
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Create backup
|
|
123
|
+
let backupPath = null;
|
|
124
|
+
if (backup && fs.existsSync(this.configFile)) {
|
|
125
|
+
backupPath = this._createBackup();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Write new config
|
|
129
|
+
const content = JSON.stringify(config, null, 2);
|
|
130
|
+
fs.writeFileSync(this.configFile, content, 'utf-8');
|
|
131
|
+
|
|
132
|
+
// Record change
|
|
133
|
+
this._recordChange('save', {
|
|
134
|
+
hash: this._hash(content),
|
|
135
|
+
backup: backupPath
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return { success: true, backup: backupPath };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Rollback to previous configuration
|
|
143
|
+
* @param {string|null} version - Backup version (null for latest)
|
|
144
|
+
* @returns {Object} Rollback result
|
|
145
|
+
*/
|
|
146
|
+
rollback(version = null) {
|
|
147
|
+
const backups = this.listBackups();
|
|
148
|
+
|
|
149
|
+
if (backups.length === 0) {
|
|
150
|
+
throw new ConfigError('No backups available', [], [
|
|
151
|
+
'Backups are created when you save config changes',
|
|
152
|
+
'Enable backup option when saving'
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const targetBackup = version
|
|
157
|
+
? backups.find(b => b.version === version)
|
|
158
|
+
: backups[0];
|
|
159
|
+
|
|
160
|
+
if (!targetBackup) {
|
|
161
|
+
throw new ConfigError(`Backup version ${version} not found`, [], [
|
|
162
|
+
'Available versions: ' + backups.map(b => b.version).join(', ')
|
|
163
|
+
]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const backupPath = path.join(this.backupDir, targetBackup.file);
|
|
167
|
+
const content = fs.readFileSync(backupPath, 'utf-8');
|
|
168
|
+
|
|
169
|
+
// Create backup of current before rollback
|
|
170
|
+
if (fs.existsSync(this.configFile)) {
|
|
171
|
+
this._createBackup('pre-rollback');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fs.writeFileSync(this.configFile, content, 'utf-8');
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
restoredFrom: targetBackup.version,
|
|
179
|
+
timestamp: targetBackup.timestamp
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* List available backups
|
|
185
|
+
* @returns {Array} List of backup info objects
|
|
186
|
+
*/
|
|
187
|
+
listBackups() {
|
|
188
|
+
if (!fs.existsSync(this.backupDir)) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const files = fs.readdirSync(this.backupDir)
|
|
193
|
+
.filter(f => f.startsWith('config-') && f.endsWith('.json'))
|
|
194
|
+
.map(f => {
|
|
195
|
+
const stat = fs.statSync(path.join(this.backupDir, f));
|
|
196
|
+
return {
|
|
197
|
+
file: f,
|
|
198
|
+
version: f.replace('config-', '').replace('.json', ''),
|
|
199
|
+
timestamp: stat.mtime,
|
|
200
|
+
size: stat.size
|
|
201
|
+
};
|
|
202
|
+
})
|
|
203
|
+
.sort((a, b) => b.timestamp - a.timestamp)
|
|
204
|
+
.slice(0, this.maxBackups);
|
|
205
|
+
|
|
206
|
+
return files;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get configuration diff
|
|
211
|
+
* @param {string|Object} left - Left config (file path or object)
|
|
212
|
+
* @param {string|Object} right - Right config (file path or object, null for current)
|
|
213
|
+
* @returns {Array} Array of change objects
|
|
214
|
+
*/
|
|
215
|
+
diff(left, right = null) {
|
|
216
|
+
const leftConfig = typeof left === 'string'
|
|
217
|
+
? JSON.parse(fs.readFileSync(left, 'utf-8'))
|
|
218
|
+
: left;
|
|
219
|
+
|
|
220
|
+
const rightConfig = right
|
|
221
|
+
? (typeof right === 'string'
|
|
222
|
+
? JSON.parse(fs.readFileSync(right, 'utf-8'))
|
|
223
|
+
: right)
|
|
224
|
+
: this.load({ validate: false });
|
|
225
|
+
|
|
226
|
+
return this._computeDiff(leftConfig, rightConfig);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create backup of current config
|
|
231
|
+
* @param {string} suffix - Optional suffix for backup name
|
|
232
|
+
* @returns {string} Backup file path
|
|
233
|
+
*/
|
|
234
|
+
_createBackup(suffix = null) {
|
|
235
|
+
const timestamp = new Date().toISOString()
|
|
236
|
+
.replace(/[:.]/g, '-')
|
|
237
|
+
.replace('T', '_')
|
|
238
|
+
.split('.')[0];
|
|
239
|
+
|
|
240
|
+
const version = `${timestamp}${suffix ? '-' + suffix : ''}`;
|
|
241
|
+
const filename = `config-${version}.json`;
|
|
242
|
+
const backupPath = path.join(this.backupDir, filename);
|
|
243
|
+
|
|
244
|
+
fs.copyFileSync(this.configFile, backupPath);
|
|
245
|
+
|
|
246
|
+
// Clean old backups
|
|
247
|
+
this._cleanOldBackups();
|
|
248
|
+
|
|
249
|
+
return backupPath;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Remove old backups beyond maxBackups limit
|
|
254
|
+
*/
|
|
255
|
+
_cleanOldBackups() {
|
|
256
|
+
const backups = this.listBackups();
|
|
257
|
+
if (backups.length <= this.maxBackups) return;
|
|
258
|
+
|
|
259
|
+
const toRemove = backups.slice(this.maxBackups);
|
|
260
|
+
for (const backup of toRemove) {
|
|
261
|
+
fs.unlinkSync(path.join(this.backupDir, backup.file));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Expand environment variables in config values
|
|
267
|
+
* Supports ${VAR} and ${VAR:default} syntax
|
|
268
|
+
* @param {*} value - Value to expand
|
|
269
|
+
* @returns {*} Expanded value
|
|
270
|
+
*/
|
|
271
|
+
_expandEnvVars(value) {
|
|
272
|
+
if (typeof value === 'string') {
|
|
273
|
+
return value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_, name, defaultValue) => {
|
|
274
|
+
return process.env[name] !== undefined
|
|
275
|
+
? process.env[name]
|
|
276
|
+
: (defaultValue !== undefined ? defaultValue : '');
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
return value.map(item => this._expandEnvVars(item));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (value && typeof value === 'object') {
|
|
285
|
+
const result = {};
|
|
286
|
+
for (const [key, val] of Object.entries(value)) {
|
|
287
|
+
result[key] = this._expandEnvVars(val);
|
|
288
|
+
}
|
|
289
|
+
return result;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return value;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Ensure necessary directories exist
|
|
297
|
+
*/
|
|
298
|
+
_ensureDirectories() {
|
|
299
|
+
[this.configDir, this.backupDir].forEach(dir => {
|
|
300
|
+
if (!fs.existsSync(dir)) {
|
|
301
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get default configuration
|
|
308
|
+
* @returns {Object} Default config
|
|
309
|
+
*/
|
|
310
|
+
_getDefaults() {
|
|
311
|
+
const defaultsPath = path.join(__dirname, '../config/defaults.json');
|
|
312
|
+
if (fs.existsSync(defaultsPath)) {
|
|
313
|
+
return JSON.parse(fs.readFileSync(defaultsPath, 'utf-8'));
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
version: '1.0.7',
|
|
317
|
+
model: 'claude-opus-4.5',
|
|
318
|
+
agents: {
|
|
319
|
+
conductor: { role: 'Task coordination and decomposition' },
|
|
320
|
+
architect: { role: 'Architecture design and decisions' },
|
|
321
|
+
builder: { role: 'Code implementation and testing' },
|
|
322
|
+
reviewer: { role: 'Code review and quality check' },
|
|
323
|
+
librarian: { role: 'Documentation and knowledge' }
|
|
324
|
+
},
|
|
325
|
+
skills: ['anthropics/skills', 'numman-ali/n-skills'],
|
|
326
|
+
hooks: { preTask: [], postTask: [] },
|
|
327
|
+
thinkingLens: {
|
|
328
|
+
enabled: true,
|
|
329
|
+
autoSync: true,
|
|
330
|
+
syncInterval: 20
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Compute SHA256 hash of content
|
|
337
|
+
* @param {string} content - Content to hash
|
|
338
|
+
* @returns {string} Hex hash
|
|
339
|
+
*/
|
|
340
|
+
_hash(content) {
|
|
341
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Record configuration change
|
|
346
|
+
* @param {string} action - Action type
|
|
347
|
+
* @param {Object} details - Change details
|
|
348
|
+
*/
|
|
349
|
+
_recordChange(action, details = {}) {
|
|
350
|
+
const historyPath = path.join(this.configDir, 'config-history.jsonl');
|
|
351
|
+
const entry = {
|
|
352
|
+
timestamp: new Date().toISOString(),
|
|
353
|
+
action,
|
|
354
|
+
...details
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const line = JSON.stringify(entry) + '\n';
|
|
358
|
+
fs.appendFileSync(historyPath, line, 'utf-8');
|
|
359
|
+
|
|
360
|
+
// Keep only last 100 entries
|
|
361
|
+
try {
|
|
362
|
+
const history = fs.readFileSync(historyPath, 'utf-8').trim().split('\n');
|
|
363
|
+
if (history.length > 100) {
|
|
364
|
+
const recent = history.slice(-100);
|
|
365
|
+
fs.writeFileSync(historyPath, recent.join('\n') + '\n', 'utf-8');
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
// First write, ignore
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get configuration history
|
|
374
|
+
* @param {number} limit - Maximum entries to return
|
|
375
|
+
* @returns {Array} History entries
|
|
376
|
+
*/
|
|
377
|
+
getHistory(limit = 20) {
|
|
378
|
+
const historyPath = path.join(this.configDir, 'config-history.jsonl');
|
|
379
|
+
if (!fs.existsSync(historyPath)) {
|
|
380
|
+
return [];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const content = fs.readFileSync(historyPath, 'utf-8');
|
|
384
|
+
const lines = content.trim().split('\n');
|
|
385
|
+
const entries = lines
|
|
386
|
+
.slice(-limit)
|
|
387
|
+
.map(line => {
|
|
388
|
+
try {
|
|
389
|
+
return JSON.parse(line);
|
|
390
|
+
} catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
.filter(Boolean);
|
|
395
|
+
|
|
396
|
+
return entries.reverse();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Compute diff between two configs
|
|
401
|
+
* @param {Object} left - Left config
|
|
402
|
+
* @param {Object} right - Right config
|
|
403
|
+
* @returns {Array} Array of changes
|
|
404
|
+
*/
|
|
405
|
+
_computeDiff(left, right) {
|
|
406
|
+
const changes = [];
|
|
407
|
+
|
|
408
|
+
const compare = (l, r, path = '') => {
|
|
409
|
+
const allKeys = new Set([
|
|
410
|
+
...(l ? Object.keys(l) : []),
|
|
411
|
+
...(r ? Object.keys(r) : [])
|
|
412
|
+
]);
|
|
413
|
+
|
|
414
|
+
for (const key of allKeys) {
|
|
415
|
+
const keyPath = path ? `${path}.${key}` : key;
|
|
416
|
+
const lv = l?.[key];
|
|
417
|
+
const rv = r?.[key];
|
|
418
|
+
|
|
419
|
+
if (JSON.stringify(lv) !== JSON.stringify(rv)) {
|
|
420
|
+
if (typeof lv === 'object' && typeof rv === 'object' &&
|
|
421
|
+
lv !== null && rv !== null && !Array.isArray(lv) && !Array.isArray(rv)) {
|
|
422
|
+
compare(lv, rv, keyPath);
|
|
423
|
+
} else {
|
|
424
|
+
changes.push({
|
|
425
|
+
path: keyPath,
|
|
426
|
+
from: lv,
|
|
427
|
+
to: rv,
|
|
428
|
+
type: lv === undefined ? 'added' :
|
|
429
|
+
rv === undefined ? 'removed' : 'changed'
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
compare(left, right);
|
|
437
|
+
return changes;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
module.exports = { ConfigManager };
|