coverme-scanner 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 (46) hide show
  1. package/README.md +227 -0
  2. package/commands/scan.md +317 -0
  3. package/dist/cli/index.d.ts +3 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +39 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli/init.d.ts +6 -0
  8. package/dist/cli/init.d.ts.map +1 -0
  9. package/dist/cli/init.js +636 -0
  10. package/dist/cli/init.js.map +1 -0
  11. package/dist/cli/scan.d.ts +11 -0
  12. package/dist/cli/scan.d.ts.map +1 -0
  13. package/dist/cli/scan.js +498 -0
  14. package/dist/cli/scan.js.map +1 -0
  15. package/dist/report/generator.d.ts +48 -0
  16. package/dist/report/generator.d.ts.map +1 -0
  17. package/dist/report/generator.js +368 -0
  18. package/dist/report/generator.js.map +1 -0
  19. package/dist/report/index.d.ts +35 -0
  20. package/dist/report/index.d.ts.map +1 -0
  21. package/dist/report/index.js +463 -0
  22. package/dist/report/index.js.map +1 -0
  23. package/dist/templates/report.html +796 -0
  24. package/dist/types.d.ts +94 -0
  25. package/dist/types.d.ts.map +1 -0
  26. package/dist/types.js +3 -0
  27. package/dist/types.js.map +1 -0
  28. package/package.json +48 -0
  29. package/src/cli/index.ts +43 -0
  30. package/src/cli/init.ts +611 -0
  31. package/src/cli/scan.ts +483 -0
  32. package/src/prompts/architecture-reviewer.md +171 -0
  33. package/src/prompts/consensus-builder.md +247 -0
  34. package/src/prompts/context-discovery.md +174 -0
  35. package/src/prompts/cross-validator.md +224 -0
  36. package/src/prompts/deep-dive-expert.md +224 -0
  37. package/src/prompts/dependency-auditor.md +190 -0
  38. package/src/prompts/performance-hunter.md +200 -0
  39. package/src/prompts/quality-analyzer.md +150 -0
  40. package/src/prompts/report-generator.md +285 -0
  41. package/src/prompts/security-scanner.md +180 -0
  42. package/src/report/generator.ts +382 -0
  43. package/src/report/index.ts +483 -0
  44. package/src/templates/report.html +796 -0
  45. package/src/types.ts +107 -0
  46. package/tsconfig.json +20 -0
@@ -0,0 +1,483 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import puppeteer from 'puppeteer';
4
+
5
+ export interface Finding {
6
+ id: string;
7
+ title: string;
8
+ severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
9
+ category: string;
10
+ file?: string;
11
+ line?: number;
12
+ description: string;
13
+ code?: string;
14
+ recommendation: string;
15
+ confidence?: number;
16
+ cwe?: string;
17
+ why?: string;
18
+ context?: string;
19
+ checkBefore?: string;
20
+ }
21
+
22
+ export interface FalsePositive {
23
+ id: string;
24
+ title: string;
25
+ file?: string;
26
+ reason: string;
27
+ }
28
+
29
+ export interface ScanReport {
30
+ projectName: string;
31
+ scanDate: string;
32
+ findings: Finding[];
33
+ falsePositives: FalsePositive[];
34
+ positiveObservations: string[];
35
+ scanDuration?: number;
36
+ agentCount?: number;
37
+ }
38
+
39
+ function escapeHtml(text: string): string {
40
+ if (!text) return '';
41
+ return text
42
+ .replace(/&/g, '&')
43
+ .replace(/</g, '&lt;')
44
+ .replace(/>/g, '&gt;')
45
+ .replace(/"/g, '&quot;')
46
+ .replace(/'/g, '&#039;');
47
+ }
48
+
49
+ function calculateScore(findings: Finding[]): { grade: string; value: number } {
50
+ const critical = findings.filter(f => f.severity === 'critical').length;
51
+ const high = findings.filter(f => f.severity === 'high').length;
52
+ const medium = findings.filter(f => f.severity === 'medium').length;
53
+ const low = findings.filter(f => f.severity === 'low').length;
54
+
55
+ let penalty = 0;
56
+ penalty += Math.min(critical * 20, 50);
57
+ penalty += Math.min(high * 5, 30);
58
+ penalty += Math.min(medium * 2, 15);
59
+ penalty += Math.min(low * 0.5, 5);
60
+
61
+ const score = Math.max(0, Math.round(100 - penalty));
62
+
63
+ let grade: string;
64
+ if (critical > 0) grade = 'f';
65
+ else if (score >= 90) grade = 'a';
66
+ else if (score >= 75) grade = 'b';
67
+ else if (score >= 60) grade = 'c';
68
+ else if (score >= 40) grade = 'd';
69
+ else grade = 'f';
70
+
71
+ return { grade, value: score };
72
+ }
73
+
74
+ function generateSummary(findings: Finding[]): string {
75
+ const critical = findings.filter(f => f.severity === 'critical').length;
76
+ const high = findings.filter(f => f.severity === 'high').length;
77
+ const total = findings.length;
78
+
79
+ if (critical > 0) {
80
+ return `This scan identified ${total} findings, including ${critical} critical issue${critical > 1 ? 's' : ''} requiring immediate attention.${high > 0 ? ` Additionally, ${high} high-priority issues were found.` : ''} Immediate remediation is recommended.`;
81
+ }
82
+ if (high > 0) {
83
+ return `This scan identified ${total} findings, with ${high} high-priority issue${high > 1 ? 's' : ''} that should be addressed soon. No critical vulnerabilities were detected.`;
84
+ }
85
+ if (total > 0) {
86
+ return `This scan identified ${total} findings, primarily medium and low priority items. No critical or high-severity issues were detected.`;
87
+ }
88
+ return `Excellent! No significant security or quality issues were found.`;
89
+ }
90
+
91
+ function renderFinding(f: Finding, severity: string): string {
92
+ const dataAttrs = `data-finding-id="${escapeHtml(f.id)}" data-file="${escapeHtml(f.file || '')}" data-line="${f.line || ''}" data-description="${escapeHtml(f.description)}" data-recommendation="${escapeHtml(f.recommendation)}" data-severity="${severity}" data-why="${escapeHtml(f.why || '')}" data-context="${escapeHtml(f.context || '')}" data-checkbefore="${escapeHtml(f.checkBefore || '')}"`;
93
+
94
+ const whyBox = f.why ? `
95
+ <div class="info-box info-why">
96
+ <span class="info-label">Why This Matters</span>
97
+ <p>${escapeHtml(f.why)}</p>
98
+ </div>` : '';
99
+
100
+ const contextBox = f.context ? `
101
+ <div class="info-box info-context">
102
+ <span class="info-label">Code Context</span>
103
+ <p>${escapeHtml(f.context)}</p>
104
+ </div>` : '';
105
+
106
+ const checkBox = f.checkBefore ? `
107
+ <div class="info-box info-check">
108
+ <span class="info-label">Check Before Fixing</span>
109
+ <p>${escapeHtml(f.checkBefore)}</p>
110
+ </div>` : '';
111
+
112
+ const codeBlock = f.code ? `
113
+ <div class="code-block"><code>${escapeHtml(f.code)}</code></div>` : '';
114
+
115
+ return `
116
+ <div class="finding" ${dataAttrs}>
117
+ <div class="finding-header" onclick="this.parentElement.classList.toggle('open')">
118
+ <span class="severity-badge sev-${severity}">${severity}</span>
119
+ <span class="finding-title">${escapeHtml(f.title)}</span>
120
+ <span class="finding-file">${f.file ? escapeHtml(f.file) : ''}</span>
121
+ <svg class="expand-arrow" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
122
+ </div>
123
+ <div class="finding-body">
124
+ <div class="location">
125
+ <span class="location-file">${escapeHtml(f.file || '')}</span>
126
+ <span class="location-line">${f.line ? ':' + f.line : ''}</span>
127
+ <button class="copy-btn" onclick="event.stopPropagation(); copyText('${escapeHtml(f.file || '')}:${f.line || ''}', this)">Copy</button>
128
+ </div>
129
+
130
+ <div class="info-grid">
131
+ <div class="info-box info-problem">
132
+ <span class="info-label">Problem</span>
133
+ <p>${escapeHtml(f.description)}</p>
134
+ </div>
135
+ ${whyBox}
136
+ ${contextBox}
137
+ ${checkBox}
138
+ ${codeBlock}
139
+ <div class="info-box info-fix">
140
+ <span class="info-label">How to Fix</span>
141
+ <p>${escapeHtml(f.recommendation)}</p>
142
+ </div>
143
+ </div>
144
+
145
+ <div class="prompt-box">
146
+ <div class="prompt-header">
147
+ <span class="prompt-label">Claude Code Prompt</span>
148
+ <button class="copy-btn" onclick="event.stopPropagation(); copyPrompt(this.closest('.finding'))">Copy</button>
149
+ </div>
150
+ <div class="prompt-text">Fix ${escapeHtml(f.id)} in ${escapeHtml(f.file || '')}:${f.line || ''}
151
+
152
+ Problem: ${escapeHtml(f.description)}
153
+ ${f.why ? `\nWhy: ${escapeHtml(f.why)}` : ''}
154
+ ${f.context ? `\nContext: ${escapeHtml(f.context)}` : ''}
155
+ ${f.checkBefore ? `\nCheck first: ${escapeHtml(f.checkBefore)}` : ''}
156
+
157
+ Solution: ${escapeHtml(f.recommendation)}</div>
158
+ </div>
159
+ </div>
160
+ </div>`;
161
+ }
162
+
163
+ export function generateHtml(report: ScanReport): string {
164
+ const { grade, value } = calculateScore(report.findings);
165
+
166
+ const critical = report.findings.filter(f => f.severity === 'critical');
167
+ const high = report.findings.filter(f => f.severity === 'high');
168
+ const medium = report.findings.filter(f => f.severity === 'medium');
169
+ const low = report.findings.filter(f => f.severity === 'low' || f.severity === 'info');
170
+
171
+ const criticalSection = critical.length > 0 ? `
172
+ <section>
173
+ <h2 class="section-title">Critical Issues</h2>
174
+ ${critical.map(f => renderFinding(f, 'critical')).join('\n')}
175
+ </section>` : '';
176
+
177
+ const highSection = high.length > 0 ? `
178
+ <section>
179
+ <h2 class="section-title">High Priority</h2>
180
+ ${high.map(f => renderFinding(f, 'high')).join('\n')}
181
+ </section>` : '';
182
+
183
+ const mediumSection = medium.length > 0 ? `
184
+ <section>
185
+ <h2 class="section-title">Medium Priority</h2>
186
+ ${medium.map(f => renderFinding(f, 'medium')).join('\n')}
187
+ </section>` : '';
188
+
189
+ const lowSection = low.length > 0 ? `
190
+ <section>
191
+ <h2 class="section-title">Low Priority</h2>
192
+ ${low.map(f => renderFinding(f, 'low')).join('\n')}
193
+ </section>` : '';
194
+
195
+ const positiveSection = report.positiveObservations.length > 0 ? `
196
+ <section>
197
+ <h2 class="section-title">What's Good</h2>
198
+ <ul class="positive-list">
199
+ ${report.positiveObservations.map(obs => `
200
+ <li class="positive-item">
201
+ <span class="check-icon">&#10003;</span>
202
+ <span>${escapeHtml(obs)}</span>
203
+ </li>
204
+ `).join('\n')}
205
+ </ul>
206
+ </section>` : '';
207
+
208
+ return `<!DOCTYPE html>
209
+ <html lang="en">
210
+ <head>
211
+ <meta charset="UTF-8">
212
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
213
+ <title>${escapeHtml(report.projectName)} - Security Report</title>
214
+ <style>
215
+ :root {
216
+ --bg: #fafafa;
217
+ --card: #ffffff;
218
+ --text: #111111;
219
+ --text-muted: #666666;
220
+ --text-light: #999999;
221
+ --border: #eaeaea;
222
+ --accent: #000000;
223
+ --critical: #e11d48;
224
+ --high: #f97316;
225
+ --medium: #eab308;
226
+ --low: #3b82f6;
227
+ --success: #10b981;
228
+ --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
229
+ --mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', monospace;
230
+ }
231
+ * { margin: 0; padding: 0; box-sizing: border-box; }
232
+ body { font-family: var(--font); font-size: 15px; line-height: 1.7; color: var(--text); background: var(--bg); -webkit-font-smoothing: antialiased; }
233
+ .container { max-width: 800px; margin: 0 auto; padding: 60px 24px; }
234
+
235
+ header { text-align: center; margin-bottom: 60px; }
236
+ .logo { font-size: 11px; font-weight: 600; letter-spacing: 2px; text-transform: uppercase; color: var(--text-light); margin-bottom: 20px; }
237
+ h1 { font-size: 42px; font-weight: 700; letter-spacing: -1px; margin-bottom: 8px; }
238
+ .date { color: var(--text-muted); font-size: 14px; }
239
+
240
+ .score-section { display: flex; justify-content: center; gap: 40px; margin: 50px 0; padding: 40px; background: var(--card); border-radius: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.04); }
241
+ .score-circle { width: 100px; height: 100px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px; font-weight: 700; color: white; text-transform: uppercase; }
242
+ .grade-a { background: var(--success); }
243
+ .grade-b { background: #22c55e; }
244
+ .grade-c { background: var(--medium); }
245
+ .grade-d { background: var(--high); }
246
+ .grade-f { background: var(--critical); }
247
+ .stats { display: flex; align-items: center; gap: 32px; }
248
+ .stat { text-align: center; }
249
+ .stat-num { font-size: 28px; font-weight: 700; line-height: 1; }
250
+ .stat-label { font-size: 11px; text-transform: uppercase; letter-spacing: 1px; color: var(--text-light); margin-top: 6px; }
251
+ .stat-critical .stat-num { color: var(--critical); }
252
+ .stat-high .stat-num { color: var(--high); }
253
+ .stat-medium .stat-num { color: var(--medium); }
254
+ .stat-low .stat-num { color: var(--low); }
255
+
256
+ .copy-all { text-align: center; margin: 40px 0; }
257
+ .copy-all-btn { display: inline-flex; align-items: center; gap: 10px; padding: 16px 32px; background: var(--accent); color: white; border: none; border-radius: 100px; font-size: 14px; font-weight: 600; cursor: pointer; transition: transform 0.2s, opacity 0.2s; }
258
+ .copy-all-btn:hover { transform: translateY(-2px); opacity: 0.9; }
259
+ .copy-all-btn.copied { background: var(--success); }
260
+ .copy-hint { margin-top: 10px; font-size: 13px; color: var(--text-light); }
261
+
262
+ section { margin-bottom: 50px; }
263
+ .section-title { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 1.5px; color: var(--text-light); margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
264
+
265
+ .finding { background: var(--card); border-radius: 12px; margin-bottom: 16px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.04); border: 1px solid var(--border); }
266
+ .finding-header { display: flex; align-items: center; gap: 16px; padding: 20px 24px; cursor: pointer; transition: background 0.15s; }
267
+ .finding-header:hover { background: var(--bg); }
268
+ .severity-badge { padding: 4px 10px; border-radius: 100px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; color: white; }
269
+ .sev-critical { background: var(--critical); }
270
+ .sev-high { background: var(--high); }
271
+ .sev-medium { background: var(--medium); color: #000; }
272
+ .sev-low { background: var(--low); }
273
+ .finding-title { flex: 1; font-weight: 600; font-size: 15px; }
274
+ .finding-file { font-family: var(--mono); font-size: 12px; color: var(--text-light); }
275
+ .expand-arrow { color: var(--text-light); transition: transform 0.2s; }
276
+ .finding.open .expand-arrow { transform: rotate(180deg); }
277
+
278
+ .finding-body { display: none; padding: 0 24px 24px; }
279
+ .finding.open .finding-body { display: block; }
280
+
281
+ .location { display: inline-flex; align-items: center; gap: 8px; padding: 8px 14px; background: #f4f4f5; border-radius: 8px; font-family: var(--mono); font-size: 13px; margin-bottom: 20px; }
282
+ .location-file { color: var(--text); }
283
+ .location-line { color: var(--text-muted); }
284
+ .copy-btn { padding: 4px 10px; background: white; border: 1px solid var(--border); border-radius: 6px; font-size: 11px; cursor: pointer; margin-left: 8px; }
285
+ .copy-btn:hover { background: var(--bg); }
286
+
287
+ .info-grid { display: flex; flex-direction: column; gap: 12px; margin: 20px 0; }
288
+ .info-box { padding: 16px 20px; border-radius: 10px; font-size: 14px; line-height: 1.6; }
289
+ .info-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; display: block; }
290
+ .info-problem { background: #fef2f2; border-left: 3px solid var(--critical); }
291
+ .info-problem .info-label { color: var(--critical); }
292
+ .info-why { background: #fff7ed; border-left: 3px solid var(--high); }
293
+ .info-why .info-label { color: var(--high); }
294
+ .info-context { background: #f0f9ff; border-left: 3px solid var(--low); }
295
+ .info-context .info-label { color: var(--low); }
296
+ .info-check { background: #fefce8; border-left: 3px solid var(--medium); }
297
+ .info-check .info-label { color: #a16207; }
298
+ .info-fix { background: #f0fdf4; border-left: 3px solid var(--success); }
299
+ .info-fix .info-label { color: var(--success); }
300
+
301
+ .code-block { background: #18181b; border-radius: 10px; padding: 16px 20px; margin: 16px 0; overflow-x: auto; }
302
+ .code-block code { font-family: var(--mono); font-size: 13px; color: #e4e4e7; line-height: 1.6; white-space: pre; }
303
+
304
+ .prompt-box { margin-top: 20px; padding: 16px 20px; background: #18181b; border-radius: 10px; }
305
+ .prompt-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
306
+ .prompt-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: #71717a; }
307
+ .prompt-text { font-family: var(--mono); font-size: 12px; color: #a1a1aa; line-height: 1.7; white-space: pre-wrap; }
308
+
309
+ .summary-box { background: var(--card); border-radius: 12px; padding: 24px; margin-bottom: 20px; border: 1px solid var(--border); }
310
+ .summary-text { color: var(--text-muted); line-height: 1.8; }
311
+
312
+ .positive-list { list-style: none; }
313
+ .positive-item { display: flex; align-items: flex-start; gap: 14px; padding: 14px 0; border-bottom: 1px solid var(--border); }
314
+ .positive-item:last-child { border-bottom: none; }
315
+ .check-icon { width: 22px; height: 22px; background: #dcfce7; color: var(--success); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0; }
316
+
317
+ footer { margin-top: 80px; padding-top: 30px; border-top: 1px solid var(--border); text-align: center; font-size: 13px; color: var(--text-light); }
318
+
319
+ @media (max-width: 768px) {
320
+ .container { padding: 30px 16px; }
321
+ h1 { font-size: 28px; }
322
+ .score-section { flex-direction: column; gap: 24px; padding: 24px; }
323
+ .stats { flex-wrap: wrap; justify-content: center; gap: 20px; }
324
+ .finding-header { flex-wrap: wrap; gap: 10px; padding: 16px; }
325
+ .finding-title { width: 100%; order: 2; }
326
+ .finding-file { width: 100%; order: 3; }
327
+ }
328
+
329
+ @media print {
330
+ .finding-body { display: block !important; }
331
+ .copy-all, .copy-btn, .prompt-box { display: none; }
332
+ .finding { break-inside: avoid; }
333
+ }
334
+ </style>
335
+ </head>
336
+ <body>
337
+ <div class="container">
338
+ <header>
339
+ <div class="logo">Security Report</div>
340
+ <h1>${escapeHtml(report.projectName)}</h1>
341
+ <p class="date">${report.scanDate}</p>
342
+ </header>
343
+
344
+ <div class="score-section">
345
+ <div class="score-circle grade-${grade}">${grade}</div>
346
+ <div class="stats">
347
+ <div class="stat stat-critical">
348
+ <div class="stat-num">${critical.length}</div>
349
+ <div class="stat-label">Critical</div>
350
+ </div>
351
+ <div class="stat stat-high">
352
+ <div class="stat-num">${high.length}</div>
353
+ <div class="stat-label">High</div>
354
+ </div>
355
+ <div class="stat stat-medium">
356
+ <div class="stat-num">${medium.length}</div>
357
+ <div class="stat-label">Medium</div>
358
+ </div>
359
+ <div class="stat stat-low">
360
+ <div class="stat-num">${low.length}</div>
361
+ <div class="stat-label">Low</div>
362
+ </div>
363
+ </div>
364
+ </div>
365
+
366
+ <div class="summary-box">
367
+ <p class="summary-text">${generateSummary(report.findings)}</p>
368
+ </div>
369
+
370
+ <div class="copy-all">
371
+ <button class="copy-all-btn" onclick="copyAllFixes(this)">
372
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
373
+ <rect x="9" y="9" width="13" height="13" rx="2"></rect>
374
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
375
+ </svg>
376
+ Copy All Fixes for Claude Code
377
+ </button>
378
+ <p class="copy-hint">Paste directly into Claude Code to fix everything</p>
379
+ </div>
380
+
381
+ ${criticalSection}
382
+ ${highSection}
383
+ ${mediumSection}
384
+ ${lowSection}
385
+ ${positiveSection}
386
+
387
+ <footer>
388
+ <p>Generated by CoverMe Scanner</p>
389
+ <p>${report.scanDuration ? Math.round(report.scanDuration / 1000) + 's' : ''}${report.agentCount ? ' · ' + report.agentCount + ' agents' : ''}</p>
390
+ </footer>
391
+ </div>
392
+
393
+ <script>
394
+ function copyText(text, btn) {
395
+ navigator.clipboard.writeText(text).then(() => {
396
+ btn.textContent = 'Copied!';
397
+ setTimeout(() => btn.textContent = 'Copy', 1500);
398
+ });
399
+ }
400
+
401
+ function copyPrompt(el) {
402
+ const text = el.querySelector('.prompt-text').textContent;
403
+ const btn = el.querySelector('.prompt-box .copy-btn');
404
+ copyText(text, btn);
405
+ }
406
+
407
+ function copyAllFixes(btn) {
408
+ const findings = document.querySelectorAll('.finding[data-finding-id]');
409
+ let prompt = 'Fix these security issues:\\n\\n';
410
+
411
+ findings.forEach((f, i) => {
412
+ prompt += '## ' + (i + 1) + '. [' + f.dataset.severity.toUpperCase() + '] ' + f.dataset.findingId + '\\n';
413
+ prompt += 'File: ' + f.dataset.file + ':' + f.dataset.line + '\\n';
414
+ prompt += 'Problem: ' + f.dataset.description + '\\n';
415
+ if (f.dataset.why) prompt += 'Why: ' + f.dataset.why + '\\n';
416
+ if (f.dataset.checkbefore) prompt += 'Check first: ' + f.dataset.checkbefore + '\\n';
417
+ prompt += 'Fix: ' + f.dataset.recommendation + '\\n\\n';
418
+ });
419
+
420
+ navigator.clipboard.writeText(prompt).then(() => {
421
+ btn.classList.add('copied');
422
+ btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 6L9 17l-5-5"/></svg> Copied!';
423
+ setTimeout(() => {
424
+ btn.classList.remove('copied');
425
+ btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy All Fixes for Claude Code';
426
+ }, 2500);
427
+ });
428
+ }
429
+
430
+ window.onbeforeprint = () => document.querySelectorAll('.finding').forEach(f => f.classList.add('open'));
431
+ </script>
432
+ </body>
433
+ </html>`;
434
+ }
435
+
436
+ export async function generatePdf(report: ScanReport, outputPath: string): Promise<void> {
437
+ const html = generateHtml(report);
438
+
439
+ const browser = await puppeteer.launch({
440
+ headless: true,
441
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
442
+ });
443
+
444
+ const page = await browser.newPage();
445
+ await page.setViewport({ width: 900, height: 800 });
446
+ await page.setContent(html, { waitUntil: 'networkidle0' });
447
+ await page.evaluate('document.querySelectorAll(".finding").forEach(f => f.classList.add("open"))');
448
+
449
+ const dimensions = await page.evaluate(`({
450
+ width: document.body.scrollWidth,
451
+ height: document.body.scrollHeight
452
+ })`) as { width: number; height: number };
453
+
454
+ await page.pdf({
455
+ path: outputPath,
456
+ width: '900px',
457
+ height: `${dimensions.height + 80}px`,
458
+ margin: { top: '40px', right: '40px', bottom: '40px', left: '40px' },
459
+ printBackground: true
460
+ });
461
+
462
+ await browser.close();
463
+ console.log(`PDF generated: ${outputPath}`);
464
+ }
465
+
466
+ export async function generateReport(
467
+ jsonPath: string,
468
+ outputPath?: string,
469
+ format: 'pdf' | 'html' = 'pdf'
470
+ ): Promise<void> {
471
+ const reportData = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')) as ScanReport;
472
+
473
+ const ext = format === 'pdf' ? '.pdf' : '.html';
474
+ const finalPath = outputPath || jsonPath.replace('.json', ext);
475
+
476
+ if (format === 'pdf') {
477
+ await generatePdf(reportData, finalPath);
478
+ } else {
479
+ const html = generateHtml(reportData);
480
+ fs.writeFileSync(finalPath, html);
481
+ console.log(`HTML generated: ${finalPath}`);
482
+ }
483
+ }