@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 +21 -0
- package/README.md +82 -0
- package/dist/analyze.d.ts +2 -0
- package/dist/analyze.js +122 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +43 -0
- package/dist/format.d.ts +2 -0
- package/dist/format.js +41 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/scan.d.ts +7 -0
- package/dist/scan.js +72 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/package.json +50 -0
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
|
package/dist/analyze.js
ADDED
|
@@ -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
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
|
+
});
|
package/dist/format.d.ts
ADDED
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
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/scan.d.ts
ADDED
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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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
|
+
}
|