vibeclean 1.0.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 +245 -0
- package/bin/vibeclean.js +154 -0
- package/package.json +58 -0
- package/src/analyzers/deadcode.js +294 -0
- package/src/analyzers/dependencies.js +241 -0
- package/src/analyzers/errorhandling.js +216 -0
- package/src/analyzers/leftovers.js +422 -0
- package/src/analyzers/naming.js +247 -0
- package/src/analyzers/patterns.js +381 -0
- package/src/analyzers/utils.js +204 -0
- package/src/baseline.js +134 -0
- package/src/config.js +207 -0
- package/src/fixers/safe-fixes.js +125 -0
- package/src/index.js +302 -0
- package/src/markdown-report.js +90 -0
- package/src/reporter.js +200 -0
- package/src/rules-generator.js +145 -0
- package/src/scanner.js +237 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function toTitle(value) {
|
|
5
|
+
return value
|
|
6
|
+
.split(/[-_]/)
|
|
7
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
8
|
+
.join(" ");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function pickPreferences(report, config = {}) {
|
|
12
|
+
const resultById = Object.fromEntries(report.categories.map((item) => [item.id, item]));
|
|
13
|
+
|
|
14
|
+
const namingPreference = resultById.naming?.preferences?.namingStyle || "camelCase";
|
|
15
|
+
const fileNaming = resultById.naming?.preferences?.fileNamingStyle || "kebab-case";
|
|
16
|
+
const httpClient =
|
|
17
|
+
config.allowedPatterns?.httpClient || resultById.patterns?.preferences?.httpClient || "fetch";
|
|
18
|
+
const asyncStyle =
|
|
19
|
+
config.allowedPatterns?.asyncStyle || resultById.patterns?.preferences?.asyncStyle || "async-await";
|
|
20
|
+
const importStyle = resultById.patterns?.preferences?.importStyle || "esm";
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
namingPreference,
|
|
24
|
+
fileNaming,
|
|
25
|
+
httpClient,
|
|
26
|
+
asyncStyle,
|
|
27
|
+
importStyle
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function markdownRules(preferences) {
|
|
32
|
+
const asyncPhrase = preferences.asyncStyle === "then-chains" ? ".then() chains" : "async/await";
|
|
33
|
+
|
|
34
|
+
return `# Project Coding Standards (Generated by vibeclean)
|
|
35
|
+
|
|
36
|
+
## Naming Conventions
|
|
37
|
+
- Use ${preferences.namingPreference} for variables and functions
|
|
38
|
+
- Use PascalCase for components and classes
|
|
39
|
+
- Use ${preferences.fileNaming} for file names
|
|
40
|
+
|
|
41
|
+
## Patterns
|
|
42
|
+
- Use ${preferences.httpClient} for all HTTP requests
|
|
43
|
+
- Use ${asyncPhrase} for all asynchronous code
|
|
44
|
+
- Use one module style consistently (${preferences.importStyle === "cjs" ? "CommonJS" : "ES modules"})
|
|
45
|
+
|
|
46
|
+
## Error Handling
|
|
47
|
+
- Always wrap risky async operations in try/catch
|
|
48
|
+
- Never use empty catch blocks
|
|
49
|
+
- Do not catch-and-log only; rethrow or return typed failures
|
|
50
|
+
|
|
51
|
+
## Imports
|
|
52
|
+
- Prefer ${preferences.importStyle === "cjs" ? "require/module.exports" : "import/export"}
|
|
53
|
+
- Keep import style consistent (default vs named) per library
|
|
54
|
+
|
|
55
|
+
## Code Hygiene
|
|
56
|
+
- No console.log in production code (use a logger utility)
|
|
57
|
+
- No TODO comments left behind in committed code
|
|
58
|
+
- No hardcoded localhost URLs or placeholder credentials
|
|
59
|
+
- No commented-out code blocks
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cursorRules(preferences) {
|
|
64
|
+
const asyncPhrase = preferences.asyncStyle === "then-chains" ? ".then() chains" : "async/await";
|
|
65
|
+
|
|
66
|
+
return `# .cursorrules generated by vibeclean
|
|
67
|
+
|
|
68
|
+
You are working on a codebase with strict consistency standards.
|
|
69
|
+
|
|
70
|
+
- Naming: ${preferences.namingPreference} for functions/variables, PascalCase for components.
|
|
71
|
+
- File names: ${preferences.fileNaming}.
|
|
72
|
+
- HTTP client: ${preferences.httpClient} only.
|
|
73
|
+
- Async style: ${asyncPhrase} only.
|
|
74
|
+
- Module system: ${preferences.importStyle === "cjs" ? "CommonJS" : "ES modules"} only.
|
|
75
|
+
- Error handling: no empty catch blocks, no catch-and-log-only handlers.
|
|
76
|
+
- Hygiene: avoid console logs, TODO leftovers, placeholders, and commented-out code.
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function claudeRules(preferences) {
|
|
81
|
+
return `# CLAUDE.md (generated by vibeclean)
|
|
82
|
+
|
|
83
|
+
## Repository Standards
|
|
84
|
+
|
|
85
|
+
1. Keep naming consistent:
|
|
86
|
+
- ${preferences.namingPreference} for variables/functions
|
|
87
|
+
- PascalCase for components/classes
|
|
88
|
+
- ${preferences.fileNaming} for filenames
|
|
89
|
+
|
|
90
|
+
2. Keep implementation patterns consistent:
|
|
91
|
+
- Use ${preferences.httpClient} for HTTP requests
|
|
92
|
+
- Use ${preferences.asyncStyle === "then-chains" ? ".then() chains" : "async/await"} for async flows
|
|
93
|
+
- Keep module syntax consistent (${preferences.importStyle === "cjs" ? "CommonJS" : "ES Modules"})
|
|
94
|
+
|
|
95
|
+
3. Keep error handling explicit:
|
|
96
|
+
- Use try/catch around async boundaries
|
|
97
|
+
- Avoid empty catch blocks
|
|
98
|
+
- Do not swallow errors after logging
|
|
99
|
+
|
|
100
|
+
4. Keep codebase clean:
|
|
101
|
+
- Remove TODO/FIXME placeholders before final output
|
|
102
|
+
- Avoid console logging unless explicitly requested
|
|
103
|
+
- Never commit hardcoded localhost URLs, keys, or dummy credentials
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function generateRulesFiles(report, options = {}) {
|
|
108
|
+
const preferences = pickPreferences(report, options.config);
|
|
109
|
+
const writes = [];
|
|
110
|
+
|
|
111
|
+
const rulesPath = path.join(options.rootDir, ".vibeclean-rules.md");
|
|
112
|
+
writes.push(
|
|
113
|
+
fs.writeFile(rulesPath, markdownRules(preferences), "utf8").then(() => ({
|
|
114
|
+
type: "rules",
|
|
115
|
+
path: rulesPath
|
|
116
|
+
}))
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
if (options.cursor) {
|
|
120
|
+
const cursorPath = path.join(options.rootDir, ".cursorrules");
|
|
121
|
+
writes.push(
|
|
122
|
+
fs.writeFile(cursorPath, cursorRules(preferences), "utf8").then(() => ({
|
|
123
|
+
type: "cursor",
|
|
124
|
+
path: cursorPath
|
|
125
|
+
}))
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (options.claude) {
|
|
130
|
+
const claudePath = path.join(options.rootDir, "CLAUDE.md");
|
|
131
|
+
writes.push(
|
|
132
|
+
fs.writeFile(claudePath, claudeRules(preferences), "utf8").then(() => ({
|
|
133
|
+
type: "claude",
|
|
134
|
+
path: claudePath
|
|
135
|
+
}))
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const generated = await Promise.all(writes);
|
|
140
|
+
return {
|
|
141
|
+
generated,
|
|
142
|
+
summary: generated.map((item) => `${toTitle(item.type)}: ${item.path}`).join("\n")
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
package/src/scanner.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { glob } from "glob";
|
|
7
|
+
import ignore from "ignore";
|
|
8
|
+
|
|
9
|
+
const SUPPORTED_EXTENSIONS = new Set([
|
|
10
|
+
".js",
|
|
11
|
+
".jsx",
|
|
12
|
+
".ts",
|
|
13
|
+
".tsx",
|
|
14
|
+
".mjs",
|
|
15
|
+
".cjs",
|
|
16
|
+
".vue",
|
|
17
|
+
".svelte"
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const BUILTIN_IGNORE_GLOBS = [
|
|
21
|
+
"**/node_modules/**",
|
|
22
|
+
"**/.git/**",
|
|
23
|
+
"**/dist/**",
|
|
24
|
+
"**/build/**",
|
|
25
|
+
"**/.next/**",
|
|
26
|
+
"**/.cache/**",
|
|
27
|
+
"**/coverage/**",
|
|
28
|
+
"**/__pycache__/**",
|
|
29
|
+
"**/*.lock",
|
|
30
|
+
"**/package-lock.json",
|
|
31
|
+
"**/pnpm-lock.yaml",
|
|
32
|
+
"**/yarn.lock",
|
|
33
|
+
"**/*.min.js",
|
|
34
|
+
"**/*.bundle.js",
|
|
35
|
+
"**/*.png",
|
|
36
|
+
"**/*.jpg",
|
|
37
|
+
"**/*.jpeg",
|
|
38
|
+
"**/*.gif",
|
|
39
|
+
"**/*.webp",
|
|
40
|
+
"**/*.svg",
|
|
41
|
+
"**/*.ico",
|
|
42
|
+
"**/*.woff",
|
|
43
|
+
"**/*.woff2",
|
|
44
|
+
"**/*.ttf",
|
|
45
|
+
"**/*.eot",
|
|
46
|
+
"**/*.otf",
|
|
47
|
+
"**/.env",
|
|
48
|
+
"**/.env.*"
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const execFileAsync = promisify(execFile);
|
|
52
|
+
|
|
53
|
+
function isTextContent(content) {
|
|
54
|
+
return !content.includes("\u0000");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function readFileTextStream(filePath) {
|
|
58
|
+
return await new Promise((resolve, reject) => {
|
|
59
|
+
const chunks = [];
|
|
60
|
+
const stream = createReadStream(filePath, { encoding: "utf8" });
|
|
61
|
+
|
|
62
|
+
stream.on("data", (chunk) => {
|
|
63
|
+
chunks.push(chunk);
|
|
64
|
+
});
|
|
65
|
+
stream.on("error", reject);
|
|
66
|
+
stream.on("end", () => {
|
|
67
|
+
resolve(chunks.join(""));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function readGitignore(rootDir) {
|
|
73
|
+
const gitignorePath = path.join(rootDir, ".gitignore");
|
|
74
|
+
try {
|
|
75
|
+
return await fs.readFile(gitignorePath, "utf8");
|
|
76
|
+
} catch {
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function splitLines(raw = "") {
|
|
82
|
+
return raw
|
|
83
|
+
.split(/\r?\n/)
|
|
84
|
+
.map((line) => line.trim())
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function listGitChangedPaths(rootDir, baseRef, warnings) {
|
|
89
|
+
try {
|
|
90
|
+
await execFileAsync("git", ["-C", rootDir, "rev-parse", "--is-inside-work-tree"]);
|
|
91
|
+
} catch {
|
|
92
|
+
warnings.push("`--changed` requested, but this directory is not a git repository. Scanning full project.");
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let changedFromBase = [];
|
|
97
|
+
try {
|
|
98
|
+
const { stdout } = await execFileAsync(
|
|
99
|
+
"git",
|
|
100
|
+
["-C", rootDir, "diff", "--name-only", "--diff-filter=ACMRTUXB", baseRef],
|
|
101
|
+
{ maxBuffer: 4 * 1024 * 1024 }
|
|
102
|
+
);
|
|
103
|
+
changedFromBase = splitLines(stdout);
|
|
104
|
+
} catch {
|
|
105
|
+
warnings.push(
|
|
106
|
+
`Could not resolve git base ref "${baseRef}" for --changed. Scanning full project instead.`
|
|
107
|
+
);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let untracked = [];
|
|
112
|
+
try {
|
|
113
|
+
const { stdout } = await execFileAsync(
|
|
114
|
+
"git",
|
|
115
|
+
["-C", rootDir, "ls-files", "--others", "--exclude-standard"],
|
|
116
|
+
{ maxBuffer: 2 * 1024 * 1024 }
|
|
117
|
+
);
|
|
118
|
+
untracked = splitLines(stdout);
|
|
119
|
+
} catch {
|
|
120
|
+
// Ignore untracked-file probe errors and keep changed-file scan usable.
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return [...new Set([...changedFromBase, ...untracked])];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function scanProject(rootDir, options = {}) {
|
|
127
|
+
const maxFiles = Number.isFinite(options.maxFiles) ? options.maxFiles : 500;
|
|
128
|
+
const maxFileSizeBytes =
|
|
129
|
+
(Number.isFinite(options.maxFileSizeKb) ? options.maxFileSizeKb : 100) * 1024;
|
|
130
|
+
const changedOnly = Boolean(options.changedOnly);
|
|
131
|
+
const changedBase =
|
|
132
|
+
typeof options.changedBase === "string" && options.changedBase.trim()
|
|
133
|
+
? options.changedBase.trim()
|
|
134
|
+
: "HEAD";
|
|
135
|
+
const warnings = [];
|
|
136
|
+
|
|
137
|
+
const ig = ignore();
|
|
138
|
+
const gitignoreRaw = await readGitignore(rootDir);
|
|
139
|
+
if (gitignoreRaw.trim()) {
|
|
140
|
+
ig.add(gitignoreRaw);
|
|
141
|
+
}
|
|
142
|
+
if (Array.isArray(options.ignore) && options.ignore.length > 0) {
|
|
143
|
+
ig.add(options.ignore);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let candidates = [];
|
|
147
|
+
let hasChangedSelection = false;
|
|
148
|
+
if (changedOnly) {
|
|
149
|
+
const changedPaths = await listGitChangedPaths(rootDir, changedBase, warnings);
|
|
150
|
+
if (Array.isArray(changedPaths)) {
|
|
151
|
+
hasChangedSelection = true;
|
|
152
|
+
candidates = changedPaths;
|
|
153
|
+
if (candidates.length === 0) {
|
|
154
|
+
warnings.push(`No changed files found relative to "${changedBase}".`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!hasChangedSelection) {
|
|
160
|
+
candidates = await glob("**/*", {
|
|
161
|
+
cwd: rootDir,
|
|
162
|
+
nodir: true,
|
|
163
|
+
dot: false,
|
|
164
|
+
absolute: false,
|
|
165
|
+
ignore: BUILTIN_IGNORE_GLOBS
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const filtered = [];
|
|
170
|
+
for (const relativePath of candidates) {
|
|
171
|
+
if (ig.ignores(relativePath)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const ext = path.extname(relativePath).toLowerCase();
|
|
176
|
+
if (!SUPPORTED_EXTENSIONS.has(ext)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
filtered.push(relativePath);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
filtered.sort();
|
|
184
|
+
|
|
185
|
+
const limited = filtered.slice(0, maxFiles);
|
|
186
|
+
if (filtered.length > maxFiles) {
|
|
187
|
+
warnings.push(
|
|
188
|
+
`Scan capped at ${maxFiles} files. ${filtered.length - maxFiles} files were not analyzed.`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const files = [];
|
|
193
|
+
for (const relativePath of limited) {
|
|
194
|
+
const absolutePath = path.join(rootDir, relativePath);
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const stats = await fs.stat(absolutePath);
|
|
198
|
+
if (!stats.isFile()) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (stats.size > maxFileSizeBytes) {
|
|
203
|
+
warnings.push(`Skipped large file: ${relativePath} (${Math.ceil(stats.size / 1024)}KB)`);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const content = await readFileTextStream(absolutePath);
|
|
208
|
+
if (!isTextContent(content)) {
|
|
209
|
+
warnings.push(`Skipped binary-like file: ${relativePath}`);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
files.push({
|
|
214
|
+
path: absolutePath,
|
|
215
|
+
relativePath,
|
|
216
|
+
content,
|
|
217
|
+
extension: path.extname(relativePath).toLowerCase(),
|
|
218
|
+
size: stats.size
|
|
219
|
+
});
|
|
220
|
+
} catch {
|
|
221
|
+
warnings.push(`Could not read file: ${relativePath}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
files,
|
|
227
|
+
warnings,
|
|
228
|
+
stats: {
|
|
229
|
+
scanned: files.length,
|
|
230
|
+
matched: filtered.length,
|
|
231
|
+
durationMs: 0
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export { SUPPORTED_EXTENSIONS, BUILTIN_IGNORE_GLOBS };
|
|
237
|
+
|