codeflow-hook 2.0.1 → 2.0.3

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.
@@ -1,585 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * CLI Integration Service - Phase 4
4
- *
5
- * Bridges the local CLI commands with EKG backend services.
6
- * Transforms CLI operations from local processing to backend-driven workflows.
7
- *
8
- * Key transformations:
9
- * - `codeflow index` → EKG Ingestion Service webhook simulation
10
- * - `codeflow analyze-diff` → EKG Query Service context-enhanced analysis
11
- */
12
- import axios from 'axios';
13
- import { simpleGit } from 'simple-git';
14
- import { config } from 'dotenv';
15
- import winston from 'winston';
16
- import path from 'path';
17
- import fs from 'fs';
18
- // Load environment variables
19
- config();
20
- // Configure logger
21
- const logger = winston.createLogger({
22
- level: process.env.LOG_LEVEL || 'info',
23
- format: winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()),
24
- defaultMeta: { service: 'cli-integration' },
25
- transports: [
26
- new winston.transports.Console({
27
- format: winston.format.combine(winston.format.colorize(), winston.format.simple())
28
- }),
29
- new winston.transports.File({ filename: 'cli-integration.log' })
30
- ]
31
- });
32
- /**
33
- * CLI Integration Service
34
- * Provides methods that CLI commands can call to interact with EKG backend
35
- */
36
- export class CLIIntegrationService {
37
- constructor() {
38
- this.config = {
39
- ingestionServiceUrl: process.env.INGESTION_SERVICE_URL || 'http://localhost:3000',
40
- queryServiceUrl: process.env.QUERY_SERVICE_URL || 'http://localhost:4000',
41
- timeout: parseInt(process.env.REQUEST_TIMEOUT || '30000'),
42
- retries: parseInt(process.env.REQUEST_RETRIES || '3')
43
- };
44
- this.git = simpleGit();
45
- logger.info('CLI Integration Service initialized', this.config);
46
- }
47
- /**
48
- * Index repository for EKG - equivalent to `codeflow index`
49
- *
50
- * Sends repository URL to EKG Ingestion Service for analysis and graph population
51
- */
52
- async indexRepository(options = {}) {
53
- const startTime = Date.now();
54
- try {
55
- // Get repository information
56
- const repoInfo = await this.getRepositoryInfo();
57
- if (options.dryRun) {
58
- logger.info('Dry run mode - would index repository', repoInfo);
59
- const filesToIndex = await this.getIndexableFiles(repoInfo.repositoryPath);
60
- return {
61
- success: true,
62
- message: `Dry run: Would index ${filesToIndex.length} files from ${repoInfo.fullName}`,
63
- stats: {
64
- indexedFiles: filesToIndex.length,
65
- analysisTime: Date.now() - startTime,
66
- webhookAccepted: true // Would be accepted
67
- }
68
- };
69
- }
70
- // Calculate repository ID for tracking
71
- const repositoryId = this.generateRepositoryId(repoInfo.fullName);
72
- logger.info('Indexing repository via EKG backend', {
73
- repositoryId,
74
- fullName: repoInfo.fullName,
75
- url: repoInfo.cloneUrl
76
- });
77
- // Send webhook payload to EKG Ingestion Service
78
- const webhookPayload = {
79
- action: 'repository.indexed',
80
- repository: {
81
- id: repositoryId,
82
- name: repoInfo.name,
83
- full_name: repoInfo.fullName,
84
- clone_url: repoInfo.cloneUrl,
85
- html_url: repoInfo.htmlUrl,
86
- private: repoInfo.isPrivate
87
- },
88
- sender: {
89
- login: this.getCurrentUser(),
90
- type: 'User'
91
- },
92
- installation: {
93
- id: 'cli-integration',
94
- node_id: 'cli-integration-node'
95
- },
96
- // Add PRISM analysis request
97
- prism: {
98
- includePatterns: true,
99
- includeDependencies: true,
100
- includeMetrics: true
101
- }
102
- };
103
- const response = await this.makeBackendRequest(`${this.config.ingestionServiceUrl}/webhooks/github`, webhookPayload, {
104
- 'X-GitHub-Event': 'repository.indexed',
105
- 'X-GitHub-Delivery': `delivery-cli-${repositoryId}-${Date.now()}`,
106
- 'X-Request-ID': `req-cli-index-${repositoryId}`,
107
- 'Content-Type': 'application/json'
108
- });
109
- const analysisTime = Date.now() - startTime;
110
- logger.info('Repository indexing initiated', {
111
- repositoryId,
112
- responseStatus: response.status,
113
- analysisTime
114
- });
115
- return {
116
- success: true,
117
- repositoryId,
118
- message: `Repository ${repoInfo.fullName} submitted for EKG analysis`,
119
- stats: {
120
- indexedFiles: 0, // Will be populated by backend
121
- analysisTime,
122
- webhookAccepted: response.status === 200
123
- }
124
- };
125
- }
126
- catch (error) {
127
- const errorMessage = this.formatError(error);
128
- logger.error('Repository indexing failed', { error: errorMessage });
129
- return {
130
- success: false,
131
- message: `Repository indexing failed: ${errorMessage}`,
132
- stats: {
133
- indexedFiles: 0,
134
- analysisTime: Date.now() - startTime,
135
- webhookAccepted: false
136
- }
137
- };
138
- }
139
- }
140
- /**
141
- * Analyze code diff with EKG context enhancement
142
- *
143
- * Sends diff to Query Service for EKG-enhanced analysis instead of local RAG
144
- */
145
- async analyzeDiff(diffContent, options = {}) {
146
- const startTime = Date.now();
147
- try {
148
- if (!diffContent.trim()) {
149
- return {
150
- success: true,
151
- analysis: { type: 'no-changes', message: 'No changes to analyze' },
152
- message: 'No changes to analyze'
153
- };
154
- }
155
- if (options.legacy) {
156
- // Fallback to local analysis (would integrate with existing agents)
157
- logger.warn('Legacy mode requested - falling back to local analysis');
158
- return {
159
- success: false,
160
- analysis: null,
161
- message: 'Legacy mode not yet implemented with EKG integration'
162
- };
163
- }
164
- logger.info('Analyzing diff with EKG context enhancement', {
165
- diffSize: diffContent.length,
166
- lines: diffContent.split('\n').length
167
- });
168
- // Analyze diff and extract context
169
- const diffAnalysis = this.analyzeDiffContent(diffContent);
170
- if (diffAnalysis.files.length === 0) {
171
- return {
172
- success: true,
173
- analysis: { type: 'no-relevant-changes', message: 'No code changes detected' },
174
- message: 'No code changes detected in diff'
175
- };
176
- }
177
- // Query EKG for context on affected files
178
- const ekgContext = await this.getEKGContext(diffAnalysis);
179
- // Generate enhanced analysis with EKG data
180
- const enhancedAnalysis = await this.generateEKGEnhancedAnalysis(diffAnalysis, ekgContext);
181
- const analysisTime = Date.now() - startTime;
182
- logger.info('Diff analysis completed with EKG enhancement', {
183
- affectedFiles: diffAnalysis.files.length,
184
- ekgQueries: ekgContext.queriesMade,
185
- similarReposFound: ekgContext.similarRepositories?.length || 0,
186
- analysisTime
187
- });
188
- return {
189
- success: true,
190
- analysis: enhancedAnalysis,
191
- message: 'Diff analyzed with EKG context enhancement',
192
- stats: {
193
- ekg_queries: ekgContext.queriesMade,
194
- similar_repos_found: ekgContext.similarRepositories?.length || 0,
195
- analysis_time: analysisTime
196
- }
197
- };
198
- }
199
- catch (error) {
200
- const errorMessage = this.formatError(error);
201
- logger.error('Diff analysis failed', { error: errorMessage });
202
- return {
203
- success: false,
204
- analysis: null,
205
- message: `Diff analysis failed: ${errorMessage}`,
206
- stats: {
207
- ekg_queries: 0,
208
- similar_repos_found: 0,
209
- analysis_time: Date.now() - startTime
210
- }
211
- };
212
- }
213
- }
214
- /**
215
- * Analyze diff content and extract structured information
216
- */
217
- analyzeDiffContent(diffContent) {
218
- const files = [];
219
- const lines = diffContent.split('\n');
220
- let currentFile = null;
221
- let totalAdditions = 0;
222
- let totalDeletions = 0;
223
- for (const line of lines) {
224
- if (line.startsWith('diff --git')) {
225
- // Save previous file if exists
226
- if (currentFile) {
227
- files.push(currentFile);
228
- }
229
- // Extract file path
230
- const match = line.match(/diff --git a\/(.+) b\/(.+)/);
231
- if (match && match[2]) {
232
- currentFile = {
233
- path: match[2],
234
- additions: 0,
235
- deletions: 0,
236
- isNew: false,
237
- language: this.detectLanguage(match[2])
238
- };
239
- }
240
- }
241
- else if (line.startsWith('new file mode')) {
242
- if (currentFile) {
243
- currentFile.isNew = true;
244
- }
245
- }
246
- else if (line.startsWith('+') && !line.startsWith('+++')) {
247
- if (currentFile) {
248
- currentFile.additions++;
249
- totalAdditions++;
250
- }
251
- }
252
- else if (line.startsWith('-') && !line.startsWith('---')) {
253
- if (currentFile) {
254
- currentFile.deletions++;
255
- totalDeletions++;
256
- }
257
- }
258
- }
259
- // Push final file
260
- if (currentFile) {
261
- files.push(currentFile);
262
- }
263
- const summary = `Modified ${files.length} files: +${totalAdditions} -${totalDeletions}`;
264
- return {
265
- files,
266
- totalAdditions,
267
- totalDeletions,
268
- summary
269
- };
270
- }
271
- /**
272
- * Query EKG for context on affected files
273
- */
274
- async getEKGContext(diffAnalysis) {
275
- let queriesMade = 0;
276
- try {
277
- // Get current repository information
278
- const repoInfo = await this.getRepositoryInfo();
279
- const repositoryId = this.generateRepositoryId(repoInfo.fullName);
280
- // Query repository intelligence if repository exists in EKG
281
- let repositoryIntelligence = null;
282
- try {
283
- const response = await this.makeGraphQLRequest(`
284
- query GetRepositoryIntelligence($repoId: ID!) {
285
- repositoryIntelligence(repositoryId: $repoId) {
286
- repository {
287
- id name fullName language
288
- }
289
- patterns {
290
- name type confidence category
291
- }
292
- dependencies {
293
- dependencyType currentVersion confidence
294
- }
295
- }
296
- }
297
- `, { repoId: repositoryId });
298
- repositoryIntelligence = response.data?.repositoryIntelligence;
299
- queriesMade++;
300
- }
301
- catch (error) {
302
- logger.warn('Repository not found in EKG, continuing analysis', { repositoryId });
303
- }
304
- // Find similar repositories and patterns for context
305
- let similarRepositories = [];
306
- try {
307
- const response = await this.makeGraphQLRequest(`
308
- query FindSimilarRepositories($repoId: ID!, $limit: Int) {
309
- similarRepositories(repositoryId: $repoId, limit: $limit) {
310
- repository { name fullName language }
311
- similarityScore reasons
312
- sharedPatterns sizeComparison
313
- }
314
- }
315
- `, { repoId: repositoryId, limit: 5 });
316
- similarRepositories = response.data?.similarRepositories || [];
317
- queriesMade++;
318
- }
319
- catch (error) {
320
- logger.warn('Could not fetch similar repositories', { error: this.formatError(error) });
321
- }
322
- // Get enterprise-wide patterns that might be relevant
323
- let patterns = [];
324
- try {
325
- const languages = [...new Set(diffAnalysis.files.map((f) => f.language))];
326
- const response = await this.makeGraphQLRequest(`
327
- query GetRelevantPatterns($language: String, $limit: Int) {
328
- patterns(language: $language, minConfidence: 0.7, limit: $limit) {
329
- name type category confidence observationCount
330
- }
331
- }
332
- `, { language: languages[0], limit: 10 });
333
- patterns = response.data?.patterns || [];
334
- queriesMade++;
335
- }
336
- catch (error) {
337
- logger.warn('Could not fetch patterns', { error: this.formatError(error) });
338
- }
339
- return {
340
- queriesMade,
341
- repositoryIntelligence,
342
- similarRepositories,
343
- patterns
344
- };
345
- }
346
- catch (error) {
347
- logger.error('EKG context retrieval failed', { error: this.formatError(error) });
348
- return {
349
- queriesMade,
350
- similarRepositories: [],
351
- patterns: []
352
- };
353
- }
354
- }
355
- /**
356
- * Generate enhanced analysis using EKG context
357
- */
358
- async generateEKGEnhancedAnalysis(diffAnalysis, ekgContext) {
359
- // Analyze changes with EKG context
360
- const issues = [];
361
- const recommendations = [];
362
- // Check against existing patterns
363
- if (ekgContext.patterns && ekgContext.patterns.length > 0) {
364
- for (const file of diffAnalysis.files) {
365
- const relevantPatterns = ekgContext.patterns.filter((p) => p.type === 'security' || p.type === 'architecture');
366
- if (relevantPatterns.length > 0) {
367
- recommendations.push({
368
- type: 'ekg_pattern_alignment',
369
- description: `File ${file.path} modified - consider these established patterns: ${relevantPatterns.map((p) => p.name).join(', ')}`,
370
- severity: 'info',
371
- file: file.path
372
- });
373
- }
374
- }
375
- }
376
- // Compare against similar repositories
377
- if (ekgContext.similarRepositories && ekgContext.similarRepositories.length > 0) {
378
- const similarRepoNames = ekgContext.similarRepositories.map((sr) => sr.repository.fullName);
379
- recommendations.push({
380
- type: 'similar_repositories',
381
- description: `Changes similar to patterns seen in: ${similarRepoNames.slice(0, 3).join(', ')}`,
382
- severity: 'info'
383
- });
384
- }
385
- // Add repository-specific context if available
386
- if (ekgContext.repositoryIntelligence) {
387
- const repo = ekgContext.repositoryIntelligence.repository;
388
- issues.push({
389
- type: 'repository_context',
390
- description: `Analyzing changes in repository ${repo.fullName} (${repo.language})`,
391
- severity: 'info'
392
- });
393
- }
394
- return {
395
- summary: {
396
- totalFiles: diffAnalysis.files.length,
397
- totalAdditions: diffAnalysis.totalAdditions,
398
- totalDeletions: diffAnalysis.totalDeletions,
399
- ekgEnhanced: true
400
- },
401
- files: diffAnalysis.files.map((file) => ({
402
- path: file.path,
403
- language: file.language,
404
- additions: file.additions,
405
- deletions: file.deletions,
406
- isNew: file.isNew
407
- })),
408
- issues,
409
- recommendations,
410
- ekg_context: {
411
- patterns_analyzed: ekgContext.patterns?.length || 0,
412
- similar_repositories_found: ekgContext.similarRepositories?.length || 0,
413
- repository_known: !!ekgContext.repositoryIntelligence
414
- }
415
- };
416
- }
417
- /**
418
- * Get current repository information
419
- */
420
- async getRepositoryInfo() {
421
- try {
422
- const remotes = await this.git.getRemotes(true);
423
- const originRemote = remotes.find((r) => r.name === 'origin');
424
- if (!originRemote) {
425
- throw new Error('No origin remote found');
426
- }
427
- // Parse GitHub URL
428
- const urlMatch = originRemote.refs.fetch.match(/github\.com[:/](.+?)(\.git)?$/);
429
- if (!urlMatch) {
430
- throw new Error('Remote URL is not a GitHub repository');
431
- }
432
- const fullName = urlMatch[1];
433
- if (!fullName) {
434
- throw new Error('Could not extract repository name from remote URL');
435
- }
436
- const repoParts = fullName.split('/');
437
- const repo = repoParts[1];
438
- if (!repo) {
439
- throw new Error('Could not extract repository name from full name');
440
- }
441
- return {
442
- name: repo,
443
- fullName,
444
- repositoryPath: process.cwd(),
445
- cloneUrl: `https://github.com/${fullName}.git`,
446
- htmlUrl: `https://github.com/${fullName}`,
447
- isPrivate: false // Assume public unless told otherwise
448
- };
449
- }
450
- catch (error) {
451
- throw new Error(`Could not determine repository information: ${this.formatError(error)}`);
452
- }
453
- }
454
- /**
455
- * Get list of files that would be indexed
456
- */
457
- async getIndexableFiles(repoPath) {
458
- // This mimics the logic from the original RAG indexer
459
- const keyFileExtensions = [
460
- '.js', '.ts', '.tsx', '.jsx', '.py', '.java', '.cpp', '.c', '.cs',
461
- '.php', '.rb', '.go', '.rs', '.swift', '.kt', '.scala', '.clj'
462
- ];
463
- try {
464
- const files = [];
465
- const walkDirectory = (dir, relativePath = '') => {
466
- const items = fs.readdirSync(dir);
467
- for (const item of items) {
468
- const fullPath = path.join(dir, item);
469
- const relativeFilePath = path.join(relativePath, item);
470
- const stat = fs.statSync(fullPath);
471
- // Skip common exclude patterns
472
- if (item.startsWith('.') || item === 'node_modules' || item === 'dist' ||
473
- item === 'build' || item === 'target' || item === '.git') {
474
- continue;
475
- }
476
- if (stat.isDirectory()) {
477
- walkDirectory(fullPath, relativeFilePath);
478
- }
479
- else if (stat.isFile()) {
480
- const ext = path.extname(item);
481
- if (keyFileExtensions.includes(ext)) {
482
- files.push(relativeFilePath);
483
- }
484
- }
485
- }
486
- };
487
- walkDirectory(repoPath);
488
- return files;
489
- }
490
- catch (error) {
491
- logger.warn('Could not scan repository files', { error: this.formatError(error) });
492
- return [];
493
- }
494
- }
495
- /**
496
- * Make HTTP request to backend service with retry logic
497
- */
498
- async makeBackendRequest(url, data, headers = {}) {
499
- let lastError;
500
- for (let attempt = 1; attempt <= this.config.retries; attempt++) {
501
- try {
502
- const response = await axios.post(url, data, {
503
- headers: {
504
- ...headers,
505
- 'X-Attempt': attempt.toString()
506
- },
507
- timeout: this.config.timeout
508
- });
509
- return response;
510
- }
511
- catch (error) {
512
- lastError = error;
513
- logger.warn(`Backend request attempt ${attempt} failed`, {
514
- url,
515
- attempt,
516
- error: this.formatError(error)
517
- });
518
- if (attempt < this.config.retries) {
519
- // Wait before retry (exponential backoff)
520
- await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
521
- }
522
- }
523
- }
524
- throw lastError;
525
- }
526
- /**
527
- * Make GraphQL request to Query Service
528
- */
529
- async makeGraphQLRequest(query, variables = {}) {
530
- return this.makeBackendRequest(`${this.config.queryServiceUrl}/graphql`, { query, variables }, { 'Content-Type': 'application/json' });
531
- }
532
- /**
533
- * Generate repository ID (similar to ingestion service)
534
- */
535
- generateRepositoryId(fullName) {
536
- return `${fullName.replace('/', '-')}-${Date.now().toString(36)}`;
537
- }
538
- /**
539
- * Get current user information
540
- */
541
- getCurrentUser() {
542
- return process.env.USER || process.env.USERNAME || 'anonymous';
543
- }
544
- /**
545
- * Detect language from file extension
546
- */
547
- detectLanguage(filePath) {
548
- const ext = path.extname(filePath).toLowerCase();
549
- const languageMap = {
550
- '.js': 'javascript',
551
- '.ts': 'typescript',
552
- '.tsx': 'typescript',
553
- '.jsx': 'javascript',
554
- '.py': 'python',
555
- '.java': 'java',
556
- '.cpp': 'cpp',
557
- '.c': 'c',
558
- '.cs': 'csharp',
559
- '.php': 'php',
560
- '.rb': 'ruby',
561
- '.go': 'go',
562
- '.rs': 'rust',
563
- '.swift': 'swift',
564
- '.kt': 'kotlin',
565
- '.scala': 'scala'
566
- };
567
- return languageMap[ext] || 'unknown';
568
- }
569
- /**
570
- * Format error for logging and display
571
- */
572
- formatError(error) {
573
- if (axios.isAxiosError(error)) {
574
- return `HTTP ${error.response?.status}: ${error.response?.statusText || error.message}`;
575
- }
576
- return error.message || 'Unknown error';
577
- }
578
- }
579
- // Export singleton instance for CLI use
580
- export const cliIntegrationService = new CLIIntegrationService();
581
- // Export main methods for backward compatibility
582
- export const indexProject = cliIntegrationService.indexRepository.bind(cliIntegrationService);
583
- export const analyzeDiff = cliIntegrationService.analyzeDiff.bind(cliIntegrationService);
584
- // Default export
585
- export default cliIntegrationService;
@@ -1,60 +0,0 @@
1
- import { PipelineConfig } from './types.js';
2
- /**
3
- * Pre-built pipeline configurations for common CI/CD scenarios
4
- * These can be used as templates or loaded directly for simulation
5
- */
6
- export declare class PipelineConfigManager {
7
- /**
8
- * Get a basic Node.js CI/CD pipeline configuration
9
- */
10
- static getNodeJSPipeline(): PipelineConfig;
11
- /**
12
- * Get a comprehensive enterprise pipeline with advanced features
13
- */
14
- static getEnterprisePipeline(): PipelineConfig;
15
- /**
16
- * Get a fast pipeline for development/testing scenarios
17
- */
18
- static getFastPipeline(): PipelineConfig;
19
- /**
20
- * Get a chaotic pipeline for testing error handling
21
- */
22
- static getChaoticPipeline(): PipelineConfig;
23
- /**
24
- * Get a microservices pipeline with parallel execution
25
- */
26
- static getMicroservicesPipeline(): PipelineConfig;
27
- private static createTriggerStage;
28
- private static createAiReviewStage;
29
- private static createUnitTestStage;
30
- private static createDockerBuildStage;
31
- private static createDeployStage;
32
- private static createSecurityScanStage;
33
- private static createIntegrationTestStage;
34
- private static createPerformanceTestStage;
35
- private static createSecurityAuditStage;
36
- private static createDeployStagingStage;
37
- private static createE2eTestStage;
38
- private static createDeployProductionStage;
39
- /**
40
- * Load a pipeline configuration from JSON file
41
- */
42
- static loadFromFile(filePath: string): Promise<PipelineConfig>;
43
- /**
44
- * Save a pipeline configuration to JSON file
45
- */
46
- static saveToFile(config: PipelineConfig, filePath: string): Promise<void>;
47
- /**
48
- * Get all available pipeline templates
49
- */
50
- static getAvailableTemplates(): Array<{
51
- id: string;
52
- name: string;
53
- description: string;
54
- category: string;
55
- }>;
56
- /**
57
- * Get a pipeline configuration by ID
58
- */
59
- static getPipelineById(id: string): PipelineConfig | null;
60
- }