@juspay/yama 1.1.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.
@@ -0,0 +1,555 @@
1
+ "use strict";
2
+ /**
3
+ * Enhanced Configuration Manager for Yama
4
+ * Handles configuration loading, validation, and merging from multiple sources
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.configManager = exports.ConfigManager = void 0;
11
+ exports.createConfigManager = createConfigManager;
12
+ const fs_1 = __importDefault(require("fs"));
13
+ const path_1 = __importDefault(require("path"));
14
+ const yaml_1 = __importDefault(require("yaml"));
15
+ const types_1 = require("../types");
16
+ const Logger_1 = require("./Logger");
17
+ class ConfigManager {
18
+ constructor() {
19
+ this.config = null;
20
+ this.configPaths = [];
21
+ this.setupConfigPaths();
22
+ }
23
+ /**
24
+ * Setup configuration file search paths
25
+ */
26
+ setupConfigPaths() {
27
+ const cwd = process.cwd();
28
+ const homeDir = require('os').homedir();
29
+ this.configPaths = [
30
+ path_1.default.join(cwd, 'yama.config.yaml'),
31
+ path_1.default.join(cwd, 'yama.config.yml'),
32
+ path_1.default.join(cwd, 'yama.config.json'),
33
+ path_1.default.join(cwd, '.yama.yaml'),
34
+ path_1.default.join(cwd, '.yama.yml'),
35
+ path_1.default.join(cwd, '.yama.json'),
36
+ path_1.default.join(homeDir, '.yama', 'config.yaml'),
37
+ path_1.default.join(homeDir, '.yama', 'config.yml'),
38
+ path_1.default.join(homeDir, '.yama', 'config.json'),
39
+ path_1.default.join(homeDir, '.config', 'yama', 'config.yaml'),
40
+ path_1.default.join(homeDir, '.config', 'yama', 'config.yml'),
41
+ path_1.default.join(homeDir, '.config', 'yama', 'config.json')
42
+ ];
43
+ }
44
+ /**
45
+ * Load configuration from files and environment
46
+ */
47
+ async loadConfig(configPath) {
48
+ if (this.config) {
49
+ return this.config;
50
+ }
51
+ Logger_1.logger.debug('Loading Yama configuration...');
52
+ // Start with default config
53
+ let config = this.deepClone(ConfigManager.DEFAULT_CONFIG);
54
+ // If specific config path provided, use only that
55
+ if (configPath) {
56
+ if (!fs_1.default.existsSync(configPath)) {
57
+ throw new types_1.ConfigurationError(`Configuration file not found: ${configPath}`);
58
+ }
59
+ const fileConfig = await this.loadConfigFile(configPath);
60
+ config = this.mergeConfigs(config, fileConfig);
61
+ Logger_1.logger.debug(`Loaded configuration from: ${configPath}`);
62
+ }
63
+ else {
64
+ // Search for config files in predefined paths
65
+ for (const configFilePath of this.configPaths) {
66
+ if (fs_1.default.existsSync(configFilePath)) {
67
+ try {
68
+ const fileConfig = await this.loadConfigFile(configFilePath);
69
+ config = this.mergeConfigs(config, fileConfig);
70
+ Logger_1.logger.debug(`Loaded configuration from: ${configFilePath}`);
71
+ break;
72
+ }
73
+ catch (error) {
74
+ Logger_1.logger.warn(`Failed to load config from ${configFilePath}:`, error);
75
+ }
76
+ }
77
+ }
78
+ }
79
+ // Override with environment variables
80
+ config = this.applyEnvironmentOverrides(config);
81
+ // Validate configuration
82
+ this.validateConfig(config);
83
+ this.config = config;
84
+ Logger_1.logger.debug('Configuration loaded successfully');
85
+ return config;
86
+ }
87
+ /**
88
+ * Load configuration from a specific file
89
+ */
90
+ async loadConfigFile(filePath) {
91
+ try {
92
+ const content = fs_1.default.readFileSync(filePath, 'utf8');
93
+ const ext = path_1.default.extname(filePath).toLowerCase();
94
+ switch (ext) {
95
+ case '.yaml':
96
+ case '.yml':
97
+ return yaml_1.default.parse(content);
98
+ case '.json':
99
+ return JSON.parse(content);
100
+ default:
101
+ throw new types_1.ConfigurationError(`Unsupported config file format: ${ext}`);
102
+ }
103
+ }
104
+ catch (error) {
105
+ throw new types_1.ConfigurationError(`Failed to parse config file ${filePath}: ${error.message}`);
106
+ }
107
+ }
108
+ /**
109
+ * Apply environment variable overrides
110
+ */
111
+ applyEnvironmentOverrides(config) {
112
+ const env = process.env;
113
+ // AI Provider overrides
114
+ if (env.AI_PROVIDER) {
115
+ config.providers.ai.provider = env.AI_PROVIDER;
116
+ }
117
+ if (env.AI_MODEL) {
118
+ config.providers.ai.model = env.AI_MODEL;
119
+ }
120
+ if (env.AI_TIMEOUT) {
121
+ config.providers.ai.timeout = env.AI_TIMEOUT;
122
+ }
123
+ if (env.AI_TEMPERATURE) {
124
+ config.providers.ai.temperature = parseFloat(env.AI_TEMPERATURE);
125
+ }
126
+ if (env.AI_MAX_TOKENS) {
127
+ config.providers.ai.maxTokens = parseInt(env.AI_MAX_TOKENS);
128
+ }
129
+ // Git Provider overrides
130
+ if (env.BITBUCKET_USERNAME) {
131
+ config.providers.git.credentials.username = env.BITBUCKET_USERNAME;
132
+ }
133
+ if (env.BITBUCKET_TOKEN) {
134
+ config.providers.git.credentials.token = env.BITBUCKET_TOKEN;
135
+ }
136
+ if (env.BITBUCKET_BASE_URL) {
137
+ config.providers.git.credentials.baseUrl = env.BITBUCKET_BASE_URL;
138
+ }
139
+ // Feature toggles
140
+ if (env.ENABLE_CODE_REVIEW !== undefined) {
141
+ config.features.codeReview.enabled = env.ENABLE_CODE_REVIEW === 'true';
142
+ }
143
+ if (env.ENABLE_DESCRIPTION_ENHANCEMENT !== undefined) {
144
+ config.features.descriptionEnhancement.enabled = env.ENABLE_DESCRIPTION_ENHANCEMENT === 'true';
145
+ }
146
+ if (env.ENABLE_SECURITY_SCAN !== undefined) {
147
+ config.features.securityScan.enabled = env.ENABLE_SECURITY_SCAN === 'true';
148
+ }
149
+ if (env.ENABLE_ANALYTICS !== undefined) {
150
+ config.features.analytics.enabled = env.ENABLE_ANALYTICS === 'true';
151
+ }
152
+ // Cache configuration
153
+ if (env.CACHE_ENABLED !== undefined) {
154
+ config.cache.enabled = env.CACHE_ENABLED === 'true';
155
+ }
156
+ if (env.CACHE_TTL) {
157
+ config.cache.ttl = env.CACHE_TTL;
158
+ }
159
+ if (env.CACHE_STORAGE) {
160
+ config.cache.storage = env.CACHE_STORAGE;
161
+ }
162
+ // Debug mode
163
+ if (env.GUARDIAN_DEBUG === 'true') {
164
+ Logger_1.logger.setLevel('debug');
165
+ Logger_1.logger.setVerbose(true);
166
+ }
167
+ Logger_1.logger.debug('Applied environment variable overrides');
168
+ return config;
169
+ }
170
+ /**
171
+ * Validate configuration
172
+ */
173
+ validateConfig(config) {
174
+ const errors = [];
175
+ // Validate AI provider credentials
176
+ if (!config.providers.ai.provider) {
177
+ errors.push('AI provider must be specified');
178
+ }
179
+ // Validate Git provider credentials
180
+ if (!config.providers.git.credentials.username) {
181
+ errors.push('Git username must be specified');
182
+ }
183
+ if (!config.providers.git.credentials.token) {
184
+ errors.push('Git token must be specified');
185
+ }
186
+ // Validate enabled features have required configuration
187
+ if (config.features.codeReview.enabled) {
188
+ if (!config.features.codeReview.severityLevels?.length) {
189
+ errors.push('Code review severity levels must be specified when enabled');
190
+ }
191
+ }
192
+ if (config.features.descriptionEnhancement.enabled) {
193
+ if (!config.features.descriptionEnhancement.requiredSections?.length) {
194
+ errors.push('Description enhancement required sections must be specified when enabled');
195
+ }
196
+ }
197
+ // Validate cache configuration
198
+ if (config.cache?.enabled) {
199
+ if (!config.cache.storage) {
200
+ errors.push('Cache storage type must be specified when cache is enabled');
201
+ }
202
+ }
203
+ if (errors.length > 0) {
204
+ throw new types_1.ConfigurationError(`Configuration validation failed:\n${errors.map(e => ` - ${e}`).join('\n')}`);
205
+ }
206
+ Logger_1.logger.debug('Configuration validation passed');
207
+ }
208
+ /**
209
+ * Merge two configuration objects deeply
210
+ */
211
+ mergeConfigs(base, override) {
212
+ return this.deepMerge(base, override);
213
+ }
214
+ /**
215
+ * Deep merge utility
216
+ */
217
+ deepMerge(target, source) {
218
+ const result = { ...target };
219
+ for (const key in source) {
220
+ if (source[key] !== null && typeof source[key] === 'object' && !Array.isArray(source[key])) {
221
+ result[key] = this.deepMerge(target[key] || {}, source[key]);
222
+ }
223
+ else {
224
+ result[key] = source[key];
225
+ }
226
+ }
227
+ return result;
228
+ }
229
+ /**
230
+ * Deep clone utility
231
+ */
232
+ deepClone(obj) {
233
+ return JSON.parse(JSON.stringify(obj));
234
+ }
235
+ /**
236
+ * Get current configuration
237
+ */
238
+ getConfig() {
239
+ if (!this.config) {
240
+ throw new types_1.ConfigurationError('Configuration not loaded. Call loadConfig() first.');
241
+ }
242
+ return this.config;
243
+ }
244
+ /**
245
+ * Create default configuration file
246
+ */
247
+ async createDefaultConfig(outputPath) {
248
+ const defaultPath = outputPath || path_1.default.join(process.cwd(), 'yama.config.yaml');
249
+ const configContent = yaml_1.default.stringify(ConfigManager.DEFAULT_CONFIG, {
250
+ indent: 2,
251
+ lineWidth: 100
252
+ });
253
+ // Add comments to make the config file more user-friendly
254
+ const commentedConfig = this.addConfigComments(configContent);
255
+ fs_1.default.writeFileSync(defaultPath, commentedConfig, 'utf8');
256
+ Logger_1.logger.info(`Default configuration created at: ${defaultPath}`);
257
+ return defaultPath;
258
+ }
259
+ /**
260
+ * Add helpful comments to configuration file
261
+ */
262
+ addConfigComments(content) {
263
+ const header = `# Yama Configuration
264
+ # This file configures all aspects of Yama behavior
265
+ # For more information, visit: https://github.com/juspay/yama
266
+
267
+ `;
268
+ const sections = [
269
+ '# AI Provider Configuration',
270
+ '# Git Platform Configuration',
271
+ '# Feature Configuration',
272
+ '# Cache Configuration',
273
+ '# Performance Configuration',
274
+ '# Custom Rules Configuration',
275
+ '# Reporting Configuration'
276
+ ];
277
+ let commented = header + content;
278
+ // Add section comments (this is a simplified approach)
279
+ sections.forEach(section => {
280
+ const key = section.split(' ')[1].toLowerCase();
281
+ commented = commented.replace(new RegExp(`^(${key}:)`, 'm'), `${section}\n$1`);
282
+ });
283
+ return commented;
284
+ }
285
+ /**
286
+ * Validate specific configuration section
287
+ */
288
+ validateSection(section, config) {
289
+ try {
290
+ switch (section) {
291
+ case 'providers':
292
+ return this.validateProviders(config);
293
+ case 'features':
294
+ return this.validateFeatures(config);
295
+ case 'cache':
296
+ return this.validateCache(config);
297
+ default:
298
+ return true;
299
+ }
300
+ }
301
+ catch (error) {
302
+ Logger_1.logger.error(`Validation failed for section ${section}:`, error);
303
+ return false;
304
+ }
305
+ }
306
+ validateProviders(providers) {
307
+ return !!(providers?.ai?.provider && providers?.git?.credentials?.username && providers?.git?.credentials?.token);
308
+ }
309
+ validateFeatures(features) {
310
+ return !!(features && typeof features === 'object');
311
+ }
312
+ validateCache(cacheConfig) {
313
+ if (!cacheConfig?.enabled)
314
+ return true;
315
+ return !!(cacheConfig.storage && cacheConfig.ttl);
316
+ }
317
+ /**
318
+ * Get configuration schema for validation
319
+ */
320
+ getSchema() {
321
+ return {
322
+ type: 'object',
323
+ required: ['providers', 'features'],
324
+ properties: {
325
+ providers: {
326
+ type: 'object',
327
+ required: ['ai', 'git'],
328
+ properties: {
329
+ ai: {
330
+ type: 'object',
331
+ required: ['provider'],
332
+ properties: {
333
+ provider: { type: 'string' },
334
+ model: { type: 'string' },
335
+ enableFallback: { type: 'boolean' },
336
+ enableAnalytics: { type: 'boolean' },
337
+ timeout: { type: ['string', 'number'] },
338
+ temperature: { type: 'number', minimum: 0, maximum: 2 },
339
+ maxTokens: { type: 'number', minimum: 1 }
340
+ }
341
+ },
342
+ git: {
343
+ type: 'object',
344
+ required: ['platform', 'credentials'],
345
+ properties: {
346
+ platform: { type: 'string', enum: ['bitbucket', 'github', 'gitlab', 'azure-devops'] },
347
+ credentials: {
348
+ type: 'object',
349
+ required: ['username', 'token'],
350
+ properties: {
351
+ username: { type: 'string' },
352
+ token: { type: 'string' },
353
+ baseUrl: { type: 'string' }
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }
359
+ },
360
+ features: {
361
+ type: 'object',
362
+ required: ['codeReview', 'descriptionEnhancement'],
363
+ properties: {
364
+ codeReview: {
365
+ type: 'object',
366
+ required: ['enabled'],
367
+ properties: {
368
+ enabled: { type: 'boolean' },
369
+ severityLevels: { type: 'array', items: { type: 'string' } },
370
+ categories: { type: 'array', items: { type: 'string' } },
371
+ excludePatterns: { type: 'array', items: { type: 'string' } }
372
+ }
373
+ },
374
+ descriptionEnhancement: {
375
+ type: 'object',
376
+ required: ['enabled'],
377
+ properties: {
378
+ enabled: { type: 'boolean' },
379
+ preserveContent: { type: 'boolean' },
380
+ requiredSections: { type: 'array' },
381
+ autoFormat: { type: 'boolean' }
382
+ }
383
+ }
384
+ }
385
+ }
386
+ }
387
+ };
388
+ }
389
+ /**
390
+ * Find the first available configuration file
391
+ */
392
+ findConfigFile() {
393
+ for (const configPath of this.configPaths) {
394
+ if (fs_1.default.existsSync(configPath)) {
395
+ return configPath;
396
+ }
397
+ }
398
+ return null;
399
+ }
400
+ /**
401
+ * Watch configuration file for changes and reload automatically
402
+ */
403
+ watchConfig(callback) {
404
+ if (!this.configPaths.length) {
405
+ Logger_1.logger.warn('No configuration file found to watch');
406
+ return () => { };
407
+ }
408
+ const configPath = this.findConfigFile();
409
+ if (!configPath) {
410
+ Logger_1.logger.warn('No configuration file found to watch');
411
+ return () => { };
412
+ }
413
+ Logger_1.logger.debug(`Watching configuration file: ${configPath}`);
414
+ fs_1.default.watchFile(configPath, { interval: 1000 }, async () => {
415
+ try {
416
+ Logger_1.logger.info('Configuration file changed, reloading...');
417
+ const newConfig = await this.loadConfig();
418
+ if (callback) {
419
+ callback(newConfig);
420
+ }
421
+ Logger_1.logger.success('Configuration reloaded successfully');
422
+ }
423
+ catch (error) {
424
+ Logger_1.logger.error(`Failed to reload configuration: ${error.message}`);
425
+ }
426
+ });
427
+ // Return cleanup function
428
+ return () => {
429
+ fs_1.default.unwatchFile(configPath);
430
+ Logger_1.logger.debug('Stopped watching configuration file');
431
+ };
432
+ }
433
+ /**
434
+ * Enable hot-reload for configuration
435
+ */
436
+ enableHotReload(callback) {
437
+ return this.watchConfig(callback);
438
+ }
439
+ }
440
+ exports.ConfigManager = ConfigManager;
441
+ /**
442
+ * Default configuration
443
+ */
444
+ ConfigManager.DEFAULT_CONFIG = {
445
+ providers: {
446
+ ai: {
447
+ provider: 'auto',
448
+ enableFallback: true,
449
+ enableAnalytics: false,
450
+ enableEvaluation: false,
451
+ timeout: '10m',
452
+ retryAttempts: 3,
453
+ temperature: 0.3,
454
+ maxTokens: 1000000
455
+ },
456
+ git: {
457
+ platform: 'bitbucket',
458
+ credentials: {
459
+ username: process.env.BITBUCKET_USERNAME || '',
460
+ token: process.env.BITBUCKET_TOKEN || '',
461
+ baseUrl: process.env.BITBUCKET_BASE_URL || 'https://your-bitbucket-server.com'
462
+ }
463
+ }
464
+ },
465
+ features: {
466
+ codeReview: {
467
+ enabled: true,
468
+ severityLevels: ['CRITICAL', 'MAJOR', 'MINOR', 'SUGGESTION'],
469
+ categories: ['security', 'performance', 'maintainability', 'functionality', 'error_handling', 'testing'],
470
+ excludePatterns: ['*.lock', '*.svg', '*.min.js', '*.map'],
471
+ contextLines: 3,
472
+ systemPrompt: 'You are an Expert Security Code Reviewer for enterprise applications. Your role is to:\n\n๐Ÿ”’ SECURITY FIRST: Prioritize security vulnerabilities and data protection\nโšก PERFORMANCE AWARE: Identify performance bottlenecks and optimization opportunities\n๐Ÿ—๏ธ QUALITY FOCUSED: Ensure maintainable, readable, and robust code\n๐Ÿ›ก๏ธ ERROR RESILIENT: Verify comprehensive error handling and edge cases\n\nYou provide actionable, educational feedback with specific examples and solutions.\nFocus on critical issues that could impact production systems.',
473
+ focusAreas: ['๐Ÿ”’ Security Analysis (CRITICAL PRIORITY)', 'โšก Performance Review', '๐Ÿ—๏ธ Code Quality', '๐Ÿงช Testing & Error Handling']
474
+ },
475
+ descriptionEnhancement: {
476
+ enabled: true,
477
+ preserveContent: true,
478
+ requiredSections: [
479
+ { key: 'changelog', name: 'Changelog (Modules Modified)', required: true },
480
+ { key: 'testcases', name: 'Test Cases (What to be tested)', required: true },
481
+ { key: 'config_changes', name: 'CAC Config Or Service Config Changes', required: true }
482
+ ],
483
+ autoFormat: true,
484
+ systemPrompt: 'You are an Expert Technical Writer specializing in pull request documentation. Your role is to:\n\n๐Ÿ“ CLARITY FIRST: Create clear, comprehensive PR descriptions that help reviewers understand the changes\n๐ŸŽฅ STORY TELLING: Explain the \'why\' behind changes, not just the \'what\'\n๐Ÿ“ˆ STRUCTURED: Follow consistent formatting with required sections\n๐Ÿ”— CONTEXTUAL: Link changes to business value and technical rationale\n\nCRITICAL INSTRUCTION: Return ONLY the enhanced PR description content as clean markdown. Do NOT include any meta-commentary, explanations about what you\'re doing, or introductory text like "I will enhance..." or "Here is the enhanced description:". \n\nOutput the enhanced description directly without any wrapper text or explanations.',
485
+ outputTemplate: '# PR Title Enhancement (if needed)\n\n## Summary\n[Clear overview of what this PR accomplishes]\n\n## Changes Made\n[Specific technical changes - be precise]\n\n## Testing\n[How the changes were tested]\n\n## Impact\n[Business/technical impact and considerations]\n\n## Additional Notes\n[Any deployment notes, follow-ups, or special considerations]',
486
+ enhancementInstructions: 'Return ONLY the enhanced PR description as clean markdown. Do NOT include any explanatory text, meta-commentary, or phrases like "Here is the enhanced description:" or "I will enhance...".\n\nStart directly with the enhanced description content using this structure:'
487
+ },
488
+ securityScan: {
489
+ enabled: true,
490
+ level: 'strict',
491
+ scanTypes: ['secrets', 'vulnerabilities', 'dependencies']
492
+ },
493
+ analytics: {
494
+ enabled: true,
495
+ trackMetrics: true,
496
+ exportFormat: 'json'
497
+ }
498
+ },
499
+ cache: {
500
+ enabled: true,
501
+ ttl: '1h',
502
+ maxSize: '100MB',
503
+ storage: 'memory'
504
+ },
505
+ performance: {
506
+ batch: {
507
+ enabled: true,
508
+ maxConcurrent: 5,
509
+ delayBetween: '1s'
510
+ },
511
+ optimization: {
512
+ reuseConnections: true,
513
+ compressRequests: true,
514
+ enableHttp2: true
515
+ }
516
+ },
517
+ rules: {
518
+ security: [
519
+ {
520
+ name: 'No hardcoded secrets',
521
+ pattern: '(password|secret|key|token)\\s*[=:]\\s*[\'"][^\'"]{8,}[\'"]',
522
+ severity: 'CRITICAL',
523
+ message: 'Hardcoded secrets detected',
524
+ suggestion: 'Use environment variables or secure configuration management'
525
+ }
526
+ ],
527
+ performance: [
528
+ {
529
+ name: 'Avoid N+1 queries',
530
+ pattern: 'for.*\\.(find|get|query|select)',
531
+ severity: 'MAJOR',
532
+ message: 'Potential N+1 query pattern detected',
533
+ suggestion: 'Consider using batch queries or joins'
534
+ }
535
+ ]
536
+ },
537
+ reporting: {
538
+ formats: ['markdown', 'json'],
539
+ includeAnalytics: true,
540
+ includeMetrics: true
541
+ },
542
+ monitoring: {
543
+ enabled: true,
544
+ metrics: ['performance', 'cache', 'api_calls'],
545
+ exportFormat: 'json',
546
+ interval: '5m'
547
+ }
548
+ };
549
+ // Export singleton instance
550
+ exports.configManager = new ConfigManager();
551
+ // Export factory function
552
+ function createConfigManager() {
553
+ return new ConfigManager();
554
+ }
555
+ //# sourceMappingURL=ConfigManager.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Enhanced Logger utility - Optimized from both pr-police.js and pr-describe.js
3
+ * Provides consistent logging across all Guardian operations
4
+ */
5
+ import { Logger as ILogger, LogLevel, LoggerOptions } from '../types';
6
+ export declare class Logger implements ILogger {
7
+ private options;
8
+ constructor(options?: Partial<LoggerOptions>);
9
+ private shouldLog;
10
+ private formatMessage;
11
+ private colorize;
12
+ debug(message: string, ...args: any[]): void;
13
+ info(message: string, ...args: any[]): void;
14
+ warn(message: string, ...args: any[]): void;
15
+ error(message: string, ...args: any[]): void;
16
+ badge(): void;
17
+ phase(message: string): void;
18
+ success(message: string): void;
19
+ operation(operation: string, status: 'started' | 'completed' | 'failed'): void;
20
+ violation(severity: string, message: string, file?: string): void;
21
+ progress(current: number, total: number, operation: string): void;
22
+ private createProgressBar;
23
+ child(context: Record<string, any>): Logger;
24
+ setLevel(level: LogLevel): void;
25
+ setVerbose(verbose: boolean): void;
26
+ getConfig(): LoggerOptions;
27
+ }
28
+ export declare const logger: Logger;
29
+ export declare function createLogger(options?: Partial<LoggerOptions>): Logger;
30
+ //# sourceMappingURL=Logger.d.ts.map