@hevman/repo-roast 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hevman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # repo-roast
2
+
3
+ Roast your repository, then turn the joke into a practical quality checklist.
4
+
5
+ ```bash
6
+ npx @hevman/repo-roast
7
+ ```
8
+
9
+ Example output:
10
+
11
+ ```text
12
+ repo-roast
13
+
14
+ my-app
15
+ Score: 72/100
16
+
17
+ Roast:
18
+ This repo ships, but it also trusts vibes where a checklist would cost less emotional damage.
19
+
20
+ Findings:
21
+ x ERROR No test script
22
+ package.json has no scripts.test.
23
+ ! WARNING Thin README
24
+ README exists, but it is too short to sell or explain the project.
25
+
26
+ Next fixes:
27
+ 1. Add a test script, even if it starts with a tiny smoke test.
28
+ 2. Expand README so a stranger can use the project in under one minute.
29
+ ```
30
+
31
+ ## Why
32
+
33
+ Most project audits are boring. Most jokes are useless. `repo-roast` tries to land in the useful middle: it gives you a shareable roast, a score and a short list of fixes that actually improve the repo.
34
+
35
+ It checks for:
36
+
37
+ - missing or thin README,
38
+ - missing license,
39
+ - missing test script,
40
+ - missing GitHub Actions workflow,
41
+ - weak npm metadata,
42
+ - TODO/FIXME pileups,
43
+ - large source files,
44
+ - possible committed secrets.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ npm install -D @hevman/repo-roast
50
+ ```
51
+
52
+ ## CLI
53
+
54
+ ```bash
55
+ repo-roast
56
+ repo-roast --path ../my-project
57
+ repo-roast --json
58
+ repo-roast --strict
59
+ ```
60
+
61
+ `--strict` exits with code `1` when the score is below 80, which makes it usable in CI.
62
+
63
+ ## Library
64
+
65
+ ```ts
66
+ import { analyzeRepo, formatReport } from "@hevman/repo-roast";
67
+
68
+ const report = await analyzeRepo({ root: process.cwd() });
69
+ console.log(formatReport(report));
70
+ ```
71
+
72
+ ## Publishing
73
+
74
+ ```bash
75
+ npm test
76
+ npm pack --dry-run
77
+ npm publish --access public
78
+ ```
79
+
80
+ ## License
81
+
82
+ MIT
@@ -0,0 +1,2 @@
1
+ import type { AnalyzeOptions, RoastReport } from "./types.js";
2
+ export declare function analyzeRepo(options?: AnalyzeOptions): Promise<RoastReport>;
@@ -0,0 +1,122 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { scanFiles } from "./scan.js";
4
+ function finding(code, level, points, title, detail) {
5
+ return { code, level, points, title, detail };
6
+ }
7
+ async function exists(path) {
8
+ return access(path).then(() => true, () => false);
9
+ }
10
+ function packageName(pkg) {
11
+ if (pkg && typeof pkg === "object" && "name" in pkg && typeof pkg.name === "string") {
12
+ return pkg.name;
13
+ }
14
+ return "this repo";
15
+ }
16
+ function packageScripts(pkg) {
17
+ if (pkg && typeof pkg === "object" && "scripts" in pkg && pkg.scripts && typeof pkg.scripts === "object") {
18
+ return pkg.scripts;
19
+ }
20
+ return {};
21
+ }
22
+ function roastFor(score, findings) {
23
+ const top = findings[0];
24
+ if (score >= 90) {
25
+ return "This repo is annoyingly responsible. I came here to roast and found vegetables already chopped.";
26
+ }
27
+ if (score >= 75) {
28
+ return `Pretty healthy, but ${top?.title.toLowerCase() ?? "one thing"} is still standing in the hallway wearing shoes indoors.`;
29
+ }
30
+ if (score >= 55) {
31
+ return `This repo ships, but it also trusts vibes where a checklist would cost less emotional damage.`;
32
+ }
33
+ return "This repo has startup energy: ambition, caffeine, and a README that may or may not know what happened.";
34
+ }
35
+ function fixFor(issue) {
36
+ const fixes = {
37
+ "missing-readme": "Add a README with install, usage and one copy-paste example.",
38
+ "thin-readme": "Expand README so a stranger can use the project in under one minute.",
39
+ "missing-license": "Add a LICENSE file before publishing.",
40
+ "missing-test-script": "Add a test script, even if it starts with a tiny smoke test.",
41
+ "missing-ci": "Add a basic GitHub Actions workflow that runs npm test.",
42
+ "package-no-repository": "Add repository, homepage and bugs fields to package.json.",
43
+ "todo-heavy": "Triage TODO/FIXME comments and turn the real ones into issues.",
44
+ "large-files": "Split the largest source files or move generated output out of source control.",
45
+ "possible-secret": "Rotate any real secret and move sensitive values into ignored env files."
46
+ };
47
+ return fixes[issue.code] ?? issue.title;
48
+ }
49
+ export async function analyzeRepo(options = {}) {
50
+ const root = resolve(options.root ?? process.cwd());
51
+ const files = await scanFiles(root, options.maxFiles);
52
+ const findings = [];
53
+ const readme = files.find((file) => /^readme\.md$/i.test(file.path));
54
+ const license = files.find((file) => /^licen[cs]e/i.test(file.path));
55
+ const workflows = files.filter((file) => file.path.startsWith(".github/workflows/"));
56
+ const todoCount = files.reduce((sum, file) => {
57
+ const matches = file.text
58
+ .split(/\r?\n/)
59
+ .filter((line) => /(^|\s|\/\/|#)(TODO|FIXME|HACK)\b/i.test(line) && !line.includes("TODO/FIXME/HACK"));
60
+ return sum + matches.length;
61
+ }, 0);
62
+ const largestFiles = files
63
+ .filter((file) => /\.(js|jsx|ts|tsx|mjs|cjs)$/i.test(file.path))
64
+ .sort((a, b) => b.lines - a.lines)
65
+ .slice(0, 5)
66
+ .map((file) => ({ path: file.path, lines: file.lines }));
67
+ const packagePath = join(root, "package.json");
68
+ const packageJson = await exists(packagePath)
69
+ ? JSON.parse(await readFile(packagePath, "utf8"))
70
+ : undefined;
71
+ const scripts = packageScripts(packageJson);
72
+ if (!readme) {
73
+ findings.push(finding("missing-readme", "error", 18, "Missing README", "No README.md found at the repository root."));
74
+ }
75
+ else if (readme.text.trim().length < 500) {
76
+ findings.push(finding("thin-readme", "warning", 8, "Thin README", "README exists, but it is too short to sell or explain the project."));
77
+ }
78
+ if (!license) {
79
+ findings.push(finding("missing-license", "warning", 8, "Missing license", "No LICENSE file found."));
80
+ }
81
+ if (packageJson) {
82
+ if (!scripts.test) {
83
+ findings.push(finding("missing-test-script", "error", 14, "No test script", "package.json has no scripts.test."));
84
+ }
85
+ if (typeof packageJson === "object"
86
+ && packageJson
87
+ && (!("repository" in packageJson) || !("bugs" in packageJson) || !("homepage" in packageJson))) {
88
+ findings.push(finding("package-no-repository", "warning", 6, "Weak npm metadata", "package.json is missing repository, homepage or bugs metadata."));
89
+ }
90
+ }
91
+ if (workflows.length === 0) {
92
+ findings.push(finding("missing-ci", "warning", 7, "No GitHub Actions", "No workflow found in .github/workflows."));
93
+ }
94
+ if (todoCount >= 5) {
95
+ findings.push(finding("todo-heavy", "warning", Math.min(12, todoCount), "TODO pileup", `${todoCount} TODO/FIXME/HACK comments found.`));
96
+ }
97
+ if (largestFiles.some((file) => file.lines >= 500)) {
98
+ const detail = largestFiles.filter((file) => file.lines >= 500).map((file) => `${file.path} (${file.lines} lines)`).join(", ");
99
+ findings.push(finding("large-files", "warning", 8, "Large source files", detail));
100
+ }
101
+ const secretPattern = /(api[_-]?key|secret|password)\s*[:=]\s*["']?[A-Za-z0-9_\-]{16,}/i;
102
+ const secretHit = files.find((file) => !file.path.endsWith(".env.example") && secretPattern.test(file.text));
103
+ if (secretHit) {
104
+ findings.push(finding("possible-secret", "error", 20, "Possible secret", `A secret-looking value appears in ${secretHit.path}.`));
105
+ }
106
+ findings.sort((a, b) => b.points - a.points);
107
+ const score = Math.max(0, 100 - findings.reduce((sum, issue) => sum + issue.points, 0));
108
+ const fixes = findings.slice(0, 5).map(fixFor);
109
+ return {
110
+ name: packageName(packageJson),
111
+ root,
112
+ score,
113
+ roast: roastFor(score, findings),
114
+ findings,
115
+ fixes,
116
+ stats: {
117
+ files: files.length,
118
+ todoCount,
119
+ largestFiles
120
+ }
121
+ };
122
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ import { analyzeRepo, formatReport } from "./index.js";
3
+ function has(args, flag) {
4
+ return args.includes(flag);
5
+ }
6
+ function option(args, flag) {
7
+ const index = args.indexOf(flag);
8
+ return index === -1 ? undefined : args[index + 1];
9
+ }
10
+ function help() {
11
+ console.log(`repo-roast
12
+
13
+ Usage:
14
+ repo-roast
15
+ repo-roast --path ../my-project
16
+ repo-roast --json
17
+
18
+ Options:
19
+ --path <dir> Repository directory to scan
20
+ --json Print machine-readable JSON
21
+ --strict Exit 1 when score is below 80`);
22
+ }
23
+ async function run() {
24
+ const args = process.argv.slice(2);
25
+ if (has(args, "--help") || has(args, "-h")) {
26
+ help();
27
+ return;
28
+ }
29
+ const report = await analyzeRepo({ root: option(args, "--path") });
30
+ if (has(args, "--json")) {
31
+ console.log(JSON.stringify(report, null, 2));
32
+ }
33
+ else {
34
+ console.log(formatReport(report));
35
+ }
36
+ if (has(args, "--strict") && report.score < 80) {
37
+ process.exitCode = 1;
38
+ }
39
+ }
40
+ run().catch((error) => {
41
+ console.error(error instanceof Error ? error.message : error);
42
+ process.exitCode = 1;
43
+ });
@@ -0,0 +1,2 @@
1
+ import type { RoastReport } from "./types.js";
2
+ export declare function formatReport(report: RoastReport): string;
package/dist/format.js ADDED
@@ -0,0 +1,41 @@
1
+ function icon(level) {
2
+ if (level === "error") {
3
+ return "x";
4
+ }
5
+ if (level === "warning") {
6
+ return "!";
7
+ }
8
+ return "i";
9
+ }
10
+ export function formatReport(report) {
11
+ const lines = [
12
+ "repo-roast",
13
+ "",
14
+ `${report.name}`,
15
+ `Score: ${report.score}/100`,
16
+ "",
17
+ "Roast:",
18
+ report.roast,
19
+ ""
20
+ ];
21
+ if (report.findings.length === 0) {
22
+ lines.push("Findings:", "No major issues found. Suspiciously wholesome.", "");
23
+ }
24
+ else {
25
+ lines.push("Findings:");
26
+ for (const issue of report.findings) {
27
+ lines.push(`${icon(issue.level)} ${issue.level.toUpperCase()} ${issue.title}`);
28
+ lines.push(` ${issue.detail}`);
29
+ }
30
+ lines.push("");
31
+ }
32
+ if (report.fixes.length > 0) {
33
+ lines.push("Next fixes:");
34
+ report.fixes.forEach((fix, index) => {
35
+ lines.push(`${index + 1}. ${fix}`);
36
+ });
37
+ lines.push("");
38
+ }
39
+ lines.push(`Scanned ${report.stats.files} text files.`);
40
+ return lines.join("\n");
41
+ }
@@ -0,0 +1,3 @@
1
+ export { analyzeRepo } from "./analyze.js";
2
+ export { formatReport } from "./format.js";
3
+ export type { AnalyzeOptions, Finding, FindingLevel, RoastReport } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { analyzeRepo } from "./analyze.js";
2
+ export { formatReport } from "./format.js";
package/dist/scan.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export interface ScannedFile {
2
+ path: string;
3
+ absolutePath: string;
4
+ text: string;
5
+ lines: number;
6
+ }
7
+ export declare function scanFiles(root: string, maxFiles?: number): Promise<ScannedFile[]>;
package/dist/scan.js ADDED
@@ -0,0 +1,72 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { join, relative } from "node:path";
3
+ const IGNORED_DIRS = new Set([
4
+ ".git",
5
+ "node_modules",
6
+ "dist",
7
+ "build",
8
+ "coverage",
9
+ ".next",
10
+ ".turbo",
11
+ ".cache",
12
+ "vendor"
13
+ ]);
14
+ const TEXT_EXTENSIONS = new Set([
15
+ ".cjs",
16
+ ".css",
17
+ ".env",
18
+ ".example",
19
+ ".html",
20
+ ".js",
21
+ ".json",
22
+ ".jsx",
23
+ ".md",
24
+ ".mjs",
25
+ ".ts",
26
+ ".tsx",
27
+ ".txt",
28
+ ".yml",
29
+ ".yaml"
30
+ ]);
31
+ function extension(path) {
32
+ const dot = path.lastIndexOf(".");
33
+ return dot === -1 ? "" : path.slice(dot).toLowerCase();
34
+ }
35
+ function shouldRead(path) {
36
+ const ext = extension(path);
37
+ return TEXT_EXTENSIONS.has(ext) || path.endsWith("Dockerfile") || path.endsWith("LICENSE");
38
+ }
39
+ export async function scanFiles(root, maxFiles = 600) {
40
+ const files = [];
41
+ async function walk(directory) {
42
+ if (files.length >= maxFiles) {
43
+ return;
44
+ }
45
+ const entries = await readdir(directory, { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ const absolutePath = join(directory, entry.name);
48
+ if (entry.isDirectory()) {
49
+ if (!IGNORED_DIRS.has(entry.name)) {
50
+ await walk(absolutePath);
51
+ }
52
+ continue;
53
+ }
54
+ if (!entry.isFile() || !shouldRead(entry.name)) {
55
+ continue;
56
+ }
57
+ const info = await stat(absolutePath);
58
+ if (info.size > 250_000) {
59
+ continue;
60
+ }
61
+ const text = await readFile(absolutePath, "utf8").catch(() => "");
62
+ files.push({
63
+ path: relative(root, absolutePath).replace(/\\/g, "/"),
64
+ absolutePath,
65
+ text,
66
+ lines: text === "" ? 0 : text.split(/\r?\n/).length
67
+ });
68
+ }
69
+ }
70
+ await walk(root);
71
+ return files;
72
+ }
@@ -0,0 +1,28 @@
1
+ export type FindingLevel = "info" | "warning" | "error";
2
+ export interface Finding {
3
+ code: string;
4
+ level: FindingLevel;
5
+ title: string;
6
+ detail: string;
7
+ points: number;
8
+ }
9
+ export interface RoastReport {
10
+ name: string;
11
+ root: string;
12
+ score: number;
13
+ roast: string;
14
+ findings: Finding[];
15
+ fixes: string[];
16
+ stats: {
17
+ files: number;
18
+ todoCount: number;
19
+ largestFiles: Array<{
20
+ path: string;
21
+ lines: number;
22
+ }>;
23
+ };
24
+ }
25
+ export interface AnalyzeOptions {
26
+ root?: string;
27
+ maxFiles?: number;
28
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@hevman/repo-roast",
3
+ "version": "0.1.0",
4
+ "description": "A tiny CLI that roasts your repository and turns the joke into a practical quality checklist.",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/hevman/repo-roast.git"
9
+ },
10
+ "homepage": "https://github.com/hevman/repo-roast#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/hevman/repo-roast/issues"
13
+ },
14
+ "bin": {
15
+ "repo-roast": "dist/cli.js"
16
+ },
17
+ "main": "dist/index.js",
18
+ "types": "dist/index.d.ts",
19
+ "files": [
20
+ "dist",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "scripts": {
25
+ "clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
26
+ "build": "npm run clean && tsc",
27
+ "test": "npm run build && node --test test/*.test.mjs",
28
+ "prepare": "npm run build",
29
+ "prepublishOnly": "npm test"
30
+ },
31
+ "keywords": [
32
+ "repo",
33
+ "roast",
34
+ "audit",
35
+ "quality",
36
+ "cli",
37
+ "readme",
38
+ "github",
39
+ "developer-tools"
40
+ ],
41
+ "author": "hevman",
42
+ "license": "MIT",
43
+ "engines": {
44
+ "node": ">=20"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^22.13.0",
48
+ "typescript": "^5.7.3"
49
+ }
50
+ }