archicore 0.1.4 → 0.1.6
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 +136 -18
- 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 +68 -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,7 +17,67 @@ 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() {
|
|
72
|
+
// Prevent process from exiting on unhandled errors
|
|
73
|
+
process.on('uncaughtException', (error) => {
|
|
74
|
+
printError(`Unexpected error: ${error.message}`);
|
|
75
|
+
console.log();
|
|
76
|
+
});
|
|
77
|
+
process.on('unhandledRejection', (reason) => {
|
|
78
|
+
printError(`Unhandled promise rejection: ${reason}`);
|
|
79
|
+
console.log();
|
|
80
|
+
});
|
|
21
81
|
// Check if initialized in current directory
|
|
22
82
|
const initialized = await isInitialized(state.projectPath);
|
|
23
83
|
if (!initialized) {
|
|
@@ -102,14 +162,34 @@ export async function startInteractiveMode() {
|
|
|
102
162
|
});
|
|
103
163
|
rl.on('close', () => {
|
|
104
164
|
if (state.running) {
|
|
105
|
-
// Unexpected close
|
|
165
|
+
// Unexpected close - don't exit, restart readline
|
|
166
|
+
console.log();
|
|
167
|
+
printWarning('Input stream interrupted, restarting...');
|
|
168
|
+
// Recreate readline interface
|
|
169
|
+
const newRl = readline.createInterface({
|
|
170
|
+
input: process.stdin,
|
|
171
|
+
output: process.stdout,
|
|
172
|
+
terminal: true,
|
|
173
|
+
});
|
|
174
|
+
newRl.on('line', (input) => {
|
|
175
|
+
processLine(input).catch((err) => {
|
|
176
|
+
printError(String(err));
|
|
177
|
+
prompt();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
newRl.on('close', () => {
|
|
181
|
+
if (!state.running) {
|
|
182
|
+
printGoodbye();
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
106
186
|
console.log();
|
|
107
|
-
|
|
187
|
+
prompt();
|
|
108
188
|
}
|
|
109
189
|
else {
|
|
110
190
|
printGoodbye();
|
|
191
|
+
process.exit(0);
|
|
111
192
|
}
|
|
112
|
-
process.exit(0);
|
|
113
193
|
});
|
|
114
194
|
// Handle SIGINT (Ctrl+C)
|
|
115
195
|
process.on('SIGINT', () => {
|
|
@@ -183,6 +263,10 @@ async function handleCommand(input) {
|
|
|
183
263
|
case 'export':
|
|
184
264
|
await handleExportCommand(args);
|
|
185
265
|
break;
|
|
266
|
+
case 'docs':
|
|
267
|
+
case 'documentation':
|
|
268
|
+
await handleDocsCommand(args);
|
|
269
|
+
break;
|
|
186
270
|
case 'status':
|
|
187
271
|
await handleStatusCommand();
|
|
188
272
|
break;
|
|
@@ -312,9 +396,8 @@ async function handleAnalyzeCommand(args) {
|
|
|
312
396
|
const spinner = createSpinner('Analyzing impact...').start();
|
|
313
397
|
try {
|
|
314
398
|
const config = await loadConfig();
|
|
315
|
-
const response = await
|
|
399
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/analyze`, {
|
|
316
400
|
method: 'POST',
|
|
317
|
-
headers: { 'Content-Type': 'application/json' },
|
|
318
401
|
body: JSON.stringify({
|
|
319
402
|
description,
|
|
320
403
|
files: [],
|
|
@@ -365,6 +448,7 @@ async function handleAnalyzeCommand(args) {
|
|
|
365
448
|
console.log(` ${colors.info(icons.info)} ${rec.description}`);
|
|
366
449
|
}
|
|
367
450
|
}
|
|
451
|
+
showTokenUsage();
|
|
368
452
|
}
|
|
369
453
|
catch (error) {
|
|
370
454
|
spinner.fail('Analysis failed');
|
|
@@ -384,9 +468,8 @@ async function handleSearchCommand(query) {
|
|
|
384
468
|
const spinner = createSpinner('Searching...').start();
|
|
385
469
|
try {
|
|
386
470
|
const config = await loadConfig();
|
|
387
|
-
const response = await
|
|
471
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/search`, {
|
|
388
472
|
method: 'POST',
|
|
389
|
-
headers: { 'Content-Type': 'application/json' },
|
|
390
473
|
body: JSON.stringify({ query, limit: 10 }),
|
|
391
474
|
});
|
|
392
475
|
if (!response.ok)
|
|
@@ -411,8 +494,8 @@ async function handleSearchCommand(query) {
|
|
|
411
494
|
.replace(/\n/g, ' ');
|
|
412
495
|
console.log(` ${colors.dim(preview)}${preview.length >= 100 ? '...' : ''}`);
|
|
413
496
|
}
|
|
414
|
-
console.log();
|
|
415
497
|
}
|
|
498
|
+
showTokenUsage();
|
|
416
499
|
}
|
|
417
500
|
catch (error) {
|
|
418
501
|
spinner.fail('Search failed');
|
|
@@ -428,14 +511,14 @@ async function handleQuery(query) {
|
|
|
428
511
|
const spinner = createSpinner('Thinking...').start();
|
|
429
512
|
try {
|
|
430
513
|
const config = await loadConfig();
|
|
431
|
-
const response = await
|
|
514
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/ask`, {
|
|
432
515
|
method: 'POST',
|
|
433
|
-
headers: { 'Content-Type': 'application/json' },
|
|
434
516
|
body: JSON.stringify({ question: query }),
|
|
435
517
|
});
|
|
436
518
|
if (!response.ok)
|
|
437
519
|
throw new Error('Query failed');
|
|
438
520
|
const data = await response.json();
|
|
521
|
+
updateTokensFromResponse(data);
|
|
439
522
|
spinner.stop();
|
|
440
523
|
console.log();
|
|
441
524
|
console.log(colors.primary(' ' + icons.lightning + ' ArchiCore'));
|
|
@@ -446,6 +529,7 @@ async function handleQuery(query) {
|
|
|
446
529
|
for (const line of lines) {
|
|
447
530
|
console.log(' ' + line);
|
|
448
531
|
}
|
|
532
|
+
showTokenUsage();
|
|
449
533
|
}
|
|
450
534
|
catch (error) {
|
|
451
535
|
spinner.fail('Query failed');
|
|
@@ -460,7 +544,7 @@ async function handleDeadCodeCommand() {
|
|
|
460
544
|
const spinner = createSpinner('Analyzing dead code...').start();
|
|
461
545
|
try {
|
|
462
546
|
const config = await loadConfig();
|
|
463
|
-
const response = await
|
|
547
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/dead-code`);
|
|
464
548
|
if (!response.ok)
|
|
465
549
|
throw new Error('Analysis failed');
|
|
466
550
|
const data = await response.json();
|
|
@@ -491,7 +575,7 @@ async function handleSecurityCommand() {
|
|
|
491
575
|
const spinner = createSpinner('Analyzing security...').start();
|
|
492
576
|
try {
|
|
493
577
|
const config = await loadConfig();
|
|
494
|
-
const response = await
|
|
578
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/security`);
|
|
495
579
|
if (!response.ok)
|
|
496
580
|
throw new Error('Analysis failed');
|
|
497
581
|
const data = await response.json();
|
|
@@ -533,7 +617,7 @@ async function handleMetricsCommand() {
|
|
|
533
617
|
const spinner = createSpinner('Calculating metrics...').start();
|
|
534
618
|
try {
|
|
535
619
|
const config = await loadConfig();
|
|
536
|
-
const response = await
|
|
620
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/metrics`);
|
|
537
621
|
if (!response.ok)
|
|
538
622
|
throw new Error('Analysis failed');
|
|
539
623
|
const data = await response.json();
|
|
@@ -562,9 +646,8 @@ async function handleExportCommand(args) {
|
|
|
562
646
|
const spinner = createSpinner(`Exporting as ${format}...`).start();
|
|
563
647
|
try {
|
|
564
648
|
const config = await loadConfig();
|
|
565
|
-
const response = await
|
|
649
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/export`, {
|
|
566
650
|
method: 'POST',
|
|
567
|
-
headers: { 'Content-Type': 'application/json' },
|
|
568
651
|
body: JSON.stringify({ format }),
|
|
569
652
|
});
|
|
570
653
|
if (!response.ok)
|
|
@@ -618,6 +701,10 @@ async function handleStatusCommand() {
|
|
|
618
701
|
catch {
|
|
619
702
|
// Ignore subscription fetch errors
|
|
620
703
|
}
|
|
704
|
+
// Show session token usage
|
|
705
|
+
if (sessionTokensUsed > 0) {
|
|
706
|
+
console.log(` Session tokens: ${colors.highlight(sessionTokensUsed.toLocaleString())}`);
|
|
707
|
+
}
|
|
621
708
|
}
|
|
622
709
|
if (state.projectId) {
|
|
623
710
|
console.log(` Project: ${colors.primary(state.projectName || state.projectId)}`);
|
|
@@ -635,7 +722,7 @@ async function handleDuplicationCommand() {
|
|
|
635
722
|
const spinner = createSpinner('Analyzing code duplication...').start();
|
|
636
723
|
try {
|
|
637
724
|
const config = await loadConfig();
|
|
638
|
-
const response = await
|
|
725
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/duplication`);
|
|
639
726
|
if (!response.ok)
|
|
640
727
|
throw new Error('Analysis failed');
|
|
641
728
|
const data = await response.json();
|
|
@@ -672,7 +759,7 @@ async function handleRefactoringCommand() {
|
|
|
672
759
|
const spinner = createSpinner('Generating refactoring suggestions...').start();
|
|
673
760
|
try {
|
|
674
761
|
const config = await loadConfig();
|
|
675
|
-
const response = await
|
|
762
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/refactoring`);
|
|
676
763
|
if (!response.ok)
|
|
677
764
|
throw new Error('Analysis failed');
|
|
678
765
|
const data = await response.json();
|
|
@@ -717,7 +804,7 @@ async function handleRulesCommand() {
|
|
|
717
804
|
const spinner = createSpinner('Checking architectural rules...').start();
|
|
718
805
|
try {
|
|
719
806
|
const config = await loadConfig();
|
|
720
|
-
const response = await
|
|
807
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/rules`);
|
|
721
808
|
if (!response.ok)
|
|
722
809
|
throw new Error('Analysis failed');
|
|
723
810
|
const data = await response.json();
|
|
@@ -744,4 +831,35 @@ async function handleRulesCommand() {
|
|
|
744
831
|
throw error;
|
|
745
832
|
}
|
|
746
833
|
}
|
|
834
|
+
async function handleDocsCommand(args) {
|
|
835
|
+
if (!state.projectId) {
|
|
836
|
+
printError('No project indexed');
|
|
837
|
+
printInfo('Use /index first');
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const format = args[0] || 'markdown';
|
|
841
|
+
const output = args[1] || `documentation.${format === 'markdown' ? 'md' : format}`;
|
|
842
|
+
const spinner = createSpinner('Generating documentation...').start();
|
|
843
|
+
try {
|
|
844
|
+
const config = await loadConfig();
|
|
845
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/documentation`, {
|
|
846
|
+
method: 'POST',
|
|
847
|
+
body: JSON.stringify({ format }),
|
|
848
|
+
});
|
|
849
|
+
if (!response.ok)
|
|
850
|
+
throw new Error('Documentation generation failed');
|
|
851
|
+
const data = await response.json();
|
|
852
|
+
updateTokensFromResponse(data);
|
|
853
|
+
const { writeFile } = await import('fs/promises');
|
|
854
|
+
await writeFile(output, data.documentation);
|
|
855
|
+
spinner.succeed('Documentation generated');
|
|
856
|
+
printKeyValue('Output', output);
|
|
857
|
+
printKeyValue('Format', format);
|
|
858
|
+
showTokenUsage();
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
spinner.fail('Documentation generation failed');
|
|
862
|
+
throw error;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
747
865
|
//# 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
|
/**
|