@vibecodeqa/cli 0.9.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.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/check-meta.d.ts +15 -0
- package/dist/check-meta.js +166 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +140 -0
- package/dist/detect.d.ts +8 -0
- package/dist/detect.js +67 -0
- package/dist/fs-utils.d.ts +23 -0
- package/dist/fs-utils.js +77 -0
- package/dist/report/html.d.ts +12 -0
- package/dist/report/html.js +400 -0
- package/dist/runners/architecture.d.ts +28 -0
- package/dist/runners/architecture.js +272 -0
- package/dist/runners/complexity.d.ts +3 -0
- package/dist/runners/complexity.js +152 -0
- package/dist/runners/confusion.d.ts +16 -0
- package/dist/runners/confusion.js +198 -0
- package/dist/runners/context.d.ts +15 -0
- package/dist/runners/context.js +200 -0
- package/dist/runners/coverage.d.ts +3 -0
- package/dist/runners/coverage.js +65 -0
- package/dist/runners/dependencies.d.ts +3 -0
- package/dist/runners/dependencies.js +106 -0
- package/dist/runners/docs.d.ts +3 -0
- package/dist/runners/docs.js +97 -0
- package/dist/runners/duplication.d.ts +3 -0
- package/dist/runners/duplication.js +100 -0
- package/dist/runners/exec.d.ts +6 -0
- package/dist/runners/exec.js +25 -0
- package/dist/runners/lint.d.ts +3 -0
- package/dist/runners/lint.js +78 -0
- package/dist/runners/secrets.d.ts +3 -0
- package/dist/runners/secrets.js +108 -0
- package/dist/runners/security.d.ts +3 -0
- package/dist/runners/security.js +121 -0
- package/dist/runners/standards.d.ts +3 -0
- package/dist/runners/standards.js +153 -0
- package/dist/runners/structure.d.ts +3 -0
- package/dist/runners/structure.js +110 -0
- package/dist/runners/testing.d.ts +12 -0
- package/dist/runners/testing.js +401 -0
- package/dist/runners/tests.d.ts +3 -0
- package/dist/runners/tests.js +54 -0
- package/dist/runners/type-safety.d.ts +3 -0
- package/dist/runners/type-safety.js +74 -0
- package/dist/runners/types-check.d.ts +3 -0
- package/dist/runners/types-check.js +44 -0
- package/dist/score.d.ts +6 -0
- package/dist/score.js +19 -0
- package/dist/trend.d.ts +19 -0
- package/dist/trend.js +63 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +12 -0
- package/package.json +53 -0
package/dist/fs-utils.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Shared filesystem utilities — eliminates duplicate file-walking across runners. */
|
|
2
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, extname, join } from "node:path";
|
|
4
|
+
const SKIP_DIRS = new Set(["node_modules", "dist", ".git", ".vibe-check", "coverage", "test-results", "__pycache__"]);
|
|
5
|
+
const CODE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
6
|
+
const ALL_EXTS = new Set([...CODE_EXTS, ".json", ".env", ".yaml", ".yml", ".toml"]);
|
|
7
|
+
/** Walk source directories and return all code files. */
|
|
8
|
+
export function collectSourceFiles(cwd, opts) {
|
|
9
|
+
const files = [];
|
|
10
|
+
const dirs = ["src", "web/src"];
|
|
11
|
+
for (const dir of dirs) {
|
|
12
|
+
try {
|
|
13
|
+
walk(join(cwd, dir), cwd, files, opts?.extraExts ? ALL_EXTS : CODE_EXTS);
|
|
14
|
+
}
|
|
15
|
+
catch { /* dir doesn't exist */ }
|
|
16
|
+
}
|
|
17
|
+
if (opts?.includeTests)
|
|
18
|
+
return files;
|
|
19
|
+
return files;
|
|
20
|
+
}
|
|
21
|
+
/** Get only production source files (no tests). */
|
|
22
|
+
export function getProductionFiles(cwd) {
|
|
23
|
+
return collectSourceFiles(cwd).filter((f) => !f.isTest);
|
|
24
|
+
}
|
|
25
|
+
/** Get only test files. */
|
|
26
|
+
export function getTestFiles(cwd) {
|
|
27
|
+
return collectSourceFiles(cwd).filter((f) => f.isTest);
|
|
28
|
+
}
|
|
29
|
+
/** Read a file relative to cwd, return empty string on error. */
|
|
30
|
+
export function readSafe(cwd, path) {
|
|
31
|
+
try {
|
|
32
|
+
return readFileSync(join(cwd, path), "utf-8");
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Parse package.json dependencies. */
|
|
39
|
+
export function readDeps(cwd) {
|
|
40
|
+
const pkg = readSafe(cwd, "package.json");
|
|
41
|
+
if (!pkg)
|
|
42
|
+
return {};
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(pkg);
|
|
45
|
+
return { ...parsed.dependencies, ...parsed.devDependencies };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function walk(dir, cwd, out, exts) {
|
|
52
|
+
for (const entry of readdirSync(dir)) {
|
|
53
|
+
if (SKIP_DIRS.has(entry))
|
|
54
|
+
continue;
|
|
55
|
+
const full = join(dir, entry);
|
|
56
|
+
if (statSync(full).isDirectory()) {
|
|
57
|
+
walk(full, cwd, out, exts);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const ext = extname(entry);
|
|
61
|
+
if (!exts.has(ext))
|
|
62
|
+
continue;
|
|
63
|
+
const content = readFileSync(full, "utf-8");
|
|
64
|
+
const relPath = full.replace(cwd + "/", "");
|
|
65
|
+
const isTest = entry.includes(".test.") || entry.includes(".spec.") || relPath.includes("__tests__");
|
|
66
|
+
out.push({
|
|
67
|
+
path: relPath,
|
|
68
|
+
fullPath: full,
|
|
69
|
+
base: basename(entry, ext),
|
|
70
|
+
ext,
|
|
71
|
+
content,
|
|
72
|
+
lines: content.split("\n").length,
|
|
73
|
+
isTest,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Generate a multi-page navigable HTML report.
|
|
2
|
+
*
|
|
3
|
+
* Architecture:
|
|
4
|
+
* Top nav: Overview | Foundations | Quality | Testing | Security | Issues
|
|
5
|
+
* Sub-nav: Tabs for each check within a category
|
|
6
|
+
* Detail: Per-check stats + full issue list grouped by file
|
|
7
|
+
* File map: Cross-check heatmap of issues per file
|
|
8
|
+
*
|
|
9
|
+
* All in one self-contained HTML file using hash routing + show/hide.
|
|
10
|
+
*/
|
|
11
|
+
import type { VibeReport } from "../types.js";
|
|
12
|
+
export declare function generateHTML(report: VibeReport): string;
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/** Generate a multi-page navigable HTML report.
|
|
2
|
+
*
|
|
3
|
+
* Architecture:
|
|
4
|
+
* Top nav: Overview | Foundations | Quality | Testing | Security | Issues
|
|
5
|
+
* Sub-nav: Tabs for each check within a category
|
|
6
|
+
* Detail: Per-check stats + full issue list grouped by file
|
|
7
|
+
* File map: Cross-check heatmap of issues per file
|
|
8
|
+
*
|
|
9
|
+
* All in one self-contained HTML file using hash routing + show/hide.
|
|
10
|
+
*/
|
|
11
|
+
import { getCheckMeta } from "../check-meta.js";
|
|
12
|
+
function e(s) {
|
|
13
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
14
|
+
}
|
|
15
|
+
/** Make a file path a clickable GitHub link if repoUrl is available. */
|
|
16
|
+
function fileLink(path, line, repoUrl, branch) {
|
|
17
|
+
const clean = path.split(":")[0]; // strip :line from composite paths
|
|
18
|
+
if (!repoUrl)
|
|
19
|
+
return e(path);
|
|
20
|
+
const href = `${repoUrl}/blob/${branch}/${clean}${line ? "#L" + line : ""}`;
|
|
21
|
+
return `<a href="${e(href)}" target="_blank" rel="noopener" class="flink">${e(path)}</a>`;
|
|
22
|
+
}
|
|
23
|
+
function gc(grade) {
|
|
24
|
+
return { A: "#22c55e", B: "#84cc16", C: "#eab308", D: "#f97316", F: "#ef4444" }[grade] || "#6b7280";
|
|
25
|
+
}
|
|
26
|
+
function pc(p) {
|
|
27
|
+
return { critical: "#ef4444", high: "#f97316", medium: "#eab308", low: "#6b7280" }[p];
|
|
28
|
+
}
|
|
29
|
+
const GROUPS = [
|
|
30
|
+
{ id: "foundations", label: "Foundations", checks: ["structure", "lint", "types", "type-safety", "standards"] },
|
|
31
|
+
{ id: "quality", label: "Quality", checks: ["complexity", "duplication", "docs"] },
|
|
32
|
+
{ id: "testing", label: "Testing", checks: ["testing"] },
|
|
33
|
+
{ id: "arch", label: "Architecture", checks: ["architecture"] },
|
|
34
|
+
{ id: "security", label: "Security", checks: ["secrets", "security", "dependencies"] },
|
|
35
|
+
{ id: "llm", label: "LLM Readiness", checks: ["confusion", "context"] },
|
|
36
|
+
];
|
|
37
|
+
export function generateHTML(report) {
|
|
38
|
+
const allChecks = report.checks;
|
|
39
|
+
const checkMap = new Map(allChecks.map((c) => [c.name, c]));
|
|
40
|
+
const active = allChecks.filter((c) => !c.details.skipped);
|
|
41
|
+
const ru = report.meta.repoUrl;
|
|
42
|
+
const br = report.meta.branch;
|
|
43
|
+
const fl = (path, line) => fileLink(path, line, ru, br);
|
|
44
|
+
const totalIssues = allChecks.reduce((s, c) => s + c.issues.length, 0);
|
|
45
|
+
const proj = report.meta.cwd.split("/").pop() || "project";
|
|
46
|
+
// ── File heatmap: aggregate issues per file across all checks ──
|
|
47
|
+
const fileIssues = new Map();
|
|
48
|
+
for (const c of allChecks) {
|
|
49
|
+
for (const iss of c.issues) {
|
|
50
|
+
if (!iss.file)
|
|
51
|
+
continue;
|
|
52
|
+
const f = iss.file.split(":")[0]; // strip :line from composite file fields
|
|
53
|
+
const entry = fileIssues.get(f) || { errors: 0, warnings: 0, checks: new Set() };
|
|
54
|
+
if (iss.severity === "error")
|
|
55
|
+
entry.errors++;
|
|
56
|
+
else
|
|
57
|
+
entry.warnings++;
|
|
58
|
+
entry.checks.add(c.name);
|
|
59
|
+
fileIssues.set(f, entry);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const topFiles = [...fileIssues.entries()]
|
|
63
|
+
.map(([file, d]) => ({ file, total: d.errors + d.warnings, errors: d.errors, warnings: d.warnings, checks: [...d.checks] }))
|
|
64
|
+
.sort((a, b) => b.total - a.total)
|
|
65
|
+
.slice(0, 20);
|
|
66
|
+
// ── Category averages ──
|
|
67
|
+
const catScores = GROUPS.map((g) => {
|
|
68
|
+
const checks = g.checks.map((n) => checkMap.get(n)).filter(Boolean);
|
|
69
|
+
const scored = checks.filter((c) => !c.details.skipped);
|
|
70
|
+
const avg = scored.length > 0 ? Math.round(scored.reduce((s, c) => s + c.score, 0) / scored.length) : 0;
|
|
71
|
+
return { ...g, avg, checks };
|
|
72
|
+
});
|
|
73
|
+
// ── Build pages ──
|
|
74
|
+
// Top nav
|
|
75
|
+
const topNavItems = [
|
|
76
|
+
{ id: "overview", label: "Overview" },
|
|
77
|
+
...GROUPS.map((g) => ({ id: g.id, label: g.label })),
|
|
78
|
+
{ id: "issues", label: `Issues (${totalIssues})` },
|
|
79
|
+
{ id: "files", label: "File Map" },
|
|
80
|
+
];
|
|
81
|
+
const topNav = topNavItems.map((t) => `<a class="tn" data-page="${t.id}" onclick="go('${t.id}')">${t.label}</a>`).join("");
|
|
82
|
+
// Overview page
|
|
83
|
+
const ringPct = report.score;
|
|
84
|
+
const barChart = active.sort((a, b) => a.score - b.score).map((c) => {
|
|
85
|
+
return `<div class="brow"><span class="bl">${e(c.name)}</span><div class="bb"><div class="bf" style="width:${c.score}%;background:${gc(c.grade)}"></div></div><span class="bv" style="color:${gc(c.grade)}">${c.grade} ${c.score}</span></div>`;
|
|
86
|
+
}).join("");
|
|
87
|
+
const catCards = catScores.map((cs) => {
|
|
88
|
+
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
89
|
+
const mini = cs.checks.map((c) => {
|
|
90
|
+
const sk = c.details.skipped;
|
|
91
|
+
return `<span class="mc" style="color:${sk ? "#555" : gc(c.grade)}" title="${e(c.name)}: ${sk ? "skip" : c.score}">${sk ? "—" : c.grade}</span>`;
|
|
92
|
+
}).join("");
|
|
93
|
+
return `<div class="cc" onclick="go('${cs.id}')"><div class="cc-s" style="color:${clr}">${cs.avg}</div><div class="cc-l">${cs.label}</div><div class="cc-m">${mini}</div></div>`;
|
|
94
|
+
}).join("");
|
|
95
|
+
const radarSvg = buildRadar(catScores.map((cs) => ({ label: cs.label, score: cs.avg })));
|
|
96
|
+
const overviewPage = `<div id="p-overview" class="page active">
|
|
97
|
+
<div class="dash">
|
|
98
|
+
<div class="hero">
|
|
99
|
+
${buildRing(ringPct, gc(report.grade))}
|
|
100
|
+
<div class="hc"><span class="hg" style="color:${gc(report.grade)}">${report.grade}</span><span class="hs" style="color:${gc(report.grade)}">${report.score}/100</span><span class="hd">${active.length} checks · ${totalIssues} issues · ${report.meta.duration}ms</span></div>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="radar">${radarSvg}</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="cats">${catCards}</div>
|
|
105
|
+
<h3>All Checks</h3>
|
|
106
|
+
<div class="bars">${barChart}</div>
|
|
107
|
+
<div class="stack">${Object.entries(report.meta.stack).filter(([, v]) => v !== "none" && v !== "unknown").map(([k, v]) => `<span>${k}: <b>${v}</b></span>`).join("")}</div>
|
|
108
|
+
</div>`;
|
|
109
|
+
// Category pages (with sub-nav tabs for each check)
|
|
110
|
+
let catPages = "";
|
|
111
|
+
for (const cs of catScores) {
|
|
112
|
+
const subNav = cs.checks.map((c, i) => {
|
|
113
|
+
const sk = c.details.skipped;
|
|
114
|
+
return `<a class="sn${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}" onclick="sub(this,'${cs.id}')">${e(c.name)} <span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "—" : c.grade}</span></a>`;
|
|
115
|
+
}).join("");
|
|
116
|
+
const subPages = cs.checks.map((c, i) => {
|
|
117
|
+
const meta = getCheckMeta(c.name);
|
|
118
|
+
const sk = c.details.skipped;
|
|
119
|
+
const details = Object.entries(c.details).filter(([k]) => k !== "skipped" && k !== "reason").map(([k, v]) => {
|
|
120
|
+
const d = Array.isArray(v) ? v.join(", ") : typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
121
|
+
return `<div class="kv"><span class="k">${e(k)}</span><span class="v">${e(d)}</span></div>`;
|
|
122
|
+
}).join("");
|
|
123
|
+
// Group issues by file
|
|
124
|
+
const byFile = new Map();
|
|
125
|
+
const noFile = [];
|
|
126
|
+
for (const iss of c.issues) {
|
|
127
|
+
const f = iss.file?.split(":")[0];
|
|
128
|
+
if (f) {
|
|
129
|
+
const arr = byFile.get(f) || [];
|
|
130
|
+
arr.push(iss);
|
|
131
|
+
byFile.set(f, arr);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
noFile.push(iss);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
let issuesHtml = "";
|
|
138
|
+
for (const [file, issues] of byFile) {
|
|
139
|
+
issuesHtml += `<div class="fg"><div class="fn">${fl(file)} <span class="fc">${issues.length}</span></div>`;
|
|
140
|
+
for (const iss of issues) {
|
|
141
|
+
issuesHtml += `<div class="ir ${iss.severity}"><span class="is">${iss.severity[0].toUpperCase()}</span>${iss.line ? `<span class="il">${iss.line}</span>` : ""}<span class="im">${e(iss.message)}</span>${iss.rule ? `<span class="iru">${e(iss.rule)}</span>` : ""}</div>`;
|
|
142
|
+
}
|
|
143
|
+
issuesHtml += `</div>`;
|
|
144
|
+
}
|
|
145
|
+
if (noFile.length > 0) {
|
|
146
|
+
issuesHtml += `<div class="fg"><div class="fn">General</div>`;
|
|
147
|
+
for (const iss of noFile) {
|
|
148
|
+
issuesHtml += `<div class="ir ${iss.severity}"><span class="is">${iss.severity[0].toUpperCase()}</span><span class="im">${e(iss.message)}</span>${iss.rule ? `<span class="iru">${e(iss.rule)}</span>` : ""}</div>`;
|
|
149
|
+
}
|
|
150
|
+
issuesHtml += `</div>`;
|
|
151
|
+
}
|
|
152
|
+
return `<div class="sp${i === 0 ? " active" : ""}" data-sub="${cs.id}-${c.name}">
|
|
153
|
+
<div class="ch-head"><span class="ch-g" style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "—" : c.grade}</span><div><b>${e(meta.label)}</b><span class="ch-s">${sk ? "skipped" : c.score + "/100"} · weight ${meta.weight}% · ${c.duration}ms · ${c.issues.length} issues</span></div><span class="pri" style="color:${pc(meta.priority)}">${meta.priority}</span></div>
|
|
154
|
+
${meta.description ? `<div class="info-panel"><div class="ip-row"><span class="ip-label">What</span><span>${e(meta.description)}</span></div><div class="ip-row"><span class="ip-label">Risk</span><span>${e(meta.risk)}</span></div><div class="ip-row"><span class="ip-label">Fix</span><span>${e(meta.recommendation)}</span></div></div>` : ""}
|
|
155
|
+
${sk ? `<p class="skip-r">${e(c.details.reason || "skipped")}</p>` : ""}
|
|
156
|
+
${details ? `<div class="kvs">${details}</div>` : ""}
|
|
157
|
+
${issuesHtml ? `<div class="iss-list">${issuesHtml}</div>` : '<p style="color:var(--muted);font-size:0.8rem;margin-top:1rem">No issues found.</p>'}
|
|
158
|
+
</div>`;
|
|
159
|
+
}).join("");
|
|
160
|
+
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
161
|
+
catPages += `<div id="p-${cs.id}" class="page">
|
|
162
|
+
<div class="cat-head"><span style="color:${clr};font-size:1.8rem;font-weight:900">${cs.avg}</span><span style="color:${clr}">/100</span><span style="color:var(--muted);margin-left:0.5rem">${cs.label}</span></div>
|
|
163
|
+
<div class="bar2"><div class="bf2" style="width:${cs.avg}%;background:${clr}"></div></div>
|
|
164
|
+
<div class="sub-nav">${subNav}</div>
|
|
165
|
+
${subPages}
|
|
166
|
+
</div>`;
|
|
167
|
+
}
|
|
168
|
+
// All Issues page
|
|
169
|
+
const allIssues = allChecks.flatMap((c) => c.issues.map((i) => ({ check: c.name, ...i })));
|
|
170
|
+
const issueRows = allIssues.slice(0, 200).map((i) => {
|
|
171
|
+
const loc = i.file ? fl(i.file.split(":")[0], i.line) : "";
|
|
172
|
+
return `<tr class="${i.severity}"><td class="is2">${i.severity[0].toUpperCase()}</td><td class="ic2">${e(i.check)}</td><td class="il2">${loc}</td><td>${e(i.message)}</td><td class="iru2">${e(i.rule || "")}</td></tr>`;
|
|
173
|
+
}).join("");
|
|
174
|
+
const issuesPage = `<div id="p-issues" class="page">
|
|
175
|
+
<h2>All Issues <span style="color:var(--muted);font-weight:400">${totalIssues}</span></h2>
|
|
176
|
+
<div class="isf">${allIssues.filter((i) => i.severity === "error").length} errors · ${allIssues.filter((i) => i.severity === "warning").length} warnings</div>
|
|
177
|
+
<table class="it"><thead><tr><th></th><th>Check</th><th>Location</th><th>Message</th><th>Rule</th></tr></thead><tbody>${issueRows}</tbody></table>
|
|
178
|
+
${allIssues.length > 200 ? `<p style="color:var(--muted);text-align:center;margin-top:1rem">Showing 200 of ${allIssues.length}</p>` : ""}
|
|
179
|
+
</div>`;
|
|
180
|
+
// File heatmap page
|
|
181
|
+
const fileRows = topFiles.map((f) => {
|
|
182
|
+
const pct = Math.min(100, f.total * 5);
|
|
183
|
+
return `<div class="fr"><span class="ff">${fl(f.file)}</span><div class="fb"><div class="fbf" style="width:${pct}%;background:${f.errors > 0 ? "var(--fail)" : "var(--warn)"}"></div></div><span class="fv">${f.errors}E ${f.warnings}W</span><span class="fcs">${f.checks.join(", ")}</span></div>`;
|
|
184
|
+
}).join("");
|
|
185
|
+
const filesPage = `<div id="p-files" class="page">
|
|
186
|
+
<h2>File Heatmap</h2>
|
|
187
|
+
<p style="color:var(--muted);font-size:0.78rem;margin-bottom:1rem">Top ${topFiles.length} files by total issues across all checks</p>
|
|
188
|
+
${fileRows || '<p style="color:var(--muted)">No file-level issues found.</p>'}
|
|
189
|
+
</div>`;
|
|
190
|
+
return `<!DOCTYPE html>
|
|
191
|
+
<html lang="en">
|
|
192
|
+
<head>
|
|
193
|
+
<meta charset="utf-8">
|
|
194
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
195
|
+
<title>Vibe Check — ${e(proj)}</title>
|
|
196
|
+
<style>
|
|
197
|
+
:root{--bg:#09090b;--card:#111115;--border:#1e1e24;--text:#e5e5e5;--muted:#6b7280;--pass:#22c55e;--fail:#ef4444;--warn:#eab308;--info:#6366f1;--accent:#818cf8}
|
|
198
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
199
|
+
body{font-family:"Inter",system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.5}
|
|
200
|
+
code{font-family:"SF Mono",Menlo,monospace;font-size:0.85em}
|
|
201
|
+
|
|
202
|
+
/* Top nav */
|
|
203
|
+
.top{position:sticky;top:0;z-index:20;background:#0c0c0fcc;backdrop-filter:blur(12px);border-bottom:1px solid var(--border);padding:0 2rem;display:flex;align-items:center;gap:0}
|
|
204
|
+
.logo{font-weight:800;font-size:1rem;margin-right:1.5rem;padding:0.7rem 0}
|
|
205
|
+
.logo span{color:var(--accent)}
|
|
206
|
+
.tn{padding:0.7rem 0.8rem;font-size:0.78rem;color:var(--muted);text-decoration:none;cursor:pointer;border-bottom:2px solid transparent;transition:all 0.15s}
|
|
207
|
+
.tn:hover{color:var(--text)}
|
|
208
|
+
.tn.active{color:var(--text);border-bottom-color:var(--accent)}
|
|
209
|
+
|
|
210
|
+
/* Sidebar */
|
|
211
|
+
.side{position:fixed;top:42px;left:0;bottom:0;width:200px;background:#0c0c0f;border-right:1px solid var(--border);overflow-y:auto;padding:0.8rem 0;font-size:0.7rem;z-index:10}
|
|
212
|
+
.side-section{padding:0.3rem 0;border-bottom:1px solid var(--border)}
|
|
213
|
+
.side-section:last-child{border-bottom:none}
|
|
214
|
+
.side-score{font-size:1.4rem;font-weight:900;padding:0.3rem 0.8rem}
|
|
215
|
+
.side-cat{display:block;padding:0.3rem 0.8rem;color:var(--text);font-weight:700;cursor:pointer;text-decoration:none;font-size:0.72rem}
|
|
216
|
+
.side-cat:hover{background:#14141a}
|
|
217
|
+
.side-check{display:block;padding:0.2rem 0.8rem 0.2rem 1.2rem;color:var(--muted);cursor:pointer;text-decoration:none;font-size:0.65rem}
|
|
218
|
+
.side-check:hover{color:var(--text);background:#14141a}
|
|
219
|
+
.side-check span{display:inline-block;width:1rem;font-weight:800;text-align:center}
|
|
220
|
+
|
|
221
|
+
/* Content */
|
|
222
|
+
.content{max-width:900px;margin-left:200px;padding:2rem}
|
|
223
|
+
.page{display:none;animation:fadeIn 0.15s}
|
|
224
|
+
.page.active{display:block}
|
|
225
|
+
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
|
226
|
+
|
|
227
|
+
/* Overview */
|
|
228
|
+
.dash{display:flex;gap:2rem;margin-bottom:2rem;align-items:center;flex-wrap:wrap}
|
|
229
|
+
.hero{display:flex;align-items:center;gap:1rem}
|
|
230
|
+
.hero svg{width:100px;height:100px}
|
|
231
|
+
.hc{display:flex;flex-direction:column}
|
|
232
|
+
.hg{font-size:2.5rem;font-weight:900;line-height:1}
|
|
233
|
+
.hs{font-size:1rem;font-weight:600}
|
|
234
|
+
.hd{font-size:0.68rem;color:var(--muted)}
|
|
235
|
+
.radar{flex:1;display:flex;justify-content:center}
|
|
236
|
+
.radar svg{max-width:240px;width:100%}
|
|
237
|
+
.cats{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:0.6rem;margin-bottom:2rem}
|
|
238
|
+
.cc{background:var(--card);border:1px solid var(--border);border-radius:0.6rem;padding:0.8rem;cursor:pointer;transition:border-color 0.15s}
|
|
239
|
+
.cc:hover{border-color:var(--accent)}
|
|
240
|
+
.cc-s{font-size:1.8rem;font-weight:900}
|
|
241
|
+
.cc-l{font-size:0.75rem;color:var(--muted)}
|
|
242
|
+
.cc-m{margin-top:0.3rem;display:flex;gap:0.25rem}
|
|
243
|
+
.mc{font-size:0.65rem;font-weight:800}
|
|
244
|
+
h3{font-size:0.85rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:0.5rem}
|
|
245
|
+
.bars{margin-bottom:1.5rem}
|
|
246
|
+
.brow{display:flex;align-items:center;gap:0.4rem;margin-bottom:0.25rem;font-size:0.72rem}
|
|
247
|
+
.bl{width:80px;text-align:right;color:var(--muted);flex-shrink:0}
|
|
248
|
+
.bb{flex:1;height:14px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}
|
|
249
|
+
.bf{height:100%;border-radius:2px}
|
|
250
|
+
.bv{width:36px;font-weight:700;font-size:0.68rem}
|
|
251
|
+
.stack{display:flex;gap:0.35rem;flex-wrap:wrap}
|
|
252
|
+
.stack span{background:var(--card);border:1px solid var(--border);padding:0.1rem 0.45rem;border-radius:9999px;font-size:0.62rem;color:var(--muted)}
|
|
253
|
+
|
|
254
|
+
/* Category pages */
|
|
255
|
+
.cat-head{margin-bottom:0.3rem}
|
|
256
|
+
.bar2{height:4px;background:var(--card);border-radius:2px;margin-bottom:1rem;overflow:hidden}
|
|
257
|
+
.bf2{height:100%;border-radius:2px}
|
|
258
|
+
.sub-nav{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1rem}
|
|
259
|
+
.sn{padding:0.5rem 0.8rem;font-size:0.75rem;color:var(--muted);cursor:pointer;border-bottom:2px solid transparent}
|
|
260
|
+
.sn:hover{color:var(--text)}
|
|
261
|
+
.sn.active{color:var(--text);border-bottom-color:var(--accent)}
|
|
262
|
+
.sp{display:none}.sp.active{display:block}
|
|
263
|
+
|
|
264
|
+
/* Check detail */
|
|
265
|
+
.ch-head{display:flex;align-items:center;gap:0.7rem;margin-bottom:0.8rem}
|
|
266
|
+
.ch-g{font-size:2rem;font-weight:900}
|
|
267
|
+
.ch-s{display:block;font-size:0.7rem;color:var(--muted)}
|
|
268
|
+
.pri{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;padding:0.15rem 0.5rem;border-radius:9999px;border:1px solid currentColor;flex-shrink:0}
|
|
269
|
+
.info-panel{background:#0d0d12;border:1px solid var(--border);border-radius:0.5rem;padding:0.7rem 0.9rem;margin-bottom:1rem;font-size:0.72rem;line-height:1.6}
|
|
270
|
+
.ip-row{margin-bottom:0.4rem;display:flex;gap:0.5rem}
|
|
271
|
+
.ip-row:last-child{margin-bottom:0}
|
|
272
|
+
.ip-label{color:var(--accent);font-weight:700;min-width:2.5rem;flex-shrink:0}
|
|
273
|
+
.skip-r{color:var(--muted);font-style:italic;font-size:0.78rem}
|
|
274
|
+
.kvs{display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:1rem}
|
|
275
|
+
.kv{background:var(--card);border:1px solid var(--border);border-radius:0.4rem;padding:0.3rem 0.6rem;font-size:0.7rem}
|
|
276
|
+
.k{color:var(--muted);margin-right:0.3rem}
|
|
277
|
+
.v{font-weight:600}
|
|
278
|
+
|
|
279
|
+
/* Issue list grouped by file */
|
|
280
|
+
.iss-list{margin-top:1rem}
|
|
281
|
+
.fg{margin-bottom:0.8rem}
|
|
282
|
+
.fn{font-size:0.72rem;font-weight:600;font-family:"SF Mono",monospace;padding:0.3rem 0;border-bottom:1px solid var(--border);margin-bottom:0.2rem;display:flex;align-items:center;gap:0.5rem}
|
|
283
|
+
.fc{background:var(--border);border-radius:9999px;padding:0 0.4rem;font-size:0.6rem;color:var(--muted)}
|
|
284
|
+
.ir{font-size:0.65rem;font-family:"SF Mono",monospace;padding:0.12rem 0 0.12rem 0.5rem;display:flex;gap:0.4rem;align-items:baseline}
|
|
285
|
+
.is{font-weight:800;font-size:0.55rem;width:0.9rem;text-align:center;border-radius:2px;flex-shrink:0}
|
|
286
|
+
.ir.error .is{color:var(--fail);background:#ef444418}
|
|
287
|
+
.ir.warning .is{color:var(--warn);background:#eab30818}
|
|
288
|
+
.il{color:var(--accent);min-width:2rem;flex-shrink:0}
|
|
289
|
+
.im{flex:1;word-break:break-word}
|
|
290
|
+
.iru{color:#555;font-size:0.55rem}
|
|
291
|
+
|
|
292
|
+
/* All issues table */
|
|
293
|
+
.isf{color:var(--muted);font-size:0.75rem;margin-bottom:0.8rem}
|
|
294
|
+
.it{width:100%;border-collapse:collapse;font-size:0.68rem}
|
|
295
|
+
.it th{text-align:left;padding:0.35rem 0.4rem;color:var(--muted);font-size:0.62rem;text-transform:uppercase;border-bottom:1px solid var(--border)}
|
|
296
|
+
.it td{padding:0.25rem 0.4rem;border-bottom:1px solid var(--border);font-family:"SF Mono",monospace;font-size:0.62rem}
|
|
297
|
+
.it tr.error .is2{color:var(--fail)}
|
|
298
|
+
.it tr.warning .is2{color:var(--warn)}
|
|
299
|
+
.is2{font-weight:800;width:1rem}
|
|
300
|
+
.ic2{color:var(--muted);width:70px}
|
|
301
|
+
.il2{color:var(--muted)}
|
|
302
|
+
.iru2{color:#555;font-size:0.58rem}
|
|
303
|
+
|
|
304
|
+
/* File heatmap */
|
|
305
|
+
.fr{display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;font-size:0.7rem}
|
|
306
|
+
.ff{width:200px;font-family:"SF Mono",monospace;font-size:0.65rem;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
307
|
+
.fb{flex:1;height:12px;background:var(--card);border-radius:3px;overflow:hidden;border:1px solid var(--border)}
|
|
308
|
+
.fbf{height:100%;border-radius:2px}
|
|
309
|
+
.fv{width:50px;font-size:0.65rem;color:var(--muted);flex-shrink:0}
|
|
310
|
+
.fcs{font-size:0.6rem;color:#555}
|
|
311
|
+
|
|
312
|
+
.footer{text-align:center;color:var(--muted);font-size:0.58rem;margin-top:2rem;padding:0.8rem 0;border-top:1px solid var(--border)}
|
|
313
|
+
.footer a{color:var(--muted)}
|
|
314
|
+
.flink{color:var(--accent);text-decoration:none;font-family:"SF Mono",monospace}.flink:hover{text-decoration:underline}
|
|
315
|
+
@media(max-width:768px){.side{display:none}.content{margin-left:0;padding:1rem}.cats{grid-template-columns:1fr 1fr}.dash{flex-direction:column}}
|
|
316
|
+
</style>
|
|
317
|
+
</head>
|
|
318
|
+
<body>
|
|
319
|
+
|
|
320
|
+
<nav class="top">
|
|
321
|
+
<div class="logo"><span>vibe</span>-check</div>
|
|
322
|
+
${topNav}
|
|
323
|
+
</nav>
|
|
324
|
+
|
|
325
|
+
<aside class="side">
|
|
326
|
+
<div class="side-section">Score<div class="side-score" style="color:${gc(report.grade)}">${report.grade} ${report.score}</div></div>
|
|
327
|
+
${catScores.map((cs) => {
|
|
328
|
+
const clr = gc(cs.avg >= 90 ? "A" : cs.avg >= 75 ? "B" : cs.avg >= 60 ? "C" : cs.avg >= 40 ? "D" : "F");
|
|
329
|
+
return `<div class="side-section"><a class="side-cat" onclick="go('${cs.id}')">${cs.label} <span style="color:${clr}">${cs.avg}</span></a>${cs.checks.map((c) => {
|
|
330
|
+
const sk = c.details.skipped;
|
|
331
|
+
const meta = getCheckMeta(c.name);
|
|
332
|
+
return `<a class="side-check" onclick="go('${cs.id}')" title="${e(meta.label)}"><span style="color:${sk ? "#555" : gc(c.grade)}">${sk ? "—" : c.grade}</span> ${e(meta.label)}</a>`;
|
|
333
|
+
}).join("")}</div>`;
|
|
334
|
+
}).join("")}
|
|
335
|
+
</aside>
|
|
336
|
+
<div class="content">
|
|
337
|
+
${overviewPage}
|
|
338
|
+
${catPages}
|
|
339
|
+
${issuesPage}
|
|
340
|
+
${filesPage}
|
|
341
|
+
<div class="footer">Generated by <a href="https://github.com/freeappstore-online/vibe-check">vibe-check</a> v${report.version}</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<script>
|
|
345
|
+
function go(id){
|
|
346
|
+
document.querySelectorAll('.tn').forEach(n=>{n.classList.toggle('active',n.dataset.page===id)});
|
|
347
|
+
document.querySelectorAll('.page').forEach(p=>{p.classList.toggle('active',p.id==='p-'+id)});
|
|
348
|
+
window.scrollTo(0,0);
|
|
349
|
+
}
|
|
350
|
+
function sub(el,cat){
|
|
351
|
+
const id=el.dataset.sub;
|
|
352
|
+
el.parentElement.querySelectorAll('.sn').forEach(n=>n.classList.remove('active'));
|
|
353
|
+
el.classList.add('active');
|
|
354
|
+
document.querySelectorAll('#p-'+cat+' .sp').forEach(s=>{s.classList.toggle('active',s.dataset.sub===id)});
|
|
355
|
+
}
|
|
356
|
+
// Init: show overview
|
|
357
|
+
document.querySelector('.tn').classList.add('active');
|
|
358
|
+
</script>
|
|
359
|
+
</body></html>`;
|
|
360
|
+
}
|
|
361
|
+
// ── SVG builders ──
|
|
362
|
+
function buildRing(score, color) {
|
|
363
|
+
const r = 42;
|
|
364
|
+
const c = 2 * Math.PI * r;
|
|
365
|
+
const off = c - (score / 100) * c;
|
|
366
|
+
return `<svg viewBox="0 0 100 100" style="width:100px;height:100px"><circle cx="50" cy="50" r="${r}" fill="none" stroke="#1e1e24" stroke-width="7"/><circle cx="50" cy="50" r="${r}" fill="none" stroke="${color}" stroke-width="7" stroke-dasharray="${c}" stroke-dashoffset="${off}" stroke-linecap="round" transform="rotate(-90 50 50)"/></svg>`;
|
|
367
|
+
}
|
|
368
|
+
function buildRadar(items) {
|
|
369
|
+
const n = items.length;
|
|
370
|
+
if (n < 3)
|
|
371
|
+
return "";
|
|
372
|
+
const cx = 120, cy = 120, r = 90;
|
|
373
|
+
const step = (2 * Math.PI) / n;
|
|
374
|
+
let grid = "";
|
|
375
|
+
for (const pct of [25, 50, 75, 100]) {
|
|
376
|
+
const rr = (pct / 100) * r;
|
|
377
|
+
const pts = items.map((_, i) => `${cx + rr * Math.cos(i * step - Math.PI / 2)},${cy + rr * Math.sin(i * step - Math.PI / 2)}`).join(" ");
|
|
378
|
+
grid += `<polygon points="${pts}" fill="none" stroke="#1e1e24" stroke-width="0.7"/>`;
|
|
379
|
+
}
|
|
380
|
+
let axes = "";
|
|
381
|
+
for (let i = 0; i < n; i++) {
|
|
382
|
+
const a = i * step - Math.PI / 2;
|
|
383
|
+
axes += `<line x1="${cx}" y1="${cy}" x2="${cx + r * Math.cos(a)}" y2="${cy + r * Math.sin(a)}" stroke="#1e1e24" stroke-width="0.7"/>`;
|
|
384
|
+
const lx = cx + (r + 16) * Math.cos(a);
|
|
385
|
+
const ly = cy + (r + 16) * Math.sin(a);
|
|
386
|
+
axes += `<text x="${lx}" y="${ly}" text-anchor="middle" dominant-baseline="middle" fill="#6b7280" font-size="9" font-weight="600">${items[i].label}</text>`;
|
|
387
|
+
}
|
|
388
|
+
const dataPts = items.map((c, i) => {
|
|
389
|
+
const a = i * step - Math.PI / 2;
|
|
390
|
+
const rr = (c.score / 100) * r;
|
|
391
|
+
return `${cx + rr * Math.cos(a)},${cy + rr * Math.sin(a)}`;
|
|
392
|
+
}).join(" ");
|
|
393
|
+
let dots = "";
|
|
394
|
+
for (let i = 0; i < n; i++) {
|
|
395
|
+
const a = i * step - Math.PI / 2;
|
|
396
|
+
const rr = (items[i].score / 100) * r;
|
|
397
|
+
dots += `<circle cx="${cx + rr * Math.cos(a)}" cy="${cy + rr * Math.sin(a)}" r="3.5" fill="#818cf8"/>`;
|
|
398
|
+
}
|
|
399
|
+
return `<svg viewBox="0 0 240 240">${grid}${axes}<polygon points="${dataPts}" fill="#818cf825" stroke="#818cf8" stroke-width="1.5"/>${dots}</svg>`;
|
|
400
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Architecture analysis — import graph, circular deps, coupling metrics, god modules.
|
|
2
|
+
*
|
|
3
|
+
* Produces:
|
|
4
|
+
* 1. Import graph (adjacency list)
|
|
5
|
+
* 2. Circular dependency detection
|
|
6
|
+
* 3. Fan-in / fan-out metrics per file (coupling)
|
|
7
|
+
* 4. God modules (imported by >50% of files)
|
|
8
|
+
* 5. Orphan files (not imported by anyone, not an entrypoint)
|
|
9
|
+
* 6. Layer violations (optional: detect cross-layer imports)
|
|
10
|
+
* 7. SVG architecture diagram
|
|
11
|
+
*/
|
|
12
|
+
import type { CheckResult } from "../types.js";
|
|
13
|
+
interface ModuleNode {
|
|
14
|
+
path: string;
|
|
15
|
+
imports: string[];
|
|
16
|
+
importedBy: string[];
|
|
17
|
+
dir: string;
|
|
18
|
+
exports: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ArchGraph {
|
|
21
|
+
nodes: Map<string, ModuleNode>;
|
|
22
|
+
cycles: string[][];
|
|
23
|
+
godModules: string[];
|
|
24
|
+
orphans: string[];
|
|
25
|
+
}
|
|
26
|
+
export declare function runArchitecture(cwd: string): CheckResult;
|
|
27
|
+
export declare function generateArchSVG(details: Record<string, unknown>): string;
|
|
28
|
+
export {};
|