git-repo-analyzer-test 1.0.0

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.
Files changed (65) hide show
  1. package/.github/copilot-instructions.md +108 -0
  2. package/.idea/aianalyzer.iml +9 -0
  3. package/.idea/misc.xml +6 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/API_REFERENCE.md +244 -0
  7. package/ENHANCEMENTS.md +282 -0
  8. package/README.md +179 -0
  9. package/USAGE.md +189 -0
  10. package/analysis.txt +0 -0
  11. package/bin/cli.js +135 -0
  12. package/docs/SONARCLOUD_ANALYSIS_COVERED.md +144 -0
  13. package/docs/SonarCloud_Presentation_Points.md +81 -0
  14. package/docs/UI_IMPROVEMENTS.md +117 -0
  15. package/package-lock_cmd.json +542 -0
  16. package/package.json +44 -0
  17. package/package_command.json +16 -0
  18. package/public/analysis-options.json +31 -0
  19. package/public/images/README.txt +2 -0
  20. package/public/images/rws-logo.png +0 -0
  21. package/public/index.html +2433 -0
  22. package/repositories.example.txt +17 -0
  23. package/sample-repos.txt +20 -0
  24. package/src/analyzers/accessibility.js +47 -0
  25. package/src/analyzers/cicd-enhanced.js +113 -0
  26. package/src/analyzers/codeReview-enhanced.js +599 -0
  27. package/src/analyzers/codeReview-enhanced.js:Zone.Identifier +3 -0
  28. package/src/analyzers/codeReview.js +171 -0
  29. package/src/analyzers/codeReview.js:Zone.Identifier +3 -0
  30. package/src/analyzers/documentation-enhanced.js +137 -0
  31. package/src/analyzers/performance-enhanced.js +747 -0
  32. package/src/analyzers/performance-enhanced.js:Zone.Identifier +3 -0
  33. package/src/analyzers/performance.js +211 -0
  34. package/src/analyzers/performance.js:Zone.Identifier +3 -0
  35. package/src/analyzers/performance_cmd.js +216 -0
  36. package/src/analyzers/quality-enhanced.js +386 -0
  37. package/src/analyzers/quality-enhanced.js:Zone.Identifier +3 -0
  38. package/src/analyzers/quality.js +92 -0
  39. package/src/analyzers/quality.js:Zone.Identifier +3 -0
  40. package/src/analyzers/security-enhanced.js +512 -0
  41. package/src/analyzers/security-enhanced.js:Zone.Identifier +3 -0
  42. package/src/analyzers/snyk-ai.js:Zone.Identifier +3 -0
  43. package/src/analyzers/sonarcloud.js +928 -0
  44. package/src/analyzers/vulnerability.js +185 -0
  45. package/src/analyzers/vulnerability.js:Zone.Identifier +3 -0
  46. package/src/cli.js:Zone.Identifier +3 -0
  47. package/src/config.js +43 -0
  48. package/src/core/analyzerEngine.js +68 -0
  49. package/src/core/reportGenerator.js +21 -0
  50. package/src/gemini.js +321 -0
  51. package/src/github/client.js +124 -0
  52. package/src/github/client.js:Zone.Identifier +3 -0
  53. package/src/index.js +93 -0
  54. package/src/index_cmd.js +130 -0
  55. package/src/openai.js +297 -0
  56. package/src/report/generator.js +459 -0
  57. package/src/report/generator_cmd.js +459 -0
  58. package/src/report/pdf-generator.js +387 -0
  59. package/src/report/pdf-generator.js:Zone.Identifier +3 -0
  60. package/src/server.js +431 -0
  61. package/src/server.js:Zone.Identifier +3 -0
  62. package/src/server_cmd.js +434 -0
  63. package/src/sonarcloud/client.js +365 -0
  64. package/src/sonarcloud/scanner.js +171 -0
  65. package/src.zip +0 -0
package/src/server.js ADDED
@@ -0,0 +1,431 @@
1
+ import dotenv from 'dotenv';
2
+ import express from 'express';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import axios from 'axios';
6
+ import fs from 'fs';
7
+ import { config, safeLogToken } from './config.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ dotenv.config({ path: path.join(__dirname, '../.env') });
12
+ import { analyzeRepository } from './index.js';
13
+ import { PDFReportGenerator } from './report/pdf-generator.js';
14
+ import { cloneAndScan } from './sonarcloud/scanner.js';
15
+ import { SonarCloudClient } from './sonarcloud/client.js';
16
+ import { SonarCloudAnalyzer } from './analyzers/sonarcloud.js';
17
+ process.env.OPENAI_API_KEY="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";
18
+
19
+ function getProjectKeyFromRepo(repository) {
20
+ const parsed = parseRepository(repository);
21
+ if (!parsed) return null;
22
+ const { owner, repo } = parsed;
23
+ const orgRaw = (config.sonarOrgKey || owner).replace(/^["'\s]+|["'\s]+$/g, '').trim();
24
+ const organization = orgRaw.toLowerCase();
25
+ return `${organization}_${repo}`.replace(/\//g, '-');
26
+ }
27
+
28
+ function parseRepository(repository) {
29
+ let owner, repo;
30
+ if (repository.includes('github.com')) {
31
+ const parts = repository
32
+ .replace(/^https?:\/\//, '')
33
+ .replace('github.com/', '')
34
+ .replace(/\.git$/i, '')
35
+ .split('/').filter(Boolean);
36
+ owner = parts[parts.length - 2];
37
+ repo = parts[parts.length - 1];
38
+ } else if (repository.includes('/')) {
39
+ const parts = repository.split('/').filter(Boolean);
40
+ owner = parts[parts.length - 2];
41
+ repo = parts[parts.length - 1];
42
+ }
43
+ return owner && repo ? { owner, repo } : null;
44
+ }
45
+
46
+ const app = express();
47
+ const PORT = process.env.PORT || 3000;
48
+
49
+ // Middleware
50
+ app.use(express.json());
51
+ app.use(express.static(path.join(__dirname, '../public')));
52
+
53
+ // Routes
54
+ app.get('/', (req, res) => {
55
+ res.sendFile(path.join(__dirname, '../public/index.html'));
56
+ });
57
+
58
+ /** Gemini AI analysis: runs in-process using GEMINI_API_KEY. Independent of SonarCloud; no ai-server required. */
59
+ app.post('/api/gemini-analyze', async (req, res) => {
60
+ try {
61
+ const { runGeminiAnalysis, isGeminiConfigured } = await import('./gemini.js');
62
+ if (!isGeminiConfigured()) {
63
+ return res.status(503).json({
64
+ error: 'Gemini is not configured. Add GEMINI_API_KEY to your .env file and restart the server.',
65
+ });
66
+ }
67
+ const { repository, prompt: customPrompt, analysisOptions } = req.body;
68
+ if (!repository) {
69
+ return res.status(400).json({ error: 'Repository URL or owner/repo is required' });
70
+ }
71
+ const parsed = parseRepository(repository);
72
+ if (!parsed) {
73
+ return res.status(400).json({ error: 'Invalid repository. Use owner/repo or a GitHub URL.' });
74
+ }
75
+ const { owner, repo } = parsed;
76
+ const result = await runGeminiAnalysis(repository, customPrompt, analysisOptions);
77
+ return res.json({
78
+ success: true,
79
+ repository: `${owner}/${repo}`,
80
+ analysisMode: 'gemini',
81
+ geminiAnalysis: result.analysis,
82
+ geminiRatings: result.ratings || null,
83
+ metadata: result.metadata || {},
84
+ });
85
+ } catch (err) {
86
+ let message = err.response?.data?.message || err.message;
87
+ if (typeof message === 'string' && (message.includes('429') || message.includes('quota') || message.includes('Quota exceeded'))) {
88
+ message = 'Free tier limit reached. The Analyze button will enable again in about 90 seconds—wait for the countdown, then try again.';
89
+ }
90
+ console.error('Gemini analyze error:', message);
91
+ return res.status(500).json({ error: message });
92
+ }
93
+ });
94
+
95
+ /** Open AI analysis: runs in-process using OPENAI_API_KEY. Fetches repo files (github-fetcher style) and runs one OpenAI scan. */
96
+ app.post('/api/openai-analyze', async (req, res) => {
97
+ try {
98
+ const { runOpenAIAnalysis, isOpenAIConfigured } = await import('./openai.js');
99
+ if (!isOpenAIConfigured()) {
100
+ return res.status(503).json({
101
+ error: 'Open AI is not configured. Add OPENAI_API_KEY to your .env file and restart the server.',
102
+ });
103
+ }
104
+ const { repository, prompt: customPrompt, analysisOptions } = req.body;
105
+ if (!repository) {
106
+ return res.status(400).json({ error: 'Repository URL or owner/repo is required' });
107
+ }
108
+ const parsed = parseRepository(repository);
109
+ if (!parsed) {
110
+ return res.status(400).json({ error: 'Invalid repository. Use owner/repo or a GitHub URL.' });
111
+ }
112
+ const { owner, repo } = parsed;
113
+ const result = await runOpenAIAnalysis(repository, customPrompt, analysisOptions);
114
+ return res.json({
115
+ success: true,
116
+ repository: `${owner}/${repo}`,
117
+ analysisMode: 'openai',
118
+ openaiAnalysis: result.analysis,
119
+ openaiRatings: result.ratings || null,
120
+ metadata: result.metadata || {},
121
+ });
122
+ } catch (err) {
123
+ const message = err.response?.data?.message || err.message;
124
+ console.error('Open AI analyze error:', message);
125
+ return res.status(500).json({ error: message });
126
+ }
127
+ });
128
+
129
+ app.post('/api/analyze', async (req, res) => {
130
+ try {
131
+ const { repository, analysisOptions } = req.body;
132
+
133
+ if (!repository) {
134
+ return res.status(400).json({ error: 'Repository URL is required' });
135
+ }
136
+
137
+ // Parse repository from URL or direct format
138
+ let owner, repo;
139
+
140
+ // Handle various URL formats
141
+ if (repository.includes('github.com')) {
142
+ // https://github.com/owner/repo or https://github.com/owner/repo.git
143
+ const parts = repository
144
+ .replace('https://', '')
145
+ .replace('http://', '')
146
+ .replace('github.com/', '')
147
+ .replace('.git', '')
148
+ .split('/');
149
+ owner = parts[parts.length - 2];
150
+ repo = parts[parts.length - 1];
151
+ } else if (repository.includes('/')) {
152
+ // owner/repo format
153
+ const parts = repository.split('/');
154
+ owner = parts[parts.length - 2];
155
+ repo = parts[parts.length - 1];
156
+ } else {
157
+ return res.status(400).json({
158
+ error: 'Invalid repository format. Use owner/repo or GitHub URL',
159
+ });
160
+ }
161
+
162
+ console.log(`\nšŸ“Š Analyzing: ${owner}/${repo}`);
163
+
164
+ // Run analysis
165
+ const { report, analysis } = await analyzeRepository(owner, repo);
166
+ res.json({
167
+ success: true,
168
+ repository: `${owner}/${repo}`,
169
+ report,
170
+ analysis,
171
+ analysisMode: 'sonar',
172
+ analysisOptions: Array.isArray(analysisOptions) ? analysisOptions : undefined,
173
+ });
174
+ } catch (error) {
175
+ console.error('Analysis error:', error.message);
176
+ res.status(500).json({
177
+ success: false,
178
+ error: error.message,
179
+ });
180
+ }
181
+ });
182
+
183
+ /** Fetch SonarCloud report data by project key or by repository. Project key is derived from SONAR_ORGANIZATION + repo when repository is provided. */
184
+ app.get('/api/sonar/report-data', async (req, res) => {
185
+ try {
186
+ let projectKey = (req.query.projectKey || req.query.id || '').trim();
187
+ if (!projectKey && req.query.repository) {
188
+ const repo = (req.query.repository || '').trim();
189
+ const parsed = parseRepository(repo);
190
+ if (parsed) projectKey = getProjectKeyFromRepo(repo);
191
+ }
192
+ if (!projectKey && config.sonarProjectKey) projectKey = config.sonarProjectKey;
193
+ if (!projectKey) {
194
+ return res.status(400).json({ error: 'Missing projectKey or repository query (e.g. ?repository=owner/repo). Project key is derived from SONAR_ORGANIZATION + repo.' });
195
+ }
196
+ const analyzer = new SonarCloudAnalyzer();
197
+ const report = await analyzer.fetchReportByProjectKey(projectKey);
198
+ res.json(report);
199
+ } catch (err) {
200
+ console.error('SonarCloud report-data error:', err.message, '(token:', safeLogToken() + ')');
201
+ res.status(500).json({
202
+ available: false,
203
+ projectKey: req.query.projectKey || null,
204
+ unavailableReason: err.message || 'Failed to fetch report data.',
205
+ });
206
+ }
207
+ });
208
+
209
+ /** Proxy SonarCloud project overview page so the full report can be embedded in the UI. */
210
+ app.get('/api/sonar/report', async (req, res) => {
211
+ try {
212
+ const projectKey = (req.query.projectKey || req.query.id || '').trim();
213
+ if (!projectKey) {
214
+ return res.status(400).send('Missing projectKey query parameter.');
215
+ }
216
+ const base = config.sonarcloudHost || 'https://sonarcloud.io';
217
+ const url = `${base.replace(/\/$/, '')}/project/overview?id=${encodeURIComponent(projectKey)}`;
218
+ const { data } = await axios.get(url, {
219
+ timeout: 15000,
220
+ responseType: 'text',
221
+ headers: {
222
+ 'Accept': 'text/html',
223
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
224
+ },
225
+ maxRedirects: 5,
226
+ validateStatus: (status) => status < 400,
227
+ });
228
+ const baseHost = config.sonarcloudHost || 'https://sonarcloud.io';
229
+ let html = typeof data === 'string' ? data : '';
230
+ if (html && !html.includes('<base ')) {
231
+ const baseTag = `<base href="${baseHost.replace(/\/$/, '')}/">`;
232
+ if (html.includes('</head>')) {
233
+ html = html.replace('</head>', `${baseTag}</head>`);
234
+ } else if (html.includes('<head>')) {
235
+ html = html.replace('<head>', `<head>${baseTag}`);
236
+ } else {
237
+ html = baseTag + html;
238
+ }
239
+ }
240
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
241
+ res.setHeader('X-Frame-Options', 'SAMEORIGIN');
242
+ res.send(html);
243
+ } catch (err) {
244
+ const status = err.response?.status || 500;
245
+ const message = err.response?.status === 404 ? 'Project not found on SonarCloud.' : (err.message || 'Failed to load report.');
246
+ res.status(status).setHeader('Content-Type', 'text/html').send(
247
+ `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body style="font-family:sans-serif;padding:2rem;color:#666;">` +
248
+ `<p>${message}</p><p>If the project is private, sign in at <a href="https://sonarcloud.io">sonarcloud.io</a> and open the report link in a new tab.</p></body></html>`
249
+ );
250
+ }
251
+ });
252
+
253
+ /** Run a new SonarCloud analysis (create project if needed, clone + scanner). makePublic: true to set visibility to public. */
254
+ app.post('/api/sonar/run-scan', async (req, res) => {
255
+ try {
256
+ const { repository, makePublic } = req.body;
257
+ if (!repository) {
258
+ return res.status(400).json({ error: 'repository is required (e.g. owner/repo or GitHub URL)' });
259
+ }
260
+ const parsed = parseRepository(repository);
261
+ if (!parsed) {
262
+ return res.status(400).json({ error: 'Invalid repository. Use owner/repo or a GitHub URL.' });
263
+ }
264
+ const { owner, repo } = parsed;
265
+ const token = config.sonarToken;
266
+ if (!token) {
267
+ return res.status(400).json({ error: 'SONAR_TOKEN not set in .env' });
268
+ }
269
+ const orgRaw = (config.sonarOrgKey || owner).replace(/^["'\s]+|["'\s]+$/g, '').trim();
270
+ const organization = orgRaw.toLowerCase();
271
+ const projectKey = `${organization}_${repo}`.replace(/\//g, '-');
272
+ const projectName = `${owner}/${repo}`;
273
+ const repoUrl = `https://github.com/${owner}/${repo}`;
274
+ const sonarClient = new SonarCloudClient(token);
275
+ await sonarClient.createProject(projectKey, projectName, organization);
276
+ console.log(`\nšŸ“¦ SonarCloud: running new analysis for ${owner}/${repo} (org: ${organization})...`);
277
+ await cloneAndScan(repoUrl, projectKey, projectName, organization, token);
278
+ await new Promise((r) => setTimeout(r, 5000));
279
+ if (makePublic === true || makePublic === 'true') {
280
+ await sonarClient.updateProjectVisibility(projectKey, 'public');
281
+ console.log(`āœ… SonarCloud: scan uploaded for ${owner}/${repo} (project set to public)`);
282
+ } else {
283
+ console.log(`āœ… SonarCloud: scan uploaded for ${owner}/${repo}`);
284
+ }
285
+ res.json({
286
+ success: true,
287
+ message: 'Analysis uploaded. Wait 1–2 minutes then click "Refresh SonarCloud results" or "Retry fetching metrics".',
288
+ });
289
+ } catch (error) {
290
+ console.error('SonarCloud run-scan error:', error.message, '(token:', safeLogToken() + ')');
291
+ res.status(500).json({
292
+ success: false,
293
+ error: error.message || 'Run scan failed',
294
+ });
295
+ }
296
+ });
297
+
298
+ /** Re-fetch SonarCloud results only (no clone, no scan). Pass repository or projectKey. Project key is derived from SONAR_ORGANIZATION + repo when repository is provided. */
299
+ app.post('/api/sonar/refresh', async (req, res) => {
300
+ try {
301
+ const { repository, projectKey: bodyProjectKey } = req.body;
302
+ let projectKey = (bodyProjectKey || '').trim();
303
+ const parsed = repository ? parseRepository(repository) : null;
304
+ if (!projectKey && parsed) {
305
+ projectKey = getProjectKeyFromRepo(repository);
306
+ }
307
+ if (!projectKey && config.sonarProjectKey) projectKey = config.sonarProjectKey;
308
+ if (!projectKey) {
309
+ return res.status(400).json({
310
+ error: 'repository or projectKey is required. Project key is derived from SONAR_ORGANIZATION + repo when you pass repository.',
311
+ available: false,
312
+ });
313
+ }
314
+ const analyzer = new SonarCloudAnalyzer();
315
+ if (parsed && analyzer.client.token && !(bodyProjectKey || '').trim()) {
316
+ try {
317
+ const resolved = await analyzer.client.resolveProjectKey(parsed.owner, parsed.repo);
318
+ if (resolved && resolved !== projectKey) {
319
+ projectKey = resolved;
320
+ }
321
+ } catch (_) {}
322
+ }
323
+ const report = await analyzer.fetchReportByProjectKey(projectKey);
324
+ res.json(report);
325
+ } catch (err) {
326
+ console.error('SonarCloud refresh error:', err.message, '(token:', safeLogToken() + ')');
327
+ res.status(500).json({
328
+ available: false,
329
+ projectKey: req.body?.projectKey || (req.body?.repository ? getProjectKeyFromRepo(req.body.repository) : null) || null,
330
+ unavailableReason: err.message || 'Failed to refresh SonarCloud data.',
331
+ });
332
+ }
333
+ });
334
+
335
+ /** Normalize client payload so PDF generator always receives { repository, report: { summary }, analysis, geminiAnalysis?, openaiAnalysis? }.
336
+ * Request body often has no "report" (e.g. Gemini/OpenAI or multi). We build report.summary from sonar or defaults. */
337
+ function normalizePayloadForPdf(data) {
338
+ if (!data || typeof data !== 'object') data = {};
339
+ const repo = data.repository || '';
340
+ const incomingReport = data.report; // optional: only Sonar-only response sends this
341
+
342
+ let summary = (incomingReport != null && incomingReport.summary != null && typeof incomingReport.summary === 'object')
343
+ ? { ...incomingReport.summary }
344
+ : {};
345
+
346
+ let analysis = (data.analysis != null && typeof data.analysis === 'object') ? { ...data.analysis } : {};
347
+
348
+ const sc = data.sonar?.analysis?.sonarCloud || data.sonar?.sonarCloud;
349
+ if (sc) {
350
+ analysis.sonarCloud = sc;
351
+ if (sc.available && (sc.score != null || sc.overallSummary)) {
352
+ const score = sc.score != null ? sc.score : (sc.overallSummary?.score100 != null ? sc.overallSummary.score100 / 10 : null);
353
+ summary.sonarCloudScore = score;
354
+ summary.overallScore = score;
355
+ summary.overallScore100 = score != null ? Math.round(score * 10) : null;
356
+ summary.overallRating = sc.rating || sc.overallSummary?.rating || null;
357
+ if (!summary.healthStatus) summary.healthStatus = score >= 9 ? 'Excellent' : score >= 7 ? 'Good' : score >= 5 ? 'Fair' : 'Poor';
358
+ }
359
+ }
360
+
361
+ if (!summary.healthStatus) summary.healthStatus = 'Unavailable';
362
+ if (summary.overallScore100 == null && summary.overallScore == null) summary.overallScore100 = null;
363
+
364
+ const report = { summary };
365
+ return {
366
+ repository: repo,
367
+ report,
368
+ analysis,
369
+ geminiAnalysis: data.gemini?.geminiAnalysis ?? data.geminiAnalysis ?? null,
370
+ openaiAnalysis: data.openai?.openaiAnalysis ?? data.openaiAnalysis ?? null,
371
+ geminiSelectedOptions: Array.isArray(data.geminiSelectedOptions) ? data.geminiSelectedOptions : null,
372
+ openaiSelectedOptions: Array.isArray(data.openaiSelectedOptions) ? data.openaiSelectedOptions : null,
373
+ sonarSelectedOptions: Array.isArray(data.sonarSelectedOptions) ? data.sonarSelectedOptions : null,
374
+ };
375
+ }
376
+
377
+ app.post('/api/export-pdf', async (req, res) => {
378
+ try {
379
+ const raw = req.body;
380
+
381
+ if (!raw || !raw.repository) {
382
+ return res.status(400).json({ error: 'Invalid data for PDF export' });
383
+ }
384
+
385
+ let data = normalizePayloadForPdf(raw);
386
+ if (!data.report || typeof data.report !== 'object') data = { ...data, report: {} };
387
+ if (data.report.summary == null || typeof data.report.summary !== 'object') {
388
+ data.report = { ...data.report, summary: { healthStatus: 'Unavailable', overallScore100: null, overallScore: null, overallRating: null } };
389
+ }
390
+
391
+ const reportsDir = path.join(__dirname, '../reports');
392
+ if (!fs.existsSync(reportsDir)) {
393
+ fs.mkdirSync(reportsDir, { recursive: true });
394
+ }
395
+
396
+ const sanitizedRepo = data.repository.replace(/\//g, '-');
397
+ const timestamp = Date.now();
398
+ const filename = path.join(reportsDir, `${sanitizedRepo}-${timestamp}.pdf`);
399
+
400
+ console.log(`\nšŸ“„ Generating PDF report: ${sanitizedRepo}`);
401
+
402
+ await PDFReportGenerator.generatePDF(data, filename);
403
+
404
+ // Read the file and send it
405
+ const fileStream = fs.createReadStream(filename);
406
+ res.setHeader('Content-Type', 'application/pdf');
407
+ res.setHeader(
408
+ 'Content-Disposition',
409
+ `attachment; filename="${sanitizedRepo}-report.pdf"`
410
+ );
411
+
412
+ fileStream.pipe(res);
413
+
414
+ // Optionally clean up after sending
415
+ fileStream.on('end', () => {
416
+ // Keep the file in reports directory
417
+ });
418
+ } catch (error) {
419
+ console.error('PDF export error:', error.message);
420
+ res.status(500).json({
421
+ success: false,
422
+ error: 'Failed to generate PDF: ' + error.message,
423
+ });
424
+ }
425
+ });
426
+
427
+ app.listen(PORT, () => {
428
+ console.log(`\nšŸš€ GitHub Repository Analyzer Web Server`);
429
+ console.log(`šŸ“ Running on http://localhost:${PORT}`);
430
+ console.log(`\nOpen your browser and navigate to: http://localhost:${PORT}`);
431
+ });
@@ -0,0 +1,3 @@
1
+ [ZoneTransfer]
2
+ ZoneId=3
3
+ ReferrerUrl=C:\Users\jitendra.yadav\Downloads\GitRepoAnalyzer - Shabbir.zip