diff --git a/README.md b/README.md index 1a339f7..5de60e6 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ RepoScope/ │ ├── Board.tsx # Section-by-section status board │ ├── CommandBlock.tsx # Code block with copy-to-clipboard │ ├── ErrorCard.tsx # Individual CI failure card +│ ├── ExportButtons.tsx # JSON and Markdown audit downloads │ ├── RepoInput.tsx # Repository URL input + token settings │ ├── ScoreBoard.tsx # Health-score ring + detailed breakdown │ ├── SmartDiagnosis.tsx # AI-powered diagnosis with fix commands @@ -208,6 +209,7 @@ RepoScope/ │ ├── github-error-patterns.ts # Known error patterns & solutions │ ├── github-health-monitor.ts # Repository health metrics │ ├── github-log-parser.ts # Parse GitHub Actions logs +│ ├── export.ts # JSON and Markdown audit serializers │ └── utils.ts # Helpers: parseRepo(), decodeBase64Utf8() │ ├── public/ # Static assets diff --git a/app/page.tsx b/app/page.tsx index 0a60749..cfd1611 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -8,6 +8,7 @@ import AuditTabs from '@/components/AuditTabs'; import ActionsPanel from '@/components/ActionsPanel'; import TokenInput from '@/components/TokenInput'; import ActionsDashboard from '@/components/ActionsDashboard'; +import ExportButtons from '@/components/ExportButtons'; import { parseRepo, decodeBase64Utf8 } from '@/lib/utils'; import { evaluateTarget, generateFindings, scoreAndExplain } from '@/lib/audit'; @@ -190,6 +191,7 @@ export default function Home() { + diff --git a/components/ExportButtons.tsx b/components/ExportButtons.tsx new file mode 100644 index 0000000..d4237ad --- /dev/null +++ b/components/ExportButtons.tsx @@ -0,0 +1,49 @@ +'use client'; + +import { useState } from 'react'; +import { buildAuditJson, buildAuditMarkdown, AuditExportResult } from '@/lib/export'; + +function download(name: string, content: string, type: string) { + const blob = new Blob([content], { type }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = name; + link.click(); + URL.revokeObjectURL(url); +} + +export default function ExportButtons({ result }: { result: AuditExportResult }) { + const [format, setFormat] = useState(null); + const baseName = String(result.meta.full_name || 'reposcope-audit').replace(/[^a-z0-9._-]+/gi, '-'); + + function exportJson() { + download(`${baseName}-audit.json`, JSON.stringify(buildAuditJson(result), null, 2), 'application/json'); + setFormat('JSON'); + } + + function exportMarkdown() { + download(`${baseName}-audit.md`, buildAuditMarkdown(result), 'text/markdown'); + setFormat('Markdown'); + } + + return ( + + + + Export audit results + Download a shareable report without leaving your browser. + + + + JSON + + + Markdown + + + + {format && Downloaded {format} report.} + + ); +} diff --git a/lib/export.ts b/lib/export.ts new file mode 100644 index 0000000..f5d5b6c --- /dev/null +++ b/lib/export.ts @@ -0,0 +1,87 @@ +export type AuditExportResult = { + meta: any; + tree: any[]; + branches: any[]; + claims: any[]; + findings: any[]; + expl: { + score: number; + critical: number; + errors: number; + warnings: number; + infos: number; + summary: string; + }; +}; + +export function buildAuditJson(result: AuditExportResult) { + return { + exportedAt: new Date().toISOString(), + repository: { + fullName: result.meta.full_name, + htmlUrl: result.meta.html_url, + defaultBranch: result.meta.default_branch, + description: result.meta.description || '', + }, + score: { + value: result.expl.score, + summary: result.expl.summary, + counts: { + critical: result.expl.critical, + errors: result.expl.errors, + warnings: result.expl.warnings, + infos: result.expl.infos, + }, + }, + findings: result.findings, + readmeClaims: result.claims, + branches: result.branches.map(branch => ({ name: branch.name, protected: branch.protected })), + tree: result.tree.map(entry => ({ path: entry.path, type: entry.type, size: entry.size })), + }; +} + +function escapeMarkdown(value: unknown) { + return String(value ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' '); +} + +export function buildAuditMarkdown(result: AuditExportResult) { + const lines = [ + `# RepoScope audit: ${result.meta.full_name}`, + '', + `- **Repository:** ${result.meta.html_url}`, + `- **Default branch:** ${result.meta.default_branch}`, + `- **Exported:** ${new Date().toISOString()}`, + '', + `## Health score: ${result.expl.summary}`, + '', + `| Critical | Errors | Warnings | Info |`, + `| ---: | ---: | ---: | ---: |`, + `| ${result.expl.critical} | ${result.expl.errors} | ${result.expl.warnings} | ${result.expl.infos} |`, + '', + '## Findings', + '', + ]; + + if (result.findings.length === 0) { + lines.push('No findings were reported.', ''); + } else { + lines.push('| Severity | Finding | Details | Suggested fix |', '| --- | --- | --- | --- |'); + result.findings.forEach(finding => { + lines.push(`| ${escapeMarkdown(finding.severity)} | ${escapeMarkdown(finding.title)} | ${escapeMarkdown(finding.detail)} | ${escapeMarkdown(finding.fix)} |`); + }); + lines.push(''); + } + + lines.push('## README claims', ''); + if (result.claims.length === 0) { + lines.push('No README claims were analyzed.', ''); + } else { + lines.push('| Line | Target | Status | Detail |', '| ---: | --- | --- | --- |'); + result.claims.forEach(claim => { + lines.push(`| ${claim.line} | ${escapeMarkdown(claim.target)} | ${escapeMarkdown(claim.status)} | ${escapeMarkdown(claim.detail)} |`); + }); + lines.push(''); + } + + return lines.join('\n'); +}
Download a shareable report without leaving your browser.
Downloaded {format} report.