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