sumulige-claude 1.1.0 → 1.1.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.
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Configuration Validator
3
+ *
4
+ * AJV-based configuration validation with detailed error reporting.
5
+ * Provides structured error messages, severity levels, and auto-fix suggestions.
6
+ *
7
+ * @module lib/config-validator
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { CONFIG_SCHEMA, SETTINGS_SCHEMA, QUALITY_GATE_SCHEMA } = require('./config-schema');
13
+ const { ConfigError, parseAJVErrors } = require('./errors');
14
+
15
+ // Try to load AJV, provide fallback if not available
16
+ let Ajv = null;
17
+ let addFormats = null;
18
+
19
+ try {
20
+ Ajv = require('ajv');
21
+ addFormats = require('ajv-formats');
22
+ } catch {
23
+ // AJV not installed - will use basic validation
24
+ }
25
+
26
+ /**
27
+ * Configuration Validator class
28
+ */
29
+ class ConfigValidator {
30
+ /**
31
+ * @param {Object} options - Validator options
32
+ * @param {boolean} options.strict - Strict mode (default: false)
33
+ * @param {boolean} options.allErrors - Collect all errors (default: true)
34
+ * @param {boolean} options.coerceTypes - Coerce types (default: true)
35
+ * @param {boolean} options.useDefaults - Use default values (default: false)
36
+ */
37
+ constructor(options = {}) {
38
+ this.strict = options.strict !== false;
39
+ this.allErrors = options.allErrors !== false;
40
+ this.coerceTypes = options.coerceTypes !== false;
41
+ this.useDefaults = options.useDefaults || false;
42
+
43
+ // Initialize AJV if available
44
+ if (Ajv) {
45
+ this.ajv = new Ajv({
46
+ allErrors: this.allErrors,
47
+ verbose: true,
48
+ coerceTypes: this.coerceTypes,
49
+ useDefaults: this.useDefaults,
50
+ allowUnionTypes: true,
51
+ strict: false,
52
+ removeAdditional: false // Keep additional properties
53
+ });
54
+
55
+ // Add formats if available
56
+ if (addFormats) {
57
+ addFormats(this.ajv);
58
+ }
59
+
60
+ // Compile schemas
61
+ this.configValidate = this.ajv.compile(CONFIG_SCHEMA);
62
+ this.settingsValidate = this.ajv.compile(SETTINGS_SCHEMA);
63
+ this.qualityGateValidate = this.ajv.compile(QUALITY_GATE_SCHEMA);
64
+ } else {
65
+ // Fallback: basic validation without AJV
66
+ this.configValidate = null;
67
+ this.settingsValidate = null;
68
+ this.qualityGateValidate = null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Validate configuration object
74
+ * @param {Object} config - Configuration to validate
75
+ * @param {string} schemaName - Schema name ('config' | 'settings' | 'quality-gate')
76
+ * @returns {Object} Validation result
77
+ */
78
+ validate(config, schemaName = 'config') {
79
+ // If AJV not available, do basic validation
80
+ if (!this.ajv) {
81
+ return this._basicValidate(config, schemaName);
82
+ }
83
+
84
+ const validate = this._getValidator(schemaName);
85
+
86
+ if (!validate) {
87
+ return {
88
+ valid: false,
89
+ errors: [{
90
+ path: 'schema',
91
+ message: `Unknown schema: ${schemaName}`,
92
+ severity: 'critical',
93
+ fix: `Use valid schema name: config, settings, quality-gate`
94
+ }],
95
+ warnings: [],
96
+ fixes: []
97
+ };
98
+ }
99
+
100
+ const valid = validate(config);
101
+
102
+ if (valid) {
103
+ return { valid: true, errors: [], warnings: [], fixes: [] };
104
+ }
105
+
106
+ // Process AJV errors
107
+ const result = {
108
+ valid: false,
109
+ errors: [],
110
+ warnings: [],
111
+ fixes: []
112
+ };
113
+
114
+ const processedErrors = parseAJVErrors(validate.errors);
115
+
116
+ for (const error of processedErrors) {
117
+ if (error.severity === 'warn' || error.severity === 'info') {
118
+ result.warnings.push(error);
119
+ } else {
120
+ result.errors.push(error);
121
+ }
122
+ if (error.fix) {
123
+ result.fixes.push(error.fix);
124
+ }
125
+ }
126
+
127
+ return result;
128
+ }
129
+
130
+ /**
131
+ * Validate configuration file
132
+ * @param {string} configPath - Path to config file
133
+ * @param {string} schemaName - Schema name to use
134
+ * @returns {Object} Validation result
135
+ */
136
+ validateFile(configPath, schemaName = null) {
137
+ // Auto-detect schema from filename if not provided
138
+ if (!schemaName) {
139
+ const filename = path.basename(configPath);
140
+ if (filename === 'config.json') {
141
+ schemaName = 'config';
142
+ } else if (filename === 'settings.json' || filename === 'settings.local.json') {
143
+ schemaName = 'settings';
144
+ } else if (filename === 'quality-gate.json') {
145
+ schemaName = 'quality-gate';
146
+ } else {
147
+ schemaName = 'config';
148
+ }
149
+ }
150
+
151
+ if (!fs.existsSync(configPath)) {
152
+ return {
153
+ valid: false,
154
+ errors: [{
155
+ path: configPath,
156
+ message: 'Configuration file not found',
157
+ severity: 'critical',
158
+ fix: `Create config at: ${configPath}`
159
+ }],
160
+ warnings: [],
161
+ fixes: []
162
+ };
163
+ }
164
+
165
+ try {
166
+ const content = fs.readFileSync(configPath, 'utf-8');
167
+ const config = JSON.parse(content);
168
+ return this.validate(config, schemaName);
169
+ } catch (e) {
170
+ if (e instanceof SyntaxError) {
171
+ return {
172
+ valid: false,
173
+ errors: [{
174
+ path: configPath,
175
+ message: `JSON parse error: ${e.message}`,
176
+ severity: 'critical',
177
+ fix: this._suggestJsonFix(e, configPath)
178
+ }],
179
+ warnings: [],
180
+ fixes: []
181
+ };
182
+ }
183
+ throw e;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Validate with error throwing
189
+ * @param {Object} config - Configuration to validate
190
+ * @param {string} schemaName - Schema name
191
+ * @throws {ConfigError} If validation fails
192
+ */
193
+ validateOrThrow(config, schemaName = 'config') {
194
+ const result = this.validate(config, schemaName);
195
+ if (!result.valid) {
196
+ throw new ConfigError(
197
+ 'Configuration validation failed',
198
+ result.errors,
199
+ result.fixes
200
+ );
201
+ }
202
+ return config;
203
+ }
204
+
205
+ /**
206
+ * Get validator for schema name
207
+ * @param {string} schemaName - Schema name
208
+ * @returns {Function|null} Validator function
209
+ */
210
+ _getValidator(schemaName) {
211
+ const validators = {
212
+ config: this.configValidate,
213
+ settings: this.settingsValidate,
214
+ 'quality-gate': this.qualityGateValidate
215
+ };
216
+ return validators[schemaName] || null;
217
+ }
218
+
219
+ /**
220
+ * Basic validation without AJV
221
+ * @param {Object} config - Configuration to validate
222
+ * @param {string} schemaName - Schema name
223
+ * @returns {Object} Validation result
224
+ */
225
+ _basicValidate(config, schemaName) {
226
+ const result = {
227
+ valid: true,
228
+ errors: [],
229
+ warnings: [],
230
+ fixes: []
231
+ };
232
+
233
+ // Basic type check
234
+ if (!config || typeof config !== 'object') {
235
+ result.valid = false;
236
+ result.errors.push({
237
+ path: 'root',
238
+ message: 'Configuration must be an object',
239
+ severity: 'critical',
240
+ fix: 'Ensure config is valid JSON object'
241
+ });
242
+ return result;
243
+ }
244
+
245
+ // Schema-specific basic validation
246
+ if (schemaName === 'config') {
247
+ if (!config.version) {
248
+ result.valid = false;
249
+ result.errors.push({
250
+ path: 'version',
251
+ message: 'Missing required field: version',
252
+ severity: 'critical',
253
+ fix: 'Add "version": "1.0.0" to config'
254
+ });
255
+ } else if (typeof config.version === 'string' &&
256
+ !/^\d+\.\d+\.\d+/.test(config.version)) {
257
+ result.valid = false;
258
+ result.errors.push({
259
+ path: 'version',
260
+ message: 'Invalid version format',
261
+ severity: 'error',
262
+ expected: 'X.Y.Z',
263
+ actual: config.version,
264
+ fix: 'Use semantic version format (e.g., 1.0.0)'
265
+ });
266
+ }
267
+ }
268
+
269
+ return result;
270
+ }
271
+
272
+ /**
273
+ * Suggest fix for JSON parsing errors
274
+ * @param {Error} error - JSON parse error
275
+ * @param {string} filePath - Path to file
276
+ * @returns {string} Fix suggestion
277
+ */
278
+ _suggestJsonFix(error, filePath) {
279
+ const match = error.message.match(/position (\d+)/);
280
+ if (match) {
281
+ const pos = parseInt(match[1]);
282
+ try {
283
+ const content = fs.readFileSync(filePath, 'utf-8');
284
+ const line = content.substring(0, pos).split('\n').length;
285
+ const col = pos - content.lastIndexOf('\n', pos - 1);
286
+ return `Check line ${line}, column ${col} for syntax errors (missing comma, quote, bracket, etc.)`;
287
+ } catch {
288
+ return `Check around position ${pos} for syntax errors`;
289
+ }
290
+ }
291
+ return 'Verify JSON syntax (commas, quotes, brackets are properly closed)';
292
+ }
293
+
294
+ /**
295
+ * Check if AJV is available
296
+ * @returns {boolean}
297
+ */
298
+ static isAJVAvailable() {
299
+ return Ajv !== null;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Create a default validator instance
305
+ */
306
+ const defaultValidator = new ConfigValidator();
307
+
308
+ /**
309
+ * Convenience functions using default validator
310
+ */
311
+ function validate(config, schemaName) {
312
+ return defaultValidator.validate(config, schemaName);
313
+ }
314
+
315
+ function validateFile(configPath, schemaName) {
316
+ return defaultValidator.validateFile(configPath, schemaName);
317
+ }
318
+
319
+ function validateOrThrow(config, schemaName) {
320
+ return defaultValidator.validateOrThrow(config, schemaName);
321
+ }
322
+
323
+ module.exports = {
324
+ ConfigValidator,
325
+ defaultValidator,
326
+ validate,
327
+ validateFile,
328
+ validateOrThrow,
329
+ isAJVAvailable: ConfigValidator.isAJVAvailable
330
+ };
package/lib/config.js CHANGED
@@ -2,6 +2,9 @@
2
2
  * Config - Configuration management
3
3
  *
4
4
  * Loads default config and merges with user config from ~/.claude/config.json
5
+ *
6
+ * v2.0: Supports new ConfigManager with validation, backup, and rollback.
7
+ * Enable with SMC_USE_NEW_CONFIG=1 environment variable.
5
8
  */
6
9
 
7
10
  const fs = require('fs');
@@ -11,6 +14,25 @@ const defaults = require('../config/defaults.json');
11
14
  const CONFIG_DIR = path.join(process.env.HOME, '.claude');
12
15
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
13
16
 
17
+ // Try to load new ConfigManager (v2.0)
18
+ let ConfigManager = null;
19
+ let newManager = null;
20
+
21
+ try {
22
+ ({ ConfigManager } = require('./config-manager'));
23
+ } catch {
24
+ // New system not available, use legacy
25
+ }
26
+
27
+ /**
28
+ * Check if new config system should be used
29
+ */
30
+ function useNewSystem() {
31
+ return process.env.SMC_USE_NEW_CONFIG === '1' ||
32
+ process.env.SMC_CONFIG_V2 === '1' ||
33
+ process.env.SMC_STRICT_CONFIG === '1';
34
+ }
35
+
14
36
  /**
15
37
  * Deep merge two objects
16
38
  */
@@ -31,6 +53,24 @@ function deepMerge(target, source) {
31
53
  * @returns {Object} Merged configuration
32
54
  */
33
55
  exports.loadConfig = function() {
56
+ // Use new system if enabled and available
57
+ if (useNewSystem() && ConfigManager) {
58
+ if (!newManager) {
59
+ newManager = new ConfigManager();
60
+ }
61
+ try {
62
+ return newManager.load({ expandEnv: true });
63
+ } catch (e) {
64
+ console.warn(`[Config] ${e.message}`);
65
+ if (process.env.SMC_STRICT_CONFIG === '1') {
66
+ throw e;
67
+ }
68
+ // Fall back to legacy on validation error
69
+ console.warn('[Config] Falling back to legacy config system');
70
+ }
71
+ }
72
+
73
+ // Legacy implementation
34
74
  if (fs.existsSync(CONFIG_FILE)) {
35
75
  try {
36
76
  const userConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
@@ -47,10 +87,21 @@ exports.loadConfig = function() {
47
87
  /**
48
88
  * Save configuration to file
49
89
  * @param {Object} config - Configuration to save
90
+ * @param {Object} options - Save options
50
91
  */
51
- exports.saveConfig = function(config) {
92
+ exports.saveConfig = function(config, options = {}) {
93
+ // Use new system if enabled and available
94
+ if (useNewSystem() && ConfigManager) {
95
+ if (!newManager) {
96
+ newManager = new ConfigManager();
97
+ }
98
+ return newManager.save(config, options);
99
+ }
100
+
101
+ // Legacy implementation
52
102
  exports.ensureDir(CONFIG_DIR);
53
103
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
104
+ return { success: true };
54
105
  };
55
106
 
56
107
  /**
package/lib/errors.js ADDED
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Structured Error Types
3
+ *
4
+ * Provides structured error information with recovery hints.
5
+ * All errors include:
6
+ * - Error code for programmatic handling
7
+ * - Severity level (info/warn/error/critical)
8
+ * - Details object with context
9
+ * - Hints array with recovery suggestions
10
+ * - Optional documentation URL
11
+ *
12
+ * @module lib/errors
13
+ */
14
+
15
+ /**
16
+ * Base SMC Error class
17
+ */
18
+ class SMCError extends Error {
19
+ /**
20
+ * @param {string} message - Error message
21
+ * @param {Object} options - Error options
22
+ * @param {string} options.code - Error code (e.g., 'ERR_CONFIG')
23
+ * @param {string} options.severity - Severity level: 'info' | 'warn' | 'error' | 'critical'
24
+ * @param {Object} options.details - Additional error details
25
+ * @param {string[]} options.hints - Recovery hints
26
+ * @param {string} options.docUrl - Documentation URL
27
+ */
28
+ constructor(message, options = {}) {
29
+ super(message);
30
+ this.name = this.constructor.name;
31
+ this.code = options.code || 'ERR_UNKNOWN';
32
+ this.severity = options.severity || 'error';
33
+ this.details = options.details || {};
34
+ this.hints = options.hints || [];
35
+ this.docUrl = options.docUrl;
36
+
37
+ // Maintain proper stack trace
38
+ Error.captureStackTrace?.(this, this.constructor);
39
+ }
40
+
41
+ /**
42
+ * Convert error to JSON-serializable object
43
+ */
44
+ toJSON() {
45
+ return {
46
+ name: this.name,
47
+ code: this.code,
48
+ message: this.message,
49
+ severity: this.severity,
50
+ details: this.details,
51
+ hints: this.hints,
52
+ docUrl: this.docUrl
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Format error for console output
58
+ */
59
+ toString() {
60
+ let output = `[${this.code}] ${this.message}`;
61
+ if (this.hints.length > 0) {
62
+ output += '\n\nSuggestions:\n';
63
+ this.hints.forEach((h, i) => {
64
+ output += ` ${i + 1}. ${h}\n`;
65
+ });
66
+ }
67
+ if (this.docUrl) {
68
+ output += `\nDocs: ${this.docUrl}\n`;
69
+ }
70
+ return output;
71
+ }
72
+
73
+ /**
74
+ * Check if this error has a specific severity level or higher
75
+ * @param {string} minSeverity - Minimum severity to check against
76
+ * @returns {boolean}
77
+ */
78
+ hasSeverity(minSeverity) {
79
+ const levels = { info: 0, warn: 1, error: 2, critical: 3 };
80
+ return levels[this.severity] >= levels[minSeverity];
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Configuration-related errors
86
+ * Used for config file parsing, validation, and loading issues
87
+ */
88
+ class ConfigError extends SMCError {
89
+ /**
90
+ * @param {string} message - Error message
91
+ * @param {Object[]} errors - Array of validation errors
92
+ * @param {string[]} fixes - Array of auto-fix suggestions
93
+ * @param {Object} options - Additional options passed to SMCError
94
+ */
95
+ constructor(message, errors = [], fixes = [], options = {}) {
96
+ super(message, {
97
+ code: 'ERR_CONFIG',
98
+ severity: 'critical',
99
+ ...options
100
+ });
101
+ this.errors = errors;
102
+ this.fixes = fixes;
103
+ this.details = {
104
+ errorCount: errors.length,
105
+ fixCount: fixes.length,
106
+ criticalCount: errors.filter(e => e.severity === 'critical').length,
107
+ errorCount: errors.length,
108
+ fixCount: fixes.length,
109
+ criticalCount: errors.filter(e => e.severity === 'critical').length
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Format config error for console output
115
+ */
116
+ toString() {
117
+ let output = `ConfigError: ${this.message}\n`;
118
+
119
+ if (this.details.errorCount > 0) {
120
+ output += `\nErrors (${this.details.errorCount}):\n`;
121
+ this.errors.forEach(e => {
122
+ const icon = e.severity === 'critical' ? 'X' : e.severity === 'error' ? 'E' : 'W';
123
+ output += ` [${icon}] ${e.path}: ${e.message}\n`;
124
+ if (e.fix) {
125
+ output += ` Fix: ${e.fix}\n`;
126
+ }
127
+ });
128
+ }
129
+
130
+ if (this.details.fixCount > 0) {
131
+ output += `\nSuggested fixes:\n`;
132
+ this.fixes.forEach((f, i) => {
133
+ output += ` ${i + 1}. ${f}\n`;
134
+ });
135
+ }
136
+
137
+ return output;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Validation errors
143
+ * Used when data validation fails
144
+ */
145
+ class ValidationError extends SMCError {
146
+ constructor(message, options = {}) {
147
+ super(message, {
148
+ code: 'ERR_VALIDATION',
149
+ severity: 'error',
150
+ ...options
151
+ });
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Quality gate errors
157
+ * Used when quality checks fail
158
+ */
159
+ class QualityGateError extends SMCError {
160
+ /**
161
+ * @param {string} message - Error message
162
+ * @param {Object} results - Quality check results
163
+ * @param {Object} options - Additional options
164
+ */
165
+ constructor(message, results = {}, options = {}) {
166
+ super(message, {
167
+ code: 'ERR_QUALITY_GATE',
168
+ severity: 'error',
169
+ ...options
170
+ });
171
+ this.results = results;
172
+ this.details = {
173
+ passed: results.passed || false,
174
+ total: results.summary?.total || 0,
175
+ critical: results.summary?.critical || 0,
176
+ error: results.summary?.error || 0,
177
+ warn: results.summary?.warn || 0
178
+ };
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Migration errors
184
+ * Used during config migration
185
+ */
186
+ class MigrationError extends SMCError {
187
+ constructor(message, options = {}) {
188
+ super(message, {
189
+ code: 'ERR_MIGRATION',
190
+ severity: 'critical',
191
+ ...options
192
+ });
193
+ }
194
+ }
195
+
196
+ /**
197
+ * File operation errors
198
+ * Used for file read/write issues
199
+ */
200
+ class FileError extends SMCError {
201
+ constructor(message, filePath, options = {}) {
202
+ super(message, {
203
+ code: 'ERR_FILE',
204
+ severity: 'error',
205
+ ...options
206
+ });
207
+ this.filePath = filePath;
208
+ this.details.file = filePath;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Rule execution errors
214
+ * Used when a quality rule fails to execute
215
+ */
216
+ class RuleError extends SMCError {
217
+ constructor(message, ruleId, options = {}) {
218
+ super(message, {
219
+ code: 'ERR_RULE',
220
+ severity: 'warn',
221
+ ...options
222
+ });
223
+ this.ruleId = ruleId;
224
+ this.details.rule = ruleId;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Parse error details from AJV validation output
230
+ * @param {Object[]} ajvErrors - AJV validation errors
231
+ * @returns {Object[]} Parsed error objects
232
+ */
233
+ function parseAJVErrors(ajvErrors) {
234
+ return ajvErrors.map(error => {
235
+ const path = error.instancePath || 'root';
236
+ const message = error.message || 'Validation failed';
237
+
238
+ return {
239
+ path,
240
+ message,
241
+ severity: getSeverityFromKeyword(error.keyword),
242
+ expected: error.schema?.type || error.schema?.enum?.join('|'),
243
+ actual: error.data,
244
+ keyword: error.keyword,
245
+ fix: generateFixFromError(error)
246
+ };
247
+ });
248
+ }
249
+
250
+ /**
251
+ * Map AJV keyword to severity level
252
+ * @param {string} keyword - AJV error keyword
253
+ * @returns {string} Severity level
254
+ */
255
+ function getSeverityFromKeyword(keyword) {
256
+ const severityMap = {
257
+ required: 'critical',
258
+ type: 'error',
259
+ enum: 'error',
260
+ pattern: 'warn',
261
+ format: 'warn',
262
+ minimum: 'warn',
263
+ maximum: 'warn',
264
+ minLength: 'warn',
265
+ maxLength: 'warn'
266
+ };
267
+ return severityMap[keyword] || 'warn';
268
+ }
269
+
270
+ /**
271
+ * Generate auto-fix suggestion from AJV error
272
+ * @param {Object} error - AJV error object
273
+ * @returns {string|null} Fix suggestion
274
+ */
275
+ function generateFixFromError(error) {
276
+ const fixes = {
277
+ required: `Add missing field: ${error.params?.missingProperty}`,
278
+ pattern: `Value must match pattern: ${error.schema?.pattern}`,
279
+ enum: `Value must be one of: ${error.schema?.enum?.join(', ')}`,
280
+ type: `Change type to: ${error.schema?.type}`,
281
+ minimum: `Value must be >= ${error.schema?.minimum}`,
282
+ maximum: `Value must be <= ${error.schema?.maximum}`,
283
+ minLength: `Length must be >= ${error.schema?.minLength}`,
284
+ maxLength: `Length must be <= ${error.schema?.maxLength}`
285
+ };
286
+ return fixes[error.keyword] || null;
287
+ }
288
+
289
+ module.exports = {
290
+ // Base class
291
+ SMCError,
292
+
293
+ // Specific error types
294
+ ConfigError,
295
+ ValidationError,
296
+ QualityGateError,
297
+ MigrationError,
298
+ FileError,
299
+ RuleError,
300
+
301
+ // Utility functions
302
+ parseAJVErrors,
303
+ getSeverityFromKeyword,
304
+ generateFixFromError
305
+ };