archicore 0.3.6 → 0.3.8
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/dist/cli/commands/interactive.js +220 -1
- package/dist/github/github-service.d.ts +4 -4
- package/dist/github/github-service.js +29 -13
- package/dist/gitlab/gitlab-service.js +48 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +19 -9
- package/dist/orchestrator/index.js +72 -7
- package/dist/server/index.js +68 -1
- package/dist/server/routes/api.js +242 -2
- package/dist/server/routes/auth.d.ts +1 -0
- package/dist/server/routes/auth.js +11 -3
- package/dist/server/routes/gitlab.js +18 -46
- package/dist/server/routes/report-issue.js +103 -0
- package/dist/server/routes/upload.js +3 -3
- package/dist/server/services/encryption.js +20 -4
- package/dist/server/services/enterprise-indexer.d.ts +116 -0
- package/dist/server/services/enterprise-indexer.js +529 -0
- package/dist/server/services/project-service.js +14 -5
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/project-analyzer.d.ts +13 -0
- package/dist/utils/project-analyzer.js +130 -0
- package/package.json +1 -1
|
@@ -25,6 +25,8 @@ const COMMANDS = [
|
|
|
25
25
|
{ name: 'rules', aliases: [], description: 'Check architectural rules' },
|
|
26
26
|
{ name: 'docs', aliases: ['documentation'], description: 'Generate architecture documentation' },
|
|
27
27
|
{ name: 'export', aliases: [], description: 'Export analysis results' },
|
|
28
|
+
{ name: 'enterprise', aliases: ['ent'], description: 'Enterprise analysis for large projects (50K+ files)' },
|
|
29
|
+
{ name: 'estimate', aliases: ['est'], description: 'Estimate project size and recommended tier' },
|
|
28
30
|
{ name: 'history', aliases: ['hist'], description: 'View and search conversation history' },
|
|
29
31
|
{ name: 'resume', aliases: [], description: 'Resume previous conversation session' },
|
|
30
32
|
{ name: 'status', aliases: [], description: 'Show connection and project status' },
|
|
@@ -532,6 +534,14 @@ async function handleCommand(input) {
|
|
|
532
534
|
case 'resume':
|
|
533
535
|
await handleResumeCommand(args);
|
|
534
536
|
break;
|
|
537
|
+
case 'enterprise':
|
|
538
|
+
case 'ent':
|
|
539
|
+
await handleEnterpriseCommand(args);
|
|
540
|
+
break;
|
|
541
|
+
case 'estimate':
|
|
542
|
+
case 'est':
|
|
543
|
+
await handleEstimateCommand();
|
|
544
|
+
break;
|
|
535
545
|
default:
|
|
536
546
|
printFormattedError(`Unknown command: /${command}`, {
|
|
537
547
|
suggestion: 'Use /help to see available commands',
|
|
@@ -1321,8 +1331,11 @@ async function handleDocsCommand(args) {
|
|
|
1321
1331
|
printInfo('Use /index first');
|
|
1322
1332
|
return;
|
|
1323
1333
|
}
|
|
1334
|
+
const pathModule = await import('path');
|
|
1324
1335
|
const format = args[0] || 'markdown';
|
|
1325
|
-
const
|
|
1336
|
+
const filename = `ARCHITECTURE.${format === 'markdown' ? 'md' : format}`;
|
|
1337
|
+
// Save to project root by default, or custom path if specified
|
|
1338
|
+
const output = args[1] || pathModule.default.join(state.projectPath, filename);
|
|
1326
1339
|
const spinner = createSpinner('Generating documentation...').start();
|
|
1327
1340
|
try {
|
|
1328
1341
|
const config = await loadConfig();
|
|
@@ -1339,6 +1352,7 @@ async function handleDocsCommand(args) {
|
|
|
1339
1352
|
spinner.succeed('Documentation generated');
|
|
1340
1353
|
printKeyValue('Output', output);
|
|
1341
1354
|
printKeyValue('Format', format);
|
|
1355
|
+
printInfo(`Documentation saved to project root: ${filename}`);
|
|
1342
1356
|
showTokenUsage();
|
|
1343
1357
|
}
|
|
1344
1358
|
catch (error) {
|
|
@@ -1510,4 +1524,209 @@ async function handleResumeCommand(args) {
|
|
|
1510
1524
|
printError(String(error));
|
|
1511
1525
|
}
|
|
1512
1526
|
}
|
|
1527
|
+
async function handleEstimateCommand() {
|
|
1528
|
+
if (!state.projectId) {
|
|
1529
|
+
printError('No project indexed');
|
|
1530
|
+
printInfo('Use /index first to register the project');
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const spinner = createSpinner('Analyzing project size...').start();
|
|
1534
|
+
try {
|
|
1535
|
+
const config = await loadConfig();
|
|
1536
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/enterprise/estimate`);
|
|
1537
|
+
if (!response.ok) {
|
|
1538
|
+
spinner.fail('Failed to estimate project');
|
|
1539
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1540
|
+
printError(errorData.error || 'Unknown error');
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const data = await response.json();
|
|
1544
|
+
spinner.succeed('Project analyzed');
|
|
1545
|
+
console.log();
|
|
1546
|
+
printSection('Project Size Estimation');
|
|
1547
|
+
printKeyValue('Total Files', data.totalFiles?.toLocaleString() || 'N/A');
|
|
1548
|
+
printKeyValue('Total Size', `${data.totalSizeMB?.toFixed(1) || 'N/A'} MB`);
|
|
1549
|
+
printKeyValue('Recommended Tier', colors.primary(data.recommendation?.toUpperCase() || 'STANDARD'));
|
|
1550
|
+
printKeyValue('Estimated Time', `~${data.estimatedTimeMinutes || 'N/A'} minutes`);
|
|
1551
|
+
// Language distribution
|
|
1552
|
+
if (data.languageDistribution && Object.keys(data.languageDistribution).length > 0) {
|
|
1553
|
+
console.log();
|
|
1554
|
+
console.log(colors.highlight(' Language Distribution:'));
|
|
1555
|
+
const sorted = Object.entries(data.languageDistribution)
|
|
1556
|
+
.sort((a, b) => b[1] - a[1])
|
|
1557
|
+
.slice(0, 8);
|
|
1558
|
+
for (const [lang, count] of sorted) {
|
|
1559
|
+
const percent = data.totalFiles ? ((count / data.totalFiles) * 100).toFixed(1) : '?';
|
|
1560
|
+
console.log(` ${lang.padEnd(12)} ${colors.muted(count.toLocaleString().padStart(6))} (${percent}%)`);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
// Tier recommendations
|
|
1564
|
+
if (data.tiers) {
|
|
1565
|
+
console.log();
|
|
1566
|
+
console.log(colors.highlight(' Available Tiers:'));
|
|
1567
|
+
console.log(` ${colors.success('quick')} - ${data.tiers.quick.estimatedFiles.toLocaleString()} files - Fast overview`);
|
|
1568
|
+
console.log(` ${colors.warning('standard')} - ${data.tiers.standard.estimatedFiles.toLocaleString()} files - Balanced analysis`);
|
|
1569
|
+
console.log(` ${colors.error('deep')} - ${data.tiers.deep.estimatedFiles.toLocaleString()} files - Full analysis (Enterprise)`);
|
|
1570
|
+
}
|
|
1571
|
+
console.log();
|
|
1572
|
+
printInfo('Use /enterprise [tier] to start analysis');
|
|
1573
|
+
printInfo('Example: /enterprise quick or /enterprise standard');
|
|
1574
|
+
}
|
|
1575
|
+
catch (error) {
|
|
1576
|
+
spinner.fail('Failed to estimate project');
|
|
1577
|
+
printError(String(error));
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
async function handleEnterpriseCommand(args) {
|
|
1581
|
+
if (!state.projectId) {
|
|
1582
|
+
printError('No project indexed');
|
|
1583
|
+
printInfo('Use /index first to register the project');
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
const tier = args[0]?.toLowerCase() || 'standard';
|
|
1587
|
+
const strategy = args[1] || 'smart';
|
|
1588
|
+
if (!['quick', 'standard', 'deep'].includes(tier)) {
|
|
1589
|
+
printError(`Invalid tier: ${tier}`);
|
|
1590
|
+
printInfo('Available tiers: quick, standard, deep');
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
// First, get estimate to show what we're doing
|
|
1594
|
+
const estimateSpinner = createSpinner('Analyzing project...').start();
|
|
1595
|
+
try {
|
|
1596
|
+
const config = await loadConfig();
|
|
1597
|
+
// Get estimate first
|
|
1598
|
+
const estimateResponse = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/enterprise/estimate`);
|
|
1599
|
+
if (!estimateResponse.ok) {
|
|
1600
|
+
estimateSpinner.fail('Failed to analyze project');
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
const estimate = await estimateResponse.json();
|
|
1604
|
+
estimateSpinner.succeed(`Project has ${estimate.totalFiles?.toLocaleString()} files`);
|
|
1605
|
+
// Determine files to analyze
|
|
1606
|
+
const tierConfig = estimate.tiers?.[tier];
|
|
1607
|
+
const filesToAnalyze = tierConfig?.estimatedFiles || estimate.totalFiles || 0;
|
|
1608
|
+
console.log();
|
|
1609
|
+
console.log(` ${colors.highlight('Enterprise Analysis Configuration:')}`);
|
|
1610
|
+
console.log(` Tier: ${colors.primary(tier.toUpperCase())}`);
|
|
1611
|
+
console.log(` Strategy: ${colors.muted(strategy)}`);
|
|
1612
|
+
console.log(` Files: ${colors.muted(filesToAnalyze.toLocaleString())} of ${estimate.totalFiles?.toLocaleString()}`);
|
|
1613
|
+
console.log();
|
|
1614
|
+
// Confirm for large projects
|
|
1615
|
+
if ((estimate.totalFiles || 0) > 10000 && tier !== 'quick') {
|
|
1616
|
+
const confirmed = await promptYesNo(`This is a large project (${estimate.totalFiles?.toLocaleString()} files). Continue with ${tier} analysis?`);
|
|
1617
|
+
if (!confirmed) {
|
|
1618
|
+
printInfo('Cancelled. Use /enterprise quick for faster analysis.');
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
// Start enterprise indexing
|
|
1623
|
+
const indexSpinner = createSpinner(`Starting ${tier} analysis with ${strategy} sampling...`).start();
|
|
1624
|
+
// For CLI, we do local indexing with enterprise options
|
|
1625
|
+
const { CodeIndex } = await import('../../code-index/index.js');
|
|
1626
|
+
const { EnterpriseIndexer } = await import('../../server/services/enterprise-indexer.js');
|
|
1627
|
+
const fs = await import('fs/promises');
|
|
1628
|
+
const pathModule = await import('path');
|
|
1629
|
+
// Initialize enterprise indexer
|
|
1630
|
+
const enterpriseIndexer = new EnterpriseIndexer(state.projectPath, {
|
|
1631
|
+
tier,
|
|
1632
|
+
sampling: {
|
|
1633
|
+
enabled: true,
|
|
1634
|
+
maxFiles: tierConfig?.maxFiles || 5000,
|
|
1635
|
+
strategy: strategy,
|
|
1636
|
+
},
|
|
1637
|
+
});
|
|
1638
|
+
await enterpriseIndexer.initialize();
|
|
1639
|
+
indexSpinner.update('Selecting files to analyze...');
|
|
1640
|
+
const filesToIndex = await enterpriseIndexer.getFilesToIndex();
|
|
1641
|
+
indexSpinner.update(`Parsing ${filesToIndex.length} selected files...`);
|
|
1642
|
+
// Parse only selected files
|
|
1643
|
+
const codeIndex = new CodeIndex(state.projectPath);
|
|
1644
|
+
const asts = new Map();
|
|
1645
|
+
let parsed = 0;
|
|
1646
|
+
for (const filePath of filesToIndex) {
|
|
1647
|
+
try {
|
|
1648
|
+
const { ASTParser } = await import('../../code-index/ast-parser.js');
|
|
1649
|
+
const parser = new ASTParser();
|
|
1650
|
+
const ast = await parser.parseFile(filePath);
|
|
1651
|
+
if (ast) {
|
|
1652
|
+
asts.set(filePath, ast);
|
|
1653
|
+
parsed++;
|
|
1654
|
+
if (parsed % 100 === 0) {
|
|
1655
|
+
indexSpinner.update(`Parsed ${parsed}/${filesToIndex.length} files...`);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
catch {
|
|
1660
|
+
// Skip files that can't be parsed
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
indexSpinner.update(`Extracting symbols from ${asts.size} files...`);
|
|
1664
|
+
// Extract symbols
|
|
1665
|
+
const symbols = codeIndex.extractSymbols(asts);
|
|
1666
|
+
indexSpinner.update(`Found ${symbols.size} symbols, building graph...`);
|
|
1667
|
+
// Build dependency graph
|
|
1668
|
+
const graph = codeIndex.buildDependencyGraph(asts, symbols);
|
|
1669
|
+
// Read file contents (only for smaller selections)
|
|
1670
|
+
let fileContents = [];
|
|
1671
|
+
if (filesToIndex.length <= 2000) {
|
|
1672
|
+
indexSpinner.update('Reading file contents...');
|
|
1673
|
+
for (const [filePath] of asts) {
|
|
1674
|
+
try {
|
|
1675
|
+
const fullPath = pathModule.default.isAbsolute(filePath)
|
|
1676
|
+
? filePath
|
|
1677
|
+
: pathModule.default.join(state.projectPath, filePath);
|
|
1678
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
1679
|
+
fileContents.push([filePath, content]);
|
|
1680
|
+
}
|
|
1681
|
+
catch {
|
|
1682
|
+
// Skip unreadable files
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
// Prepare data for upload
|
|
1687
|
+
const astsArray = Array.from(asts.entries());
|
|
1688
|
+
const symbolsArray = Array.from(symbols.entries());
|
|
1689
|
+
const graphData = {
|
|
1690
|
+
nodes: Array.from(graph.nodes.entries()),
|
|
1691
|
+
edges: Array.from(graph.edges.entries()),
|
|
1692
|
+
};
|
|
1693
|
+
const indexData = {
|
|
1694
|
+
asts: astsArray,
|
|
1695
|
+
symbols: symbolsArray,
|
|
1696
|
+
graph: graphData,
|
|
1697
|
+
fileContents,
|
|
1698
|
+
statistics: {
|
|
1699
|
+
totalFiles: asts.size,
|
|
1700
|
+
totalSymbols: symbols.size,
|
|
1701
|
+
},
|
|
1702
|
+
};
|
|
1703
|
+
// Upload to server
|
|
1704
|
+
indexSpinner.update('Uploading analysis data to server...');
|
|
1705
|
+
const uploadResult = await uploadIndexData(state.projectId, indexData, (progress) => {
|
|
1706
|
+
indexSpinner.update(progress.message);
|
|
1707
|
+
});
|
|
1708
|
+
if (!uploadResult.success) {
|
|
1709
|
+
indexSpinner.fail('Failed to upload analysis');
|
|
1710
|
+
if (uploadResult.errorDetails) {
|
|
1711
|
+
printError(uploadResult.errorDetails.message);
|
|
1712
|
+
printInfo(uploadResult.errorDetails.suggestion);
|
|
1713
|
+
}
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
indexSpinner.succeed(`Enterprise analysis complete!`);
|
|
1717
|
+
console.log();
|
|
1718
|
+
printSection('Analysis Summary');
|
|
1719
|
+
printKeyValue('Tier', tier.toUpperCase());
|
|
1720
|
+
printKeyValue('Strategy', strategy);
|
|
1721
|
+
printKeyValue('Files Analyzed', asts.size.toLocaleString());
|
|
1722
|
+
printKeyValue('Symbols Found', symbols.size.toLocaleString());
|
|
1723
|
+
printKeyValue('Graph Nodes', graph.nodes.size.toLocaleString());
|
|
1724
|
+
console.log();
|
|
1725
|
+
printSuccess('Project is ready for queries!');
|
|
1726
|
+
printInfo('Try: /search "authentication" or /metrics or /security');
|
|
1727
|
+
}
|
|
1728
|
+
catch (error) {
|
|
1729
|
+
printError(`Enterprise analysis failed: ${error}`);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1513
1732
|
//# sourceMappingURL=interactive.js.map
|
|
@@ -9,11 +9,11 @@ export declare class GitHubService {
|
|
|
9
9
|
private connections;
|
|
10
10
|
private repositories;
|
|
11
11
|
private initialized;
|
|
12
|
-
private clientId;
|
|
13
|
-
private clientSecret;
|
|
14
|
-
private redirectUri;
|
|
15
|
-
private webhookBaseUrl;
|
|
16
12
|
constructor(dataDir?: string);
|
|
13
|
+
private get clientId();
|
|
14
|
+
private get clientSecret();
|
|
15
|
+
private get redirectUri();
|
|
16
|
+
private get webhookBaseUrl();
|
|
17
17
|
private ensureInitialized;
|
|
18
18
|
private saveConnections;
|
|
19
19
|
private saveRepositories;
|
|
@@ -10,8 +10,19 @@ import { Logger } from '../utils/logger.js';
|
|
|
10
10
|
const DATA_DIR = '.archicore';
|
|
11
11
|
const CONNECTIONS_FILE = 'github-connections.json';
|
|
12
12
|
const REPOSITORIES_FILE = 'github-repositories.json';
|
|
13
|
-
// Encryption key
|
|
14
|
-
|
|
13
|
+
// Encryption key - REQUIRED in production
|
|
14
|
+
function getEncryptionKey() {
|
|
15
|
+
const key = process.env.GITHUB_ENCRYPTION_KEY || process.env.ENCRYPTION_KEY;
|
|
16
|
+
if (!key) {
|
|
17
|
+
if (process.env.NODE_ENV === 'production') {
|
|
18
|
+
throw new Error('ENCRYPTION_KEY or GITHUB_ENCRYPTION_KEY is required in production');
|
|
19
|
+
}
|
|
20
|
+
// Dev-only fallback (logs warning)
|
|
21
|
+
console.warn('WARNING: Using dev-only encryption key. Set ENCRYPTION_KEY in production.');
|
|
22
|
+
return 'dev-only-github-key-32bytes!!!';
|
|
23
|
+
}
|
|
24
|
+
return key;
|
|
25
|
+
}
|
|
15
26
|
// Singleton instance
|
|
16
27
|
let instance = null;
|
|
17
28
|
export class GitHubService {
|
|
@@ -19,22 +30,27 @@ export class GitHubService {
|
|
|
19
30
|
connections = [];
|
|
20
31
|
repositories = [];
|
|
21
32
|
initialized = false;
|
|
22
|
-
clientId;
|
|
23
|
-
clientSecret;
|
|
24
|
-
redirectUri;
|
|
25
|
-
webhookBaseUrl;
|
|
26
33
|
constructor(dataDir = DATA_DIR) {
|
|
27
34
|
// Singleton pattern - return existing instance
|
|
28
35
|
if (instance) {
|
|
29
36
|
return instance;
|
|
30
37
|
}
|
|
31
38
|
this.dataDir = dataDir;
|
|
32
|
-
this.clientId = process.env.GITHUB_CLIENT_ID || '';
|
|
33
|
-
this.clientSecret = process.env.GITHUB_CLIENT_SECRET || '';
|
|
34
|
-
this.redirectUri = process.env.GITHUB_REDIRECT_URI || 'http://localhost:3000/api/github/callback';
|
|
35
|
-
this.webhookBaseUrl = process.env.ARCHICORE_WEBHOOK_URL || 'http://localhost:3000/api/github/webhook';
|
|
36
39
|
instance = this;
|
|
37
40
|
}
|
|
41
|
+
// Lazy getters for environment variables (read at runtime, not at module load)
|
|
42
|
+
get clientId() {
|
|
43
|
+
return process.env.GITHUB_CLIENT_ID || '';
|
|
44
|
+
}
|
|
45
|
+
get clientSecret() {
|
|
46
|
+
return process.env.GITHUB_CLIENT_SECRET || '';
|
|
47
|
+
}
|
|
48
|
+
get redirectUri() {
|
|
49
|
+
return process.env.GITHUB_REDIRECT_URI || process.env.GITHUB_CALLBACK_URL || 'http://localhost:3000/api/github/callback';
|
|
50
|
+
}
|
|
51
|
+
get webhookBaseUrl() {
|
|
52
|
+
return process.env.ARCHICORE_WEBHOOK_URL || 'http://localhost:3000/api/github/webhook';
|
|
53
|
+
}
|
|
38
54
|
async ensureInitialized() {
|
|
39
55
|
if (this.initialized)
|
|
40
56
|
return;
|
|
@@ -75,7 +91,7 @@ export class GitHubService {
|
|
|
75
91
|
// ===== ENCRYPTION =====
|
|
76
92
|
encrypt(text) {
|
|
77
93
|
const iv = randomBytes(16);
|
|
78
|
-
const key = createHash('sha256').update(
|
|
94
|
+
const key = createHash('sha256').update(getEncryptionKey()).digest();
|
|
79
95
|
const cipher = createCipheriv('aes-256-cbc', key, iv);
|
|
80
96
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
81
97
|
encrypted += cipher.final('hex');
|
|
@@ -84,7 +100,7 @@ export class GitHubService {
|
|
|
84
100
|
decrypt(encrypted) {
|
|
85
101
|
const parts = encrypted.split(':');
|
|
86
102
|
const iv = Buffer.from(parts[0], 'hex');
|
|
87
|
-
const key = createHash('sha256').update(
|
|
103
|
+
const key = createHash('sha256').update(getEncryptionKey()).digest();
|
|
88
104
|
const decipher = createDecipheriv('aes-256-cbc', key, iv);
|
|
89
105
|
let decrypted = decipher.update(parts[1], 'hex', 'utf8');
|
|
90
106
|
decrypted += decipher.final('utf8');
|
|
@@ -640,7 +656,7 @@ export class GitHubService {
|
|
|
640
656
|
const response = await fetch(url, {
|
|
641
657
|
headers: {
|
|
642
658
|
'Authorization': `Bearer ${accessToken}`,
|
|
643
|
-
'Accept': 'application/vnd.github+
|
|
659
|
+
'Accept': 'application/vnd.github.v3+raw',
|
|
644
660
|
'User-Agent': 'ArchiCore',
|
|
645
661
|
'X-GitHub-Api-Version': '2022-11-28'
|
|
646
662
|
},
|
|
@@ -19,8 +19,19 @@ import { Logger } from '../utils/logger.js';
|
|
|
19
19
|
const DATA_DIR = process.env.ARCHICORE_DATA_DIR || '.archicore';
|
|
20
20
|
const INSTANCES_FILE = 'gitlab-instances.json';
|
|
21
21
|
const REPOSITORIES_FILE = 'gitlab-repositories.json';
|
|
22
|
-
// Encryption key
|
|
23
|
-
|
|
22
|
+
// Encryption key - REQUIRED in production
|
|
23
|
+
function getEncryptionKey() {
|
|
24
|
+
const key = process.env.GITLAB_ENCRYPTION_KEY || process.env.ENCRYPTION_KEY;
|
|
25
|
+
if (!key) {
|
|
26
|
+
if (process.env.NODE_ENV === 'production') {
|
|
27
|
+
throw new Error('ENCRYPTION_KEY or GITLAB_ENCRYPTION_KEY is required in production');
|
|
28
|
+
}
|
|
29
|
+
// Dev-only fallback (logs warning)
|
|
30
|
+
console.warn('WARNING: Using dev-only encryption key. Set ENCRYPTION_KEY in production.');
|
|
31
|
+
return 'dev-only-gitlab-key-32bytes!!!';
|
|
32
|
+
}
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
24
35
|
// Webhook base URL - construct from PUBLIC_URL or API_URL
|
|
25
36
|
const getWebhookUrl = () => {
|
|
26
37
|
// Use explicit GitLab webhook URL if set
|
|
@@ -118,7 +129,7 @@ export class GitLabService {
|
|
|
118
129
|
// ===== ENCRYPTION =====
|
|
119
130
|
encrypt(text) {
|
|
120
131
|
const iv = randomBytes(16);
|
|
121
|
-
const key = createHash('sha256').update(
|
|
132
|
+
const key = createHash('sha256').update(getEncryptionKey()).digest();
|
|
122
133
|
const cipher = createCipheriv('aes-256-cbc', key, iv);
|
|
123
134
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
124
135
|
encrypted += cipher.final('hex');
|
|
@@ -130,7 +141,7 @@ export class GitLabService {
|
|
|
130
141
|
if (parts.length !== 2)
|
|
131
142
|
return encrypted; // Not encrypted
|
|
132
143
|
const iv = Buffer.from(parts[0], 'hex');
|
|
133
|
-
const key = createHash('sha256').update(
|
|
144
|
+
const key = createHash('sha256').update(getEncryptionKey()).digest();
|
|
134
145
|
const decipher = createDecipheriv('aes-256-cbc', key, iv);
|
|
135
146
|
let decrypted = decipher.update(parts[1], 'hex', 'utf8');
|
|
136
147
|
decrypted += decipher.final('utf8');
|
|
@@ -488,19 +499,41 @@ export class GitLabService {
|
|
|
488
499
|
throw new Error('GitLab instance not found');
|
|
489
500
|
}
|
|
490
501
|
const accessToken = this.decrypt(instance.accessToken);
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
502
|
+
// Get project info for default branch
|
|
503
|
+
const project = await this.getProject(userId, instanceId, projectId);
|
|
504
|
+
const branch = ref || project.default_branch || 'main';
|
|
505
|
+
// Use API endpoint with wget (Node.js fetch sends headers that GitLab rejects)
|
|
506
|
+
const apiUrl = `${instance.instanceUrl}/api/v4/projects/${encodeURIComponent(String(projectId))}/repository/archive?sha=${encodeURIComponent(branch)}`;
|
|
507
|
+
Logger.info(`[GitLab] Downloading from: ${apiUrl}`);
|
|
508
|
+
// Use wget via child_process - it works where fetch fails
|
|
509
|
+
const { execSync } = await import('child_process');
|
|
510
|
+
const { tmpdir } = await import('os');
|
|
511
|
+
const { join: pathJoin } = await import('path');
|
|
512
|
+
const { readFileSync, unlinkSync } = await import('fs');
|
|
513
|
+
const tempFile = pathJoin(tmpdir(), `gitlab-archive-${Date.now()}.tar.gz`);
|
|
514
|
+
try {
|
|
515
|
+
execSync(`wget -q --header="PRIVATE-TOKEN: ${accessToken}" -O "${tempFile}" "${apiUrl}"`, {
|
|
516
|
+
timeout: 300000, // 5 min timeout
|
|
517
|
+
stdio: 'pipe'
|
|
518
|
+
});
|
|
519
|
+
const buffer = readFileSync(tempFile);
|
|
520
|
+
Logger.info(`[GitLab] Downloaded ${buffer.length} bytes`);
|
|
521
|
+
// Cleanup temp file
|
|
522
|
+
try {
|
|
523
|
+
unlinkSync(tempFile);
|
|
497
524
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
525
|
+
catch { }
|
|
526
|
+
return buffer;
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
// Cleanup on error
|
|
530
|
+
try {
|
|
531
|
+
unlinkSync(tempFile);
|
|
532
|
+
}
|
|
533
|
+
catch { }
|
|
534
|
+
Logger.error(`[GitLab] wget failed: ${error}`);
|
|
535
|
+
throw new Error(`Failed to download repository: wget failed`);
|
|
501
536
|
}
|
|
502
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
503
|
-
return Buffer.from(arrayBuffer);
|
|
504
537
|
}
|
|
505
538
|
/**
|
|
506
539
|
* Get file content from repository
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ImpactEngine } from './impact-engine/index.js';
|
|
|
10
10
|
import { LLMOrchestrator } from './orchestrator/index.js';
|
|
11
11
|
import { Logger } from './utils/logger.js';
|
|
12
12
|
import { config } from 'dotenv';
|
|
13
|
+
import { analyzeProjectStack } from './utils/project-analyzer.js';
|
|
13
14
|
config();
|
|
14
15
|
export class AIArhitector {
|
|
15
16
|
codeIndex;
|
|
@@ -17,6 +18,7 @@ export class AIArhitector {
|
|
|
17
18
|
architecture;
|
|
18
19
|
impactEngine;
|
|
19
20
|
orchestrator;
|
|
21
|
+
projectMetadata = null;
|
|
20
22
|
config;
|
|
21
23
|
initialized = false;
|
|
22
24
|
constructor(config) {
|
|
@@ -38,6 +40,8 @@ export class AIArhitector {
|
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
42
|
Logger.info('Initializing AIArhitector...');
|
|
43
|
+
// Анализируем стек технологий проекта
|
|
44
|
+
this.projectMetadata = analyzeProjectStack(this.config.rootDir);
|
|
41
45
|
await this.codeIndex.indexProject(this.config.rootDir);
|
|
42
46
|
await this.semanticMemory.initialize();
|
|
43
47
|
await this.semanticMemory.indexSymbols(this.codeIndex.getSymbols(), this.codeIndex.getASTs());
|
|
@@ -53,12 +57,14 @@ export class AIArhitector {
|
|
|
53
57
|
throw new Error('Dependency graph not built');
|
|
54
58
|
}
|
|
55
59
|
const impact = this.impactEngine.analyzeChange(change, graph, this.codeIndex.getSymbols(), this.architecture.getModel());
|
|
56
|
-
const semanticContext = await this.semanticMemory.searchByQuery(change.description, 5
|
|
60
|
+
const semanticContext = await this.semanticMemory.searchByQuery(change.description, 30 // Увеличено с 5 до 30
|
|
61
|
+
);
|
|
57
62
|
const llmAnalysis = await this.orchestrator.analyzeImpact(impact, {
|
|
58
63
|
architecture: this.architecture.getModel(),
|
|
59
64
|
semanticMemory: semanticContext,
|
|
60
65
|
codeIndex: graph,
|
|
61
|
-
recentChanges: [change]
|
|
66
|
+
recentChanges: [change],
|
|
67
|
+
projectMetadata: this.projectMetadata || undefined
|
|
62
68
|
});
|
|
63
69
|
Logger.info('LLM Analysis:\n' + llmAnalysis);
|
|
64
70
|
return impact;
|
|
@@ -69,30 +75,33 @@ export class AIArhitector {
|
|
|
69
75
|
}
|
|
70
76
|
async askQuestion(question) {
|
|
71
77
|
this.ensureInitialized();
|
|
72
|
-
const semanticContext = await this.semanticMemory.searchByQuery(question,
|
|
78
|
+
const semanticContext = await this.semanticMemory.searchByQuery(question, 30); // Увеличено с 10 до 30
|
|
73
79
|
const answer = await this.orchestrator.answerArchitecturalQuestion(question, {
|
|
74
80
|
architecture: this.architecture.getModel(),
|
|
75
81
|
semanticMemory: semanticContext,
|
|
76
|
-
codeIndex: this.codeIndex.getGraph() || undefined
|
|
82
|
+
codeIndex: this.codeIndex.getGraph() || undefined,
|
|
83
|
+
projectMetadata: this.projectMetadata || undefined
|
|
77
84
|
});
|
|
78
85
|
return answer;
|
|
79
86
|
}
|
|
80
87
|
async suggestRefactoring(code, goal) {
|
|
81
88
|
this.ensureInitialized();
|
|
82
|
-
const semanticContext = await this.semanticMemory.searchSimilarCode(code, {},
|
|
89
|
+
const semanticContext = await this.semanticMemory.searchSimilarCode(code, {}, 30); // Увеличено с 5 до 30
|
|
83
90
|
const suggestion = await this.orchestrator.suggestRefactoring(code, goal, {
|
|
84
91
|
architecture: this.architecture.getModel(),
|
|
85
|
-
semanticMemory: semanticContext
|
|
92
|
+
semanticMemory: semanticContext,
|
|
93
|
+
projectMetadata: this.projectMetadata || undefined
|
|
86
94
|
});
|
|
87
95
|
return suggestion;
|
|
88
96
|
}
|
|
89
97
|
async reviewCode(code, changedFiles) {
|
|
90
98
|
this.ensureInitialized();
|
|
91
|
-
const semanticContext = await this.semanticMemory.searchSimilarCode(code, {},
|
|
99
|
+
const semanticContext = await this.semanticMemory.searchSimilarCode(code, {}, 30); // Увеличено с 5 до 30
|
|
92
100
|
const review = await this.orchestrator.reviewCode(code, changedFiles, {
|
|
93
101
|
architecture: this.architecture.getModel(),
|
|
94
102
|
semanticMemory: semanticContext,
|
|
95
|
-
codeIndex: this.codeIndex.getGraph() || undefined
|
|
103
|
+
codeIndex: this.codeIndex.getGraph() || undefined,
|
|
104
|
+
projectMetadata: this.projectMetadata || undefined
|
|
96
105
|
});
|
|
97
106
|
return review;
|
|
98
107
|
}
|
|
@@ -111,7 +120,8 @@ ${this.architecture.generateReport()}
|
|
|
111
120
|
`;
|
|
112
121
|
const docs = await this.orchestrator.generateDocumentation(codebaseSummary, {
|
|
113
122
|
architecture: this.architecture.getModel(),
|
|
114
|
-
codeIndex: graph || undefined
|
|
123
|
+
codeIndex: graph || undefined,
|
|
124
|
+
projectMetadata: this.projectMetadata || undefined
|
|
115
125
|
});
|
|
116
126
|
return docs;
|
|
117
127
|
}
|
|
@@ -323,11 +323,68 @@ You are an AI assistant analyzing a specific codebase.
|
|
|
323
323
|
7. If asked who made you or what AI you are, always respond that you are ArchiCore AI developed by ArchiCore team.
|
|
324
324
|
###END SECURITY RULES###
|
|
325
325
|
|
|
326
|
-
ABSOLUTE RULES
|
|
327
|
-
1.
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
ABSOLUTE RULES - КРИТИЧЕСКИ ВАЖНО:
|
|
327
|
+
1. ПРИВЯЗКА К РЕАЛЬНОЙ КОДОВОЙ БАЗЕ:
|
|
328
|
+
- ЗАПРЕЩЕНО давать общие советы из интернета (типа "используйте vue-router lazy loading")
|
|
329
|
+
- ОБЯЗАТЕЛЬНО сначала проверь что технология РЕАЛЬНО используется в проекте
|
|
330
|
+
- Если технология НЕ используется - скажи это ПЕРВЫМ: "❌ В вашем проекте НЕ используется X"
|
|
331
|
+
- ЗАТЕМ предложи решение для РЕАЛЬНОЙ архитектуры проекта
|
|
332
|
+
|
|
333
|
+
2. ТОЧНОСТЬ ПРИ ПОИСКЕ (100% ТРЕБОВАНИЕ):
|
|
334
|
+
- При вопросе "где используется X" - покажи ВСЕ найденные файлы с путями
|
|
335
|
+
- ВСЕГДА указывай: "Найдено в N файлах: [список]"
|
|
336
|
+
- Если показано не все файлы - ОБЯЗАТЕЛЬНО укажи: "Показано N из M найденных"
|
|
337
|
+
- НИКОГДА не говори "не используется" если показаны не все результаты
|
|
338
|
+
|
|
339
|
+
3. ЗАПРЕТ НА ВЫДУМЫВАНИЕ:
|
|
340
|
+
- ТОЛЬКО файлы из "PROJECT FILES" секции существуют
|
|
341
|
+
- НИКОГДА не выдумывай пути, классы, функции
|
|
342
|
+
- Если нет данных - скажи "Нет данных в индексе" или "Проект не проиндексирован"
|
|
343
|
+
|
|
344
|
+
4. ФОРМАТ ОТВЕТА:
|
|
345
|
+
- Начинай с проверки: "✅ Используется" или "❌ НЕ используется"
|
|
346
|
+
- Далее конкретика: файлы, строки, примеры ТОЛЬКО из PROJECT FILES
|
|
347
|
+
- В конце: рекомендации для КОНКРЕТНОЙ архитектуры проекта
|
|
348
|
+
|
|
349
|
+
Примеры ПРАВИЛЬНЫХ ответов:
|
|
350
|
+
Q: "Как оптимизировать vue-router?"
|
|
351
|
+
A: "❌ В вашем проекте НЕ используется vue-router.
|
|
352
|
+
Ваш стек: vanilla JS + Express.
|
|
353
|
+
Для оптимизации загрузки рекомендую:
|
|
354
|
+
[конкретные советы для вашей архитектуры]"
|
|
355
|
+
|
|
356
|
+
Q: "Где используется компонент Comments?"
|
|
357
|
+
A: "✅ Компонент Comments найден в 3 файлах:
|
|
358
|
+
1. src/pages/Post.vue:45
|
|
359
|
+
2. src/pages/Article.vue:89
|
|
360
|
+
3. src/components/Feed.vue:120
|
|
361
|
+
Показано 3 из 3 найденных файлов."`;
|
|
362
|
+
// Добавляем метаданные проекта (стек технологий)
|
|
363
|
+
if (context?.projectMetadata) {
|
|
364
|
+
prompt += '\n\n###PROJECT STACK (РЕАЛЬНЫЕ технологии проекта)###\n';
|
|
365
|
+
if (context.projectMetadata.framework) {
|
|
366
|
+
prompt += `Frontend Framework: ${context.projectMetadata.framework}\n`;
|
|
367
|
+
}
|
|
368
|
+
if (context.projectMetadata.backend) {
|
|
369
|
+
prompt += `Backend: ${context.projectMetadata.backend}\n`;
|
|
370
|
+
}
|
|
371
|
+
if (context.projectMetadata.database) {
|
|
372
|
+
prompt += `Database: ${context.projectMetadata.database}\n`;
|
|
373
|
+
}
|
|
374
|
+
if (context.projectMetadata.buildTool) {
|
|
375
|
+
prompt += `Build Tool: ${context.projectMetadata.buildTool}\n`;
|
|
376
|
+
}
|
|
377
|
+
// Ключевые зависимости
|
|
378
|
+
if (context.projectMetadata.dependencies) {
|
|
379
|
+
const keyDeps = Object.keys(context.projectMetadata.dependencies).filter(dep => ['vue', 'react', 'angular', 'svelte', 'express', 'fastify', 'nest'].some(key => dep.includes(key)));
|
|
380
|
+
if (keyDeps.length > 0) {
|
|
381
|
+
prompt += `Key Dependencies: ${keyDeps.join(', ')}\n`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
prompt += '\n⚠️ ИСПОЛЬЗУЙ ТОЛЬКО ЭТИ ТЕХНОЛОГИИ в советах!\n';
|
|
385
|
+
prompt += '⚠️ Если спрашивают про технологию которой нет выше - скажи что НЕ используется!\n';
|
|
386
|
+
prompt += '###END PROJECT STACK###\n';
|
|
387
|
+
}
|
|
331
388
|
if (context?.architecture?.boundedContexts && context.architecture.boundedContexts.length > 0) {
|
|
332
389
|
prompt += '\n\n**Defined Architecture:**\n';
|
|
333
390
|
for (const bc of context.architecture.boundedContexts) {
|
|
@@ -335,8 +392,16 @@ ABSOLUTE RULES:
|
|
|
335
392
|
}
|
|
336
393
|
}
|
|
337
394
|
if (context?.semanticMemory && context.semanticMemory.length > 0) {
|
|
338
|
-
|
|
339
|
-
|
|
395
|
+
const totalResults = context.semanticMemory.length;
|
|
396
|
+
const maxResults = Math.min(totalResults, 30); // Увеличено с 10 до 30
|
|
397
|
+
prompt += `\n\n###PROJECT FILES (${maxResults} из ${totalResults} найденных)###\n`;
|
|
398
|
+
prompt += `⚠️ ВНИМАНИЕ: Показаны только ${maxResults} наиболее релевантных файлов из ${totalResults}.\n`;
|
|
399
|
+
if (totalResults > maxResults) {
|
|
400
|
+
prompt += `⚠️ КРИТИЧНО: Есть еще ${totalResults - maxResults} файлов, которые могут содержать искомое.\n`;
|
|
401
|
+
prompt += `⚠️ ПРИ ОТВЕТЕ ОБЯЗАТЕЛЬНО УКАЖИ: "Найдено в ${maxResults} файлах, возможно есть еще в ${totalResults - maxResults} файлах"\n`;
|
|
402
|
+
}
|
|
403
|
+
prompt += '\n';
|
|
404
|
+
for (const result of context.semanticMemory.slice(0, maxResults)) {
|
|
340
405
|
const cleanPath = sanitizePath(result.chunk.metadata.filePath);
|
|
341
406
|
prompt += `\nФайл: ${cleanPath}\n`;
|
|
342
407
|
prompt += `${result.context}\n`;
|