archicore 0.1.5 → 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 +104 -15
- 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,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;
|
|
@@ -341,9 +396,8 @@ async function handleAnalyzeCommand(args) {
|
|
|
341
396
|
const spinner = createSpinner('Analyzing impact...').start();
|
|
342
397
|
try {
|
|
343
398
|
const config = await loadConfig();
|
|
344
|
-
const response = await
|
|
399
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/analyze`, {
|
|
345
400
|
method: 'POST',
|
|
346
|
-
headers: { 'Content-Type': 'application/json' },
|
|
347
401
|
body: JSON.stringify({
|
|
348
402
|
description,
|
|
349
403
|
files: [],
|
|
@@ -394,6 +448,7 @@ async function handleAnalyzeCommand(args) {
|
|
|
394
448
|
console.log(` ${colors.info(icons.info)} ${rec.description}`);
|
|
395
449
|
}
|
|
396
450
|
}
|
|
451
|
+
showTokenUsage();
|
|
397
452
|
}
|
|
398
453
|
catch (error) {
|
|
399
454
|
spinner.fail('Analysis failed');
|
|
@@ -413,9 +468,8 @@ async function handleSearchCommand(query) {
|
|
|
413
468
|
const spinner = createSpinner('Searching...').start();
|
|
414
469
|
try {
|
|
415
470
|
const config = await loadConfig();
|
|
416
|
-
const response = await
|
|
471
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/search`, {
|
|
417
472
|
method: 'POST',
|
|
418
|
-
headers: { 'Content-Type': 'application/json' },
|
|
419
473
|
body: JSON.stringify({ query, limit: 10 }),
|
|
420
474
|
});
|
|
421
475
|
if (!response.ok)
|
|
@@ -440,8 +494,8 @@ async function handleSearchCommand(query) {
|
|
|
440
494
|
.replace(/\n/g, ' ');
|
|
441
495
|
console.log(` ${colors.dim(preview)}${preview.length >= 100 ? '...' : ''}`);
|
|
442
496
|
}
|
|
443
|
-
console.log();
|
|
444
497
|
}
|
|
498
|
+
showTokenUsage();
|
|
445
499
|
}
|
|
446
500
|
catch (error) {
|
|
447
501
|
spinner.fail('Search failed');
|
|
@@ -457,14 +511,14 @@ async function handleQuery(query) {
|
|
|
457
511
|
const spinner = createSpinner('Thinking...').start();
|
|
458
512
|
try {
|
|
459
513
|
const config = await loadConfig();
|
|
460
|
-
const response = await
|
|
514
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/ask`, {
|
|
461
515
|
method: 'POST',
|
|
462
|
-
headers: { 'Content-Type': 'application/json' },
|
|
463
516
|
body: JSON.stringify({ question: query }),
|
|
464
517
|
});
|
|
465
518
|
if (!response.ok)
|
|
466
519
|
throw new Error('Query failed');
|
|
467
520
|
const data = await response.json();
|
|
521
|
+
updateTokensFromResponse(data);
|
|
468
522
|
spinner.stop();
|
|
469
523
|
console.log();
|
|
470
524
|
console.log(colors.primary(' ' + icons.lightning + ' ArchiCore'));
|
|
@@ -475,6 +529,7 @@ async function handleQuery(query) {
|
|
|
475
529
|
for (const line of lines) {
|
|
476
530
|
console.log(' ' + line);
|
|
477
531
|
}
|
|
532
|
+
showTokenUsage();
|
|
478
533
|
}
|
|
479
534
|
catch (error) {
|
|
480
535
|
spinner.fail('Query failed');
|
|
@@ -489,7 +544,7 @@ async function handleDeadCodeCommand() {
|
|
|
489
544
|
const spinner = createSpinner('Analyzing dead code...').start();
|
|
490
545
|
try {
|
|
491
546
|
const config = await loadConfig();
|
|
492
|
-
const response = await
|
|
547
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/dead-code`);
|
|
493
548
|
if (!response.ok)
|
|
494
549
|
throw new Error('Analysis failed');
|
|
495
550
|
const data = await response.json();
|
|
@@ -520,7 +575,7 @@ async function handleSecurityCommand() {
|
|
|
520
575
|
const spinner = createSpinner('Analyzing security...').start();
|
|
521
576
|
try {
|
|
522
577
|
const config = await loadConfig();
|
|
523
|
-
const response = await
|
|
578
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/security`);
|
|
524
579
|
if (!response.ok)
|
|
525
580
|
throw new Error('Analysis failed');
|
|
526
581
|
const data = await response.json();
|
|
@@ -562,7 +617,7 @@ async function handleMetricsCommand() {
|
|
|
562
617
|
const spinner = createSpinner('Calculating metrics...').start();
|
|
563
618
|
try {
|
|
564
619
|
const config = await loadConfig();
|
|
565
|
-
const response = await
|
|
620
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/metrics`);
|
|
566
621
|
if (!response.ok)
|
|
567
622
|
throw new Error('Analysis failed');
|
|
568
623
|
const data = await response.json();
|
|
@@ -591,9 +646,8 @@ async function handleExportCommand(args) {
|
|
|
591
646
|
const spinner = createSpinner(`Exporting as ${format}...`).start();
|
|
592
647
|
try {
|
|
593
648
|
const config = await loadConfig();
|
|
594
|
-
const response = await
|
|
649
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/export`, {
|
|
595
650
|
method: 'POST',
|
|
596
|
-
headers: { 'Content-Type': 'application/json' },
|
|
597
651
|
body: JSON.stringify({ format }),
|
|
598
652
|
});
|
|
599
653
|
if (!response.ok)
|
|
@@ -647,6 +701,10 @@ async function handleStatusCommand() {
|
|
|
647
701
|
catch {
|
|
648
702
|
// Ignore subscription fetch errors
|
|
649
703
|
}
|
|
704
|
+
// Show session token usage
|
|
705
|
+
if (sessionTokensUsed > 0) {
|
|
706
|
+
console.log(` Session tokens: ${colors.highlight(sessionTokensUsed.toLocaleString())}`);
|
|
707
|
+
}
|
|
650
708
|
}
|
|
651
709
|
if (state.projectId) {
|
|
652
710
|
console.log(` Project: ${colors.primary(state.projectName || state.projectId)}`);
|
|
@@ -664,7 +722,7 @@ async function handleDuplicationCommand() {
|
|
|
664
722
|
const spinner = createSpinner('Analyzing code duplication...').start();
|
|
665
723
|
try {
|
|
666
724
|
const config = await loadConfig();
|
|
667
|
-
const response = await
|
|
725
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/duplication`);
|
|
668
726
|
if (!response.ok)
|
|
669
727
|
throw new Error('Analysis failed');
|
|
670
728
|
const data = await response.json();
|
|
@@ -701,7 +759,7 @@ async function handleRefactoringCommand() {
|
|
|
701
759
|
const spinner = createSpinner('Generating refactoring suggestions...').start();
|
|
702
760
|
try {
|
|
703
761
|
const config = await loadConfig();
|
|
704
|
-
const response = await
|
|
762
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/refactoring`);
|
|
705
763
|
if (!response.ok)
|
|
706
764
|
throw new Error('Analysis failed');
|
|
707
765
|
const data = await response.json();
|
|
@@ -746,7 +804,7 @@ async function handleRulesCommand() {
|
|
|
746
804
|
const spinner = createSpinner('Checking architectural rules...').start();
|
|
747
805
|
try {
|
|
748
806
|
const config = await loadConfig();
|
|
749
|
-
const response = await
|
|
807
|
+
const response = await apiFetch(`${config.serverUrl}/api/projects/${state.projectId}/rules`);
|
|
750
808
|
if (!response.ok)
|
|
751
809
|
throw new Error('Analysis failed');
|
|
752
810
|
const data = await response.json();
|
|
@@ -773,4 +831,35 @@ async function handleRulesCommand() {
|
|
|
773
831
|
throw error;
|
|
774
832
|
}
|
|
775
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
|
+
}
|
|
776
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
|
/**
|