archicore 0.1.5 → 0.1.7
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 +162 -16
- package/dist/code-index/ast-parser.js +34 -2
- package/dist/github/github-service.d.ts +8 -0
- package/dist/github/github-service.js +76 -7
- package/dist/server/routes/api.js +73 -25
- package/dist/server/routes/developer.js +2 -1
- package/dist/server/routes/github.js +116 -10
- package/dist/server/routes/upload.js +9 -6
- package/dist/server/services/project-service.d.ts +22 -3
- package/dist/server/services/project-service.js +160 -8
- package/dist/utils/file-utils.js +78 -6
- package/package.json +4 -2
|
@@ -17,6 +17,57 @@ const state = {
|
|
|
17
17
|
history: [],
|
|
18
18
|
user: null,
|
|
19
19
|
};
|
|
20
|
+
// Track token usage for session
|
|
21
|
+
let sessionTokensUsed = 0;
|
|
22
|
+
let lastOperationTokens = 0;
|
|
23
|
+
/**
|
|
24
|
+
* Estimate tokens from text (roughly 1 token = 4 chars)
|
|
25
|
+
*/
|
|
26
|
+
function estimateTokens(text) {
|
|
27
|
+
return Math.ceil(text.length / 4);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Helper for authenticated API calls with token tracking
|
|
31
|
+
*/
|
|
32
|
+
async function apiFetch(url, options = {}) {
|
|
33
|
+
const config = await loadConfig();
|
|
34
|
+
const headers = {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
'Authorization': `Bearer ${config.accessToken}`,
|
|
37
|
+
...options.headers,
|
|
38
|
+
};
|
|
39
|
+
// Estimate input tokens from request body
|
|
40
|
+
const inputTokens = options.body ? estimateTokens(String(options.body)) : 0;
|
|
41
|
+
const response = await fetch(url, { ...options, headers });
|
|
42
|
+
// Try to get tokens from header first
|
|
43
|
+
const tokensHeader = response.headers.get('X-Tokens-Used');
|
|
44
|
+
if (tokensHeader) {
|
|
45
|
+
lastOperationTokens = parseInt(tokensHeader, 10);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
// Estimate based on response size (will be updated when body is read)
|
|
49
|
+
lastOperationTokens = inputTokens;
|
|
50
|
+
}
|
|
51
|
+
return response;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Update token count from response data
|
|
55
|
+
*/
|
|
56
|
+
function updateTokensFromResponse(data) {
|
|
57
|
+
const responseText = JSON.stringify(data);
|
|
58
|
+
const outputTokens = estimateTokens(responseText);
|
|
59
|
+
lastOperationTokens += outputTokens;
|
|
60
|
+
sessionTokensUsed += lastOperationTokens;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Display token usage after operation
|
|
64
|
+
*/
|
|
65
|
+
function showTokenUsage() {
|
|
66
|
+
if (lastOperationTokens > 0) {
|
|
67
|
+
console.log(colors.dim(` [${lastOperationTokens.toLocaleString()} tokens]`));
|
|
68
|
+
lastOperationTokens = 0; // Reset for next operation
|
|
69
|
+
}
|
|
70
|
+
}
|
|
20
71
|
export async function startInteractiveMode() {
|
|
21
72
|
// Prevent process from exiting on unhandled errors
|
|
22
73
|
process.on('uncaughtException', (error) => {
|
|
@@ -212,6 +263,10 @@ async function handleCommand(input) {
|
|
|
212
263
|
case 'export':
|
|
213
264
|
await handleExportCommand(args);
|
|
214
265
|
break;
|
|
266
|
+
case 'docs':
|
|
267
|
+
case 'documentation':
|
|
268
|
+
await handleDocsCommand(args);
|
|
269
|
+
break;
|
|
215
270
|
case 'status':
|
|
216
271
|
await handleStatusCommand();
|
|
217
272
|
break;
|
|
@@ -312,7 +367,64 @@ async function handleIndexCommand() {
|
|
|
312
367
|
});
|
|
313
368
|
if (!response.ok) {
|
|
314
369
|
const errorData = await response.json().catch(() => ({}));
|
|
315
|
-
|
|
370
|
+
// If access denied, re-register project with current user
|
|
371
|
+
if (response.status === 403 && errorData.error?.includes('Access denied')) {
|
|
372
|
+
indexSpinner.update('Re-registering project with current user...');
|
|
373
|
+
// Clear old project ID and re-register
|
|
374
|
+
const reRegisterResponse = await fetch(`${config.serverUrl}/api/projects`, {
|
|
375
|
+
method: 'POST',
|
|
376
|
+
headers: {
|
|
377
|
+
'Content-Type': 'application/json',
|
|
378
|
+
'Authorization': `Bearer ${config.accessToken}`,
|
|
379
|
+
},
|
|
380
|
+
body: JSON.stringify({ name: state.projectName, path: state.projectPath }),
|
|
381
|
+
});
|
|
382
|
+
if (reRegisterResponse.ok) {
|
|
383
|
+
const reData = await reRegisterResponse.json();
|
|
384
|
+
state.projectId = reData.id || reData.project?.id;
|
|
385
|
+
// Retry upload with new project ID
|
|
386
|
+
indexSpinner.update('Uploading to server...');
|
|
387
|
+
const retryResponse = await fetch(`${config.serverUrl}/api/projects/${state.projectId}/upload-index`, {
|
|
388
|
+
method: 'POST',
|
|
389
|
+
headers: {
|
|
390
|
+
'Content-Type': 'application/json',
|
|
391
|
+
'Authorization': `Bearer ${config.accessToken}`,
|
|
392
|
+
},
|
|
393
|
+
body: JSON.stringify({
|
|
394
|
+
asts: astsArray,
|
|
395
|
+
symbols: symbolsArray,
|
|
396
|
+
graph: graphData,
|
|
397
|
+
fileContents,
|
|
398
|
+
statistics: {
|
|
399
|
+
totalFiles: asts.size,
|
|
400
|
+
totalSymbols: symbols.size,
|
|
401
|
+
},
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
if (!retryResponse.ok) {
|
|
405
|
+
throw new Error('Upload failed after re-registration');
|
|
406
|
+
}
|
|
407
|
+
// Use retry response for data
|
|
408
|
+
const retryData = await retryResponse.json();
|
|
409
|
+
indexSpinner.succeed('Project re-registered and indexed');
|
|
410
|
+
printKeyValue('Files', String(retryData.statistics?.filesCount || asts.size));
|
|
411
|
+
printKeyValue('Symbols', String(retryData.statistics?.symbolsCount || symbols.size));
|
|
412
|
+
// Update local config with new project ID
|
|
413
|
+
const localProjectRetry = await getLocalProject(state.projectPath);
|
|
414
|
+
if (localProjectRetry && state.projectId) {
|
|
415
|
+
localProjectRetry.id = state.projectId;
|
|
416
|
+
localProjectRetry.indexed = true;
|
|
417
|
+
await fs.writeFile(pathModule.default.join(state.projectPath, '.archicore', 'project.json'), JSON.stringify(localProjectRetry, null, 2));
|
|
418
|
+
}
|
|
419
|
+
return; // Exit early - already handled
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
throw new Error('Failed to re-register project');
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
throw new Error(errorData.error || 'Upload failed');
|
|
427
|
+
}
|
|
316
428
|
}
|
|
317
429
|
const data = await response.json();
|
|
318
430
|
indexSpinner.succeed('Project indexed and uploaded');
|
|
@@ -341,9 +453,8 @@ async function handleAnalyzeCommand(args) {
|
|
|
341
453
|
const spinner = createSpinner('Analyzing impact...').start();
|
|
342
454
|
try {
|
|
343
455
|
const config = await loadConfig();
|
|
344
|
-
const response = await
|
|
456
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/analyze`, {
|
|
345
457
|
method: 'POST',
|
|
346
|
-
headers: { 'Content-Type': 'application/json' },
|
|
347
458
|
body: JSON.stringify({
|
|
348
459
|
description,
|
|
349
460
|
files: [],
|
|
@@ -394,6 +505,7 @@ async function handleAnalyzeCommand(args) {
|
|
|
394
505
|
console.log(` ${colors.info(icons.info)} ${rec.description}`);
|
|
395
506
|
}
|
|
396
507
|
}
|
|
508
|
+
showTokenUsage();
|
|
397
509
|
}
|
|
398
510
|
catch (error) {
|
|
399
511
|
spinner.fail('Analysis failed');
|
|
@@ -413,9 +525,8 @@ async function handleSearchCommand(query) {
|
|
|
413
525
|
const spinner = createSpinner('Searching...').start();
|
|
414
526
|
try {
|
|
415
527
|
const config = await loadConfig();
|
|
416
|
-
const response = await
|
|
528
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/search`, {
|
|
417
529
|
method: 'POST',
|
|
418
|
-
headers: { 'Content-Type': 'application/json' },
|
|
419
530
|
body: JSON.stringify({ query, limit: 10 }),
|
|
420
531
|
});
|
|
421
532
|
if (!response.ok)
|
|
@@ -440,8 +551,8 @@ async function handleSearchCommand(query) {
|
|
|
440
551
|
.replace(/\n/g, ' ');
|
|
441
552
|
console.log(` ${colors.dim(preview)}${preview.length >= 100 ? '...' : ''}`);
|
|
442
553
|
}
|
|
443
|
-
console.log();
|
|
444
554
|
}
|
|
555
|
+
showTokenUsage();
|
|
445
556
|
}
|
|
446
557
|
catch (error) {
|
|
447
558
|
spinner.fail('Search failed');
|
|
@@ -457,14 +568,14 @@ async function handleQuery(query) {
|
|
|
457
568
|
const spinner = createSpinner('Thinking...').start();
|
|
458
569
|
try {
|
|
459
570
|
const config = await loadConfig();
|
|
460
|
-
const response = await
|
|
571
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/ask`, {
|
|
461
572
|
method: 'POST',
|
|
462
|
-
headers: { 'Content-Type': 'application/json' },
|
|
463
573
|
body: JSON.stringify({ question: query }),
|
|
464
574
|
});
|
|
465
575
|
if (!response.ok)
|
|
466
576
|
throw new Error('Query failed');
|
|
467
577
|
const data = await response.json();
|
|
578
|
+
updateTokensFromResponse(data);
|
|
468
579
|
spinner.stop();
|
|
469
580
|
console.log();
|
|
470
581
|
console.log(colors.primary(' ' + icons.lightning + ' ArchiCore'));
|
|
@@ -475,6 +586,7 @@ async function handleQuery(query) {
|
|
|
475
586
|
for (const line of lines) {
|
|
476
587
|
console.log(' ' + line);
|
|
477
588
|
}
|
|
589
|
+
showTokenUsage();
|
|
478
590
|
}
|
|
479
591
|
catch (error) {
|
|
480
592
|
spinner.fail('Query failed');
|
|
@@ -489,7 +601,7 @@ async function handleDeadCodeCommand() {
|
|
|
489
601
|
const spinner = createSpinner('Analyzing dead code...').start();
|
|
490
602
|
try {
|
|
491
603
|
const config = await loadConfig();
|
|
492
|
-
const response = await
|
|
604
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/dead-code`);
|
|
493
605
|
if (!response.ok)
|
|
494
606
|
throw new Error('Analysis failed');
|
|
495
607
|
const data = await response.json();
|
|
@@ -520,7 +632,7 @@ async function handleSecurityCommand() {
|
|
|
520
632
|
const spinner = createSpinner('Analyzing security...').start();
|
|
521
633
|
try {
|
|
522
634
|
const config = await loadConfig();
|
|
523
|
-
const response = await
|
|
635
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/security`);
|
|
524
636
|
if (!response.ok)
|
|
525
637
|
throw new Error('Analysis failed');
|
|
526
638
|
const data = await response.json();
|
|
@@ -562,7 +674,7 @@ async function handleMetricsCommand() {
|
|
|
562
674
|
const spinner = createSpinner('Calculating metrics...').start();
|
|
563
675
|
try {
|
|
564
676
|
const config = await loadConfig();
|
|
565
|
-
const response = await
|
|
677
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/metrics`);
|
|
566
678
|
if (!response.ok)
|
|
567
679
|
throw new Error('Analysis failed');
|
|
568
680
|
const data = await response.json();
|
|
@@ -591,9 +703,8 @@ async function handleExportCommand(args) {
|
|
|
591
703
|
const spinner = createSpinner(`Exporting as ${format}...`).start();
|
|
592
704
|
try {
|
|
593
705
|
const config = await loadConfig();
|
|
594
|
-
const response = await
|
|
706
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/export`, {
|
|
595
707
|
method: 'POST',
|
|
596
|
-
headers: { 'Content-Type': 'application/json' },
|
|
597
708
|
body: JSON.stringify({ format }),
|
|
598
709
|
});
|
|
599
710
|
if (!response.ok)
|
|
@@ -647,6 +758,10 @@ async function handleStatusCommand() {
|
|
|
647
758
|
catch {
|
|
648
759
|
// Ignore subscription fetch errors
|
|
649
760
|
}
|
|
761
|
+
// Show session token usage
|
|
762
|
+
if (sessionTokensUsed > 0) {
|
|
763
|
+
console.log(` Session tokens: ${colors.highlight(sessionTokensUsed.toLocaleString())}`);
|
|
764
|
+
}
|
|
650
765
|
}
|
|
651
766
|
if (state.projectId) {
|
|
652
767
|
console.log(` Project: ${colors.primary(state.projectName || state.projectId)}`);
|
|
@@ -664,7 +779,7 @@ async function handleDuplicationCommand() {
|
|
|
664
779
|
const spinner = createSpinner('Analyzing code duplication...').start();
|
|
665
780
|
try {
|
|
666
781
|
const config = await loadConfig();
|
|
667
|
-
const response = await
|
|
782
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/duplication`);
|
|
668
783
|
if (!response.ok)
|
|
669
784
|
throw new Error('Analysis failed');
|
|
670
785
|
const data = await response.json();
|
|
@@ -701,7 +816,7 @@ async function handleRefactoringCommand() {
|
|
|
701
816
|
const spinner = createSpinner('Generating refactoring suggestions...').start();
|
|
702
817
|
try {
|
|
703
818
|
const config = await loadConfig();
|
|
704
|
-
const response = await
|
|
819
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/refactoring`);
|
|
705
820
|
if (!response.ok)
|
|
706
821
|
throw new Error('Analysis failed');
|
|
707
822
|
const data = await response.json();
|
|
@@ -746,7 +861,7 @@ async function handleRulesCommand() {
|
|
|
746
861
|
const spinner = createSpinner('Checking architectural rules...').start();
|
|
747
862
|
try {
|
|
748
863
|
const config = await loadConfig();
|
|
749
|
-
const response = await
|
|
864
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/rules`);
|
|
750
865
|
if (!response.ok)
|
|
751
866
|
throw new Error('Analysis failed');
|
|
752
867
|
const data = await response.json();
|
|
@@ -773,4 +888,35 @@ async function handleRulesCommand() {
|
|
|
773
888
|
throw error;
|
|
774
889
|
}
|
|
775
890
|
}
|
|
891
|
+
async function handleDocsCommand(args) {
|
|
892
|
+
if (!state.projectId) {
|
|
893
|
+
printError('No project indexed');
|
|
894
|
+
printInfo('Use /index first');
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const format = args[0] || 'markdown';
|
|
898
|
+
const output = args[1] || `documentation.${format === 'markdown' ? 'md' : format}`;
|
|
899
|
+
const spinner = createSpinner('Generating documentation...').start();
|
|
900
|
+
try {
|
|
901
|
+
const config = await loadConfig();
|
|
902
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/documentation`, {
|
|
903
|
+
method: 'POST',
|
|
904
|
+
body: JSON.stringify({ format }),
|
|
905
|
+
});
|
|
906
|
+
if (!response.ok)
|
|
907
|
+
throw new Error('Documentation generation failed');
|
|
908
|
+
const data = await response.json();
|
|
909
|
+
updateTokensFromResponse(data);
|
|
910
|
+
const { writeFile } = await import('fs/promises');
|
|
911
|
+
await writeFile(output, data.documentation);
|
|
912
|
+
spinner.succeed('Documentation generated');
|
|
913
|
+
printKeyValue('Output', output);
|
|
914
|
+
printKeyValue('Format', format);
|
|
915
|
+
showTokenUsage();
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
spinner.fail('Documentation generation failed');
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
776
922
|
//# sourceMappingURL=interactive.js.map
|
|
@@ -144,9 +144,41 @@ export class ASTParser {
|
|
|
144
144
|
/^(?:\w+\s+)*(\w+)\s*\([^)]*\)\s*{/,
|
|
145
145
|
/^struct\s+(\w+)/,
|
|
146
146
|
/^typedef\s+.*\s+(\w+)\s*;/
|
|
147
|
-
]
|
|
147
|
+
],
|
|
148
|
+
html: [
|
|
149
|
+
/<(\w+)\s+id="(\w+)"/, // Elements with id
|
|
150
|
+
/<script[^>]*>/, // Script tags
|
|
151
|
+
/<style[^>]*>/ // Style tags
|
|
152
|
+
],
|
|
153
|
+
css: [
|
|
154
|
+
/^\.(\w[\w-]*)\s*\{/, // Class selectors
|
|
155
|
+
/^#(\w[\w-]*)\s*\{/, // ID selectors
|
|
156
|
+
/^@media\s+/, // Media queries
|
|
157
|
+
/^@keyframes\s+(\w+)/ // Animations
|
|
158
|
+
],
|
|
159
|
+
scss: [
|
|
160
|
+
/^\.(\w[\w-]*)\s*\{/,
|
|
161
|
+
/^\$(\w[\w-]*)\s*:/, // Variables
|
|
162
|
+
/^@mixin\s+(\w+)/, // Mixins
|
|
163
|
+
/^@include\s+(\w+)/ // Include
|
|
164
|
+
],
|
|
165
|
+
json: [], // JSON doesn't have functions/classes
|
|
166
|
+
yaml: [], // YAML doesn't have functions/classes
|
|
167
|
+
xml: [
|
|
168
|
+
/<(\w+)\s+.*?>/ // XML elements
|
|
169
|
+
],
|
|
170
|
+
sql: [
|
|
171
|
+
/^CREATE\s+(?:TABLE|VIEW|FUNCTION|PROCEDURE)\s+(\w+)/i,
|
|
172
|
+
/^ALTER\s+TABLE\s+(\w+)/i,
|
|
173
|
+
/^INSERT\s+INTO\s+(\w+)/i
|
|
174
|
+
],
|
|
175
|
+
shell: [
|
|
176
|
+
/^(\w+)\s*\(\)\s*\{/, // Function definitions
|
|
177
|
+
/^function\s+(\w+)/ // Function keyword
|
|
178
|
+
],
|
|
179
|
+
markdown: [] // Markdown doesn't have functions/classes
|
|
148
180
|
};
|
|
149
|
-
const langPatterns = patterns[language] ||
|
|
181
|
+
const langPatterns = patterns[language] || [];
|
|
150
182
|
lines.forEach((line, index) => {
|
|
151
183
|
const trimmedLine = line.trim();
|
|
152
184
|
for (const pattern of langPatterns) {
|
|
@@ -71,6 +71,14 @@ export declare class GitHubService {
|
|
|
71
71
|
* Disconnect repository
|
|
72
72
|
*/
|
|
73
73
|
disconnectRepository(userId: string, repoId: string): Promise<boolean>;
|
|
74
|
+
/**
|
|
75
|
+
* Disconnect repository by project ID
|
|
76
|
+
*/
|
|
77
|
+
disconnectRepositoryByProjectId(projectId: string): Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Update repository's project ID
|
|
80
|
+
*/
|
|
81
|
+
updateRepositoryProjectId(repoId: string, projectId: string): Promise<void>;
|
|
74
82
|
/**
|
|
75
83
|
* Get connected repositories for user
|
|
76
84
|
*/
|
|
@@ -12,6 +12,8 @@ const CONNECTIONS_FILE = 'github-connections.json';
|
|
|
12
12
|
const REPOSITORIES_FILE = 'github-repositories.json';
|
|
13
13
|
// Encryption key (in production, use env variable)
|
|
14
14
|
const ENCRYPTION_KEY = process.env.GITHUB_ENCRYPTION_KEY || 'archicore-github-key-32bytes!!';
|
|
15
|
+
// Singleton instance
|
|
16
|
+
let instance = null;
|
|
15
17
|
export class GitHubService {
|
|
16
18
|
dataDir;
|
|
17
19
|
connections = [];
|
|
@@ -22,11 +24,16 @@ export class GitHubService {
|
|
|
22
24
|
redirectUri;
|
|
23
25
|
webhookBaseUrl;
|
|
24
26
|
constructor(dataDir = DATA_DIR) {
|
|
27
|
+
// Singleton pattern - return existing instance
|
|
28
|
+
if (instance) {
|
|
29
|
+
return instance;
|
|
30
|
+
}
|
|
25
31
|
this.dataDir = dataDir;
|
|
26
32
|
this.clientId = process.env.GITHUB_CLIENT_ID || '';
|
|
27
33
|
this.clientSecret = process.env.GITHUB_CLIENT_SECRET || '';
|
|
28
34
|
this.redirectUri = process.env.GITHUB_REDIRECT_URI || 'http://localhost:3000/api/github/callback';
|
|
29
35
|
this.webhookBaseUrl = process.env.ARCHICORE_WEBHOOK_URL || 'http://localhost:3000/api/github/webhook';
|
|
36
|
+
instance = this;
|
|
30
37
|
}
|
|
31
38
|
async ensureInitialized() {
|
|
32
39
|
if (this.initialized)
|
|
@@ -222,7 +229,8 @@ export class GitHubService {
|
|
|
222
229
|
let page = 1;
|
|
223
230
|
const perPage = 100;
|
|
224
231
|
while (true) {
|
|
225
|
-
|
|
232
|
+
// visibility=all includes private repos, affiliation includes all repo types
|
|
233
|
+
const response = await fetch(`https://api.github.com/user/repos?per_page=${perPage}&page=${page}&sort=updated&visibility=all&affiliation=owner,collaborator,organization_member`, {
|
|
226
234
|
headers: {
|
|
227
235
|
'Authorization': `Bearer ${accessToken}`,
|
|
228
236
|
'Accept': 'application/vnd.github.v3+json',
|
|
@@ -230,7 +238,16 @@ export class GitHubService {
|
|
|
230
238
|
}
|
|
231
239
|
});
|
|
232
240
|
if (!response.ok) {
|
|
233
|
-
|
|
241
|
+
const errorBody = await response.text();
|
|
242
|
+
Logger.error(`GitHub API error ${response.status}: ${errorBody}`);
|
|
243
|
+
// Check for specific errors
|
|
244
|
+
if (response.status === 401) {
|
|
245
|
+
throw new Error('GitHub token expired or invalid. Please reconnect GitHub.');
|
|
246
|
+
}
|
|
247
|
+
else if (response.status === 403) {
|
|
248
|
+
throw new Error('GitHub rate limit exceeded or insufficient permissions.');
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Failed to list repositories: ${response.status}`);
|
|
234
251
|
}
|
|
235
252
|
const data = await response.json();
|
|
236
253
|
repos.push(...data);
|
|
@@ -359,6 +376,40 @@ export class GitHubService {
|
|
|
359
376
|
Logger.info(`Repository disconnected: ${repo.fullName}`);
|
|
360
377
|
return true;
|
|
361
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Disconnect repository by project ID
|
|
381
|
+
*/
|
|
382
|
+
async disconnectRepositoryByProjectId(projectId) {
|
|
383
|
+
await this.ensureInitialized();
|
|
384
|
+
const repo = this.repositories.find(r => r.projectId === projectId);
|
|
385
|
+
if (!repo)
|
|
386
|
+
return false;
|
|
387
|
+
// Delete webhook
|
|
388
|
+
if (repo.webhookId) {
|
|
389
|
+
try {
|
|
390
|
+
await this.deleteWebhook(repo.userId, repo.fullName, repo.webhookId);
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
Logger.warn(`Failed to delete webhook: ${e}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
this.repositories = this.repositories.filter(r => r.projectId !== projectId);
|
|
397
|
+
await this.saveRepositories();
|
|
398
|
+
Logger.info(`Repository disconnected by projectId: ${repo.fullName}`);
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Update repository's project ID
|
|
403
|
+
*/
|
|
404
|
+
async updateRepositoryProjectId(repoId, projectId) {
|
|
405
|
+
await this.ensureInitialized();
|
|
406
|
+
const repo = this.repositories.find(r => r.id === repoId);
|
|
407
|
+
if (repo) {
|
|
408
|
+
repo.projectId = projectId;
|
|
409
|
+
repo.updatedAt = new Date();
|
|
410
|
+
await this.saveRepositories();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
362
413
|
/**
|
|
363
414
|
* Get connected repositories for user
|
|
364
415
|
*/
|
|
@@ -560,18 +611,36 @@ export class GitHubService {
|
|
|
560
611
|
throw new Error('GitHub not connected');
|
|
561
612
|
}
|
|
562
613
|
const url = `https://api.github.com/repos/${fullName}/zipball/${ref || 'HEAD'}`;
|
|
614
|
+
Logger.info(`Downloading from: ${url}`);
|
|
563
615
|
const response = await fetch(url, {
|
|
564
616
|
headers: {
|
|
565
617
|
'Authorization': `Bearer ${accessToken}`,
|
|
566
|
-
'Accept': 'application/vnd.github
|
|
567
|
-
'User-Agent': 'ArchiCore'
|
|
568
|
-
|
|
618
|
+
'Accept': 'application/vnd.github+json',
|
|
619
|
+
'User-Agent': 'ArchiCore',
|
|
620
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
621
|
+
},
|
|
622
|
+
redirect: 'follow'
|
|
569
623
|
});
|
|
624
|
+
Logger.info(`GitHub response status: ${response.status}, content-type: ${response.headers.get('content-type')}`);
|
|
570
625
|
if (!response.ok) {
|
|
571
|
-
|
|
626
|
+
const errorText = await response.text();
|
|
627
|
+
Logger.error(`GitHub download error: ${response.status} - ${errorText}`);
|
|
628
|
+
throw new Error(`Failed to download repository: ${response.status}`);
|
|
572
629
|
}
|
|
573
630
|
const arrayBuffer = await response.arrayBuffer();
|
|
574
|
-
|
|
631
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
632
|
+
// Validate that we got a ZIP file (should start with PK\x03\x04)
|
|
633
|
+
if (buffer.length < 100) {
|
|
634
|
+
Logger.error(`Downloaded content too small (${buffer.length} bytes), content: ${buffer.toString('utf8').substring(0, 200)}`);
|
|
635
|
+
throw new Error(`Downloaded file too small - possibly an error response`);
|
|
636
|
+
}
|
|
637
|
+
// Check ZIP magic bytes
|
|
638
|
+
if (buffer[0] !== 0x50 || buffer[1] !== 0x4B) {
|
|
639
|
+
Logger.error(`Invalid ZIP file, first bytes: ${buffer[0]}, ${buffer[1]}, content: ${buffer.toString('utf8').substring(0, 200)}`);
|
|
640
|
+
throw new Error('Downloaded file is not a valid ZIP archive');
|
|
641
|
+
}
|
|
642
|
+
Logger.info(`Valid ZIP downloaded: ${buffer.length} bytes`);
|
|
643
|
+
return buffer;
|
|
575
644
|
}
|
|
576
645
|
// ===== UTILITY =====
|
|
577
646
|
/**
|