Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -190,6 +191,7 @@ export default function Home() {
</div>

<ScoreBoard score={result.expl.score} explanation={result.expl} />
<ExportButtons result={result} />
<AuditTabs data={result} />
<ActionsPanel owner={result.meta.owner.login} repo={result.meta.name} />
</div>
Expand Down
49 changes: 49 additions & 0 deletions components/ExportButtons.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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 (
<section className="card-static rounded-2xl p-5" aria-labelledby="export-heading">
<div className="flex flex-wrap items-center justify-between gap-4">
<div>
<h2 id="export-heading" className="text-sm font-black text-white">Export audit results</h2>
<p className="text-xs text-slate-400 mt-1">Download a shareable report without leaving your browser.</p>
</div>
<div className="flex gap-2">
<button type="button" onClick={exportJson} className="rounded-lg border border-edge px-3 py-2 text-xs font-bold text-slate-200 hover:border-mint hover:text-mint">
JSON
</button>
<button type="button" onClick={exportMarkdown} className="rounded-lg border border-edge px-3 py-2 text-xs font-bold text-slate-200 hover:border-mint hover:text-mint">
Markdown
</button>
</div>
</div>
{format && <p className="mt-3 text-xs text-mint" role="status">Downloaded {format} report.</p>}
</section>
);
}
87 changes: 87 additions & 0 deletions lib/export.ts
Original file line number Diff line number Diff line change
@@ -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');
}
Loading