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.
@@ -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
+