viberails 0.1.0 → 0.2.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/dist/index.cjs +732 -231
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +729 -228
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk7 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/commands/boundaries.ts
|
|
@@ -34,11 +34,11 @@ async function confirm(message) {
|
|
|
34
34
|
input: process.stdin,
|
|
35
35
|
output: process.stdout
|
|
36
36
|
});
|
|
37
|
-
return new Promise((
|
|
37
|
+
return new Promise((resolve4) => {
|
|
38
38
|
rl.question(`${message} (Y/n) `, (answer) => {
|
|
39
39
|
rl.close();
|
|
40
40
|
const trimmed = answer.trim().toLowerCase();
|
|
41
|
-
|
|
41
|
+
resolve4(trimmed === "" || trimmed === "y" || trimmed === "yes");
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
}
|
|
@@ -187,12 +187,42 @@ ${chalk.yellow("Cycles detected:")}`);
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
// src/commands/check.ts
|
|
190
|
+
import * as fs6 from "fs";
|
|
191
|
+
import * as path6 from "path";
|
|
192
|
+
import { loadConfig as loadConfig2 } from "@viberails/config";
|
|
193
|
+
import chalk2 from "chalk";
|
|
194
|
+
|
|
195
|
+
// src/commands/check-config.ts
|
|
196
|
+
function resolveConfigForFile(relPath, config) {
|
|
197
|
+
if (!config.packages || config.packages.length === 0) {
|
|
198
|
+
return { rules: config.rules, conventions: config.conventions };
|
|
199
|
+
}
|
|
200
|
+
const sortedPackages = [...config.packages].sort((a, b) => b.path.length - a.path.length);
|
|
201
|
+
for (const pkg of sortedPackages) {
|
|
202
|
+
if (relPath.startsWith(`${pkg.path}/`) || relPath === pkg.path) {
|
|
203
|
+
return {
|
|
204
|
+
rules: { ...config.rules, ...pkg.rules },
|
|
205
|
+
conventions: { ...config.conventions, ...pkg.conventions }
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { rules: config.rules, conventions: config.conventions };
|
|
210
|
+
}
|
|
211
|
+
function resolveIgnoreForFile(relPath, config) {
|
|
212
|
+
const globalIgnore = config.ignore;
|
|
213
|
+
if (!config.packages) return globalIgnore;
|
|
214
|
+
for (const pkg of config.packages) {
|
|
215
|
+
if (pkg.ignore && relPath.startsWith(`${pkg.path}/`)) {
|
|
216
|
+
return [...globalIgnore, ...pkg.ignore];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return globalIgnore;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/commands/check-files.ts
|
|
190
223
|
import { execSync } from "child_process";
|
|
191
224
|
import * as fs4 from "fs";
|
|
192
225
|
import * as path4 from "path";
|
|
193
|
-
import { loadConfig as loadConfig2 } from "@viberails/config";
|
|
194
|
-
import chalk2 from "chalk";
|
|
195
|
-
var CONFIG_FILE2 = "viberails.config.json";
|
|
196
226
|
var SOURCE_EXTS = /* @__PURE__ */ new Set([
|
|
197
227
|
".ts",
|
|
198
228
|
".tsx",
|
|
@@ -210,6 +240,160 @@ var NAMING_PATTERNS = {
|
|
|
210
240
|
PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
|
|
211
241
|
snake_case: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
|
|
212
242
|
};
|
|
243
|
+
function isIgnored(relPath, ignorePatterns) {
|
|
244
|
+
for (const pattern of ignorePatterns) {
|
|
245
|
+
if (pattern.endsWith("/**")) {
|
|
246
|
+
const prefix = pattern.slice(0, -3);
|
|
247
|
+
if (relPath.startsWith(`${prefix}/`) || relPath === prefix) return true;
|
|
248
|
+
} else if (pattern.startsWith("**/")) {
|
|
249
|
+
const suffix = pattern.slice(3);
|
|
250
|
+
if (relPath.endsWith(suffix)) return true;
|
|
251
|
+
} else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
function countFileLines(filePath) {
|
|
258
|
+
try {
|
|
259
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
260
|
+
if (content.length === 0) return 0;
|
|
261
|
+
let count = 1;
|
|
262
|
+
for (let i = 0; i < content.length; i++) {
|
|
263
|
+
if (content.charCodeAt(i) === 10) count++;
|
|
264
|
+
}
|
|
265
|
+
return count;
|
|
266
|
+
} catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function checkNaming(relPath, conventions) {
|
|
271
|
+
const filename = path4.basename(relPath);
|
|
272
|
+
const ext = path4.extname(filename);
|
|
273
|
+
if (!SOURCE_EXTS.has(ext)) return void 0;
|
|
274
|
+
if (filename.startsWith("index.") || filename.includes(".config.") || filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith(".")) {
|
|
275
|
+
return void 0;
|
|
276
|
+
}
|
|
277
|
+
const bare = filename.slice(0, filename.indexOf("."));
|
|
278
|
+
const convention = typeof conventions.fileNaming === "string" ? conventions.fileNaming : conventions.fileNaming?.value;
|
|
279
|
+
if (!convention) return void 0;
|
|
280
|
+
const pattern = NAMING_PATTERNS[convention];
|
|
281
|
+
if (!pattern || pattern.test(bare)) return void 0;
|
|
282
|
+
return `File name "${filename}" does not follow ${convention} convention.`;
|
|
283
|
+
}
|
|
284
|
+
function getStagedFiles(projectRoot) {
|
|
285
|
+
try {
|
|
286
|
+
const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
|
|
287
|
+
cwd: projectRoot,
|
|
288
|
+
encoding: "utf-8"
|
|
289
|
+
});
|
|
290
|
+
return output.trim().split("\n").filter(Boolean);
|
|
291
|
+
} catch {
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function getAllSourceFiles(projectRoot, config) {
|
|
296
|
+
const files = [];
|
|
297
|
+
const walk = (dir) => {
|
|
298
|
+
let entries;
|
|
299
|
+
try {
|
|
300
|
+
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
301
|
+
} catch {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
for (const entry of entries) {
|
|
305
|
+
const rel = path4.relative(projectRoot, path4.join(dir, entry.name));
|
|
306
|
+
if (entry.isDirectory()) {
|
|
307
|
+
if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (isIgnored(rel, config.ignore)) continue;
|
|
311
|
+
walk(path4.join(dir, entry.name));
|
|
312
|
+
} else if (entry.isFile()) {
|
|
313
|
+
const ext = path4.extname(entry.name);
|
|
314
|
+
if (SOURCE_EXTS.has(ext) && !isIgnored(rel, config.ignore)) {
|
|
315
|
+
files.push(rel);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
walk(projectRoot);
|
|
321
|
+
return files;
|
|
322
|
+
}
|
|
323
|
+
function collectSourceFiles(dir, projectRoot) {
|
|
324
|
+
const files = [];
|
|
325
|
+
const walk = (d) => {
|
|
326
|
+
let entries;
|
|
327
|
+
try {
|
|
328
|
+
entries = fs4.readdirSync(d, { withFileTypes: true });
|
|
329
|
+
} catch {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
for (const entry of entries) {
|
|
333
|
+
if (entry.isDirectory()) {
|
|
334
|
+
if (entry.name === "node_modules") continue;
|
|
335
|
+
walk(path4.join(d, entry.name));
|
|
336
|
+
} else if (entry.isFile()) {
|
|
337
|
+
files.push(path4.relative(projectRoot, path4.join(d, entry.name)));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
walk(dir);
|
|
342
|
+
return files;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/commands/check-tests.ts
|
|
346
|
+
import * as fs5 from "fs";
|
|
347
|
+
import * as path5 from "path";
|
|
348
|
+
var SOURCE_EXTS2 = /* @__PURE__ */ new Set([
|
|
349
|
+
".ts",
|
|
350
|
+
".tsx",
|
|
351
|
+
".js",
|
|
352
|
+
".jsx",
|
|
353
|
+
".mjs",
|
|
354
|
+
".cjs",
|
|
355
|
+
".vue",
|
|
356
|
+
".svelte",
|
|
357
|
+
".astro"
|
|
358
|
+
]);
|
|
359
|
+
function checkMissingTests(projectRoot, config, severity) {
|
|
360
|
+
const violations = [];
|
|
361
|
+
const { testPattern } = config.structure;
|
|
362
|
+
if (!testPattern) return violations;
|
|
363
|
+
const srcDir = config.structure.srcDir;
|
|
364
|
+
if (!srcDir) return violations;
|
|
365
|
+
const srcPath = path5.join(projectRoot, srcDir);
|
|
366
|
+
if (!fs5.existsSync(srcPath)) return violations;
|
|
367
|
+
const testSuffix = testPattern.replace("*", "");
|
|
368
|
+
const sourceFiles = collectSourceFiles(srcPath, projectRoot);
|
|
369
|
+
for (const relFile of sourceFiles) {
|
|
370
|
+
const basename6 = path5.basename(relFile);
|
|
371
|
+
if (basename6.includes(".test.") || basename6.includes(".spec.") || basename6.startsWith("index.") || basename6.endsWith(".d.ts")) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const ext = path5.extname(basename6);
|
|
375
|
+
if (!SOURCE_EXTS2.has(ext)) continue;
|
|
376
|
+
const stem = basename6.slice(0, basename6.indexOf("."));
|
|
377
|
+
const expectedTestFile = `${stem}${testSuffix}`;
|
|
378
|
+
const dir = path5.dirname(path5.join(projectRoot, relFile));
|
|
379
|
+
const colocatedTest = path5.join(dir, expectedTestFile);
|
|
380
|
+
const testsDir = config.structure.tests;
|
|
381
|
+
const dedicatedTest = testsDir ? path5.join(projectRoot, testsDir, expectedTestFile) : null;
|
|
382
|
+
const hasTest = fs5.existsSync(colocatedTest) || dedicatedTest !== null && fs5.existsSync(dedicatedTest);
|
|
383
|
+
if (!hasTest) {
|
|
384
|
+
violations.push({
|
|
385
|
+
file: relFile,
|
|
386
|
+
rule: "missing-test",
|
|
387
|
+
message: `No test file found. Expected \`${expectedTestFile}\`.`,
|
|
388
|
+
severity
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return violations;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/commands/check.ts
|
|
396
|
+
var CONFIG_FILE2 = "viberails.config.json";
|
|
213
397
|
async function checkCommand(options, cwd) {
|
|
214
398
|
const startDir = cwd ?? process.cwd();
|
|
215
399
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -217,8 +401,8 @@ async function checkCommand(options, cwd) {
|
|
|
217
401
|
console.error(`${chalk2.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
218
402
|
return 1;
|
|
219
403
|
}
|
|
220
|
-
const configPath =
|
|
221
|
-
if (!
|
|
404
|
+
const configPath = path6.join(projectRoot, CONFIG_FILE2);
|
|
405
|
+
if (!fs6.existsSync(configPath)) {
|
|
222
406
|
console.error(
|
|
223
407
|
`${chalk2.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
224
408
|
);
|
|
@@ -240,23 +424,25 @@ async function checkCommand(options, cwd) {
|
|
|
240
424
|
const violations = [];
|
|
241
425
|
const severity = config.enforcement === "enforce" ? "error" : "warn";
|
|
242
426
|
for (const file of filesToCheck) {
|
|
243
|
-
const absPath =
|
|
244
|
-
const relPath =
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
if (
|
|
427
|
+
const absPath = path6.isAbsolute(file) ? file : path6.join(projectRoot, file);
|
|
428
|
+
const relPath = path6.relative(projectRoot, absPath);
|
|
429
|
+
const effectiveIgnore = resolveIgnoreForFile(relPath, config);
|
|
430
|
+
if (isIgnored(relPath, effectiveIgnore)) continue;
|
|
431
|
+
if (!fs6.existsSync(absPath)) continue;
|
|
432
|
+
const resolved = resolveConfigForFile(relPath, config);
|
|
433
|
+
if (resolved.rules.maxFileLines > 0) {
|
|
248
434
|
const lines = countFileLines(absPath);
|
|
249
|
-
if (lines !== null && lines >
|
|
435
|
+
if (lines !== null && lines > resolved.rules.maxFileLines) {
|
|
250
436
|
violations.push({
|
|
251
437
|
file: relPath,
|
|
252
438
|
rule: "file-size",
|
|
253
|
-
message: `${lines} lines (max ${
|
|
439
|
+
message: `${lines} lines (max ${resolved.rules.maxFileLines}). Split into focused modules.`,
|
|
254
440
|
severity
|
|
255
441
|
});
|
|
256
442
|
}
|
|
257
443
|
}
|
|
258
|
-
if (
|
|
259
|
-
const namingViolation = checkNaming(relPath,
|
|
444
|
+
if (resolved.rules.enforceNaming && resolved.conventions.fileNaming) {
|
|
445
|
+
const namingViolation = checkNaming(relPath, resolved.conventions);
|
|
260
446
|
if (namingViolation) {
|
|
261
447
|
violations.push({
|
|
262
448
|
file: relPath,
|
|
@@ -280,10 +466,10 @@ async function checkCommand(options, cwd) {
|
|
|
280
466
|
ignore: config.ignore
|
|
281
467
|
});
|
|
282
468
|
const boundaryViolations = checkBoundaries(graph, config.boundaries);
|
|
283
|
-
const filterSet = options.staged || options.files ? new Set(filesToCheck.map((f) =>
|
|
469
|
+
const filterSet = options.staged || options.files ? new Set(filesToCheck.map((f) => path6.resolve(projectRoot, f))) : null;
|
|
284
470
|
for (const bv of boundaryViolations) {
|
|
285
471
|
if (filterSet && !filterSet.has(bv.file)) continue;
|
|
286
|
-
const relFile =
|
|
472
|
+
const relFile = path6.relative(projectRoot, bv.file);
|
|
287
473
|
violations.push({
|
|
288
474
|
file: relFile,
|
|
289
475
|
rule: "boundary-violation",
|
|
@@ -311,153 +497,378 @@ ${violations.length} ${word} found.`);
|
|
|
311
497
|
}
|
|
312
498
|
return 0;
|
|
313
499
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
500
|
+
|
|
501
|
+
// src/commands/fix.ts
|
|
502
|
+
import { execSync as execSync2 } from "child_process";
|
|
503
|
+
import * as fs9 from "fs";
|
|
504
|
+
import * as path10 from "path";
|
|
505
|
+
import { createInterface as createInterface2 } from "readline";
|
|
506
|
+
import { loadConfig as loadConfig3 } from "@viberails/config";
|
|
507
|
+
import chalk3 from "chalk";
|
|
508
|
+
|
|
509
|
+
// src/commands/fix-imports.ts
|
|
510
|
+
import * as path7 from "path";
|
|
511
|
+
function stripExtension(filePath) {
|
|
512
|
+
return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
|
|
326
513
|
}
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
const bare = filename.slice(0, filename.indexOf("."));
|
|
335
|
-
const convention = typeof config.conventions.fileNaming === "string" ? config.conventions.fileNaming : config.conventions.fileNaming?.value;
|
|
336
|
-
if (!convention) return void 0;
|
|
337
|
-
const pattern = NAMING_PATTERNS[convention];
|
|
338
|
-
if (!pattern || pattern.test(bare)) return void 0;
|
|
339
|
-
return `File name "${filename}" does not follow ${convention} convention.`;
|
|
514
|
+
function computeNewSpecifier(oldSpecifier, newBare) {
|
|
515
|
+
const hasJsExt = oldSpecifier.endsWith(".js");
|
|
516
|
+
const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
|
|
517
|
+
const dir = base.lastIndexOf("/");
|
|
518
|
+
const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
|
|
519
|
+
const newSpec = prefix + newBare;
|
|
520
|
+
return hasJsExt ? `${newSpec}.js` : newSpec;
|
|
340
521
|
}
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
const {
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
522
|
+
async function updateImportsAfterRenames(renames, projectRoot) {
|
|
523
|
+
if (renames.length === 0) return [];
|
|
524
|
+
const { Project, SyntaxKind } = await import("ts-morph");
|
|
525
|
+
const renameMap = /* @__PURE__ */ new Map();
|
|
526
|
+
for (const r of renames) {
|
|
527
|
+
const oldStripped = stripExtension(r.oldAbsPath);
|
|
528
|
+
const newFilename = path7.basename(r.newPath);
|
|
529
|
+
const newName = newFilename.slice(0, newFilename.indexOf("."));
|
|
530
|
+
renameMap.set(oldStripped, { newBare: newName });
|
|
531
|
+
}
|
|
532
|
+
const project = new Project({
|
|
533
|
+
tsConfigFilePath: void 0,
|
|
534
|
+
skipAddingFilesFromTsConfig: true
|
|
535
|
+
});
|
|
536
|
+
project.addSourceFilesAtPaths(path7.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
|
|
537
|
+
const updates = [];
|
|
538
|
+
const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
539
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
540
|
+
const filePath = sourceFile.getFilePath();
|
|
541
|
+
if (filePath.includes("/node_modules/") || filePath.includes("/dist/")) continue;
|
|
542
|
+
const fileDir = path7.dirname(filePath);
|
|
543
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
544
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
545
|
+
if (!specifier.startsWith(".")) continue;
|
|
546
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
547
|
+
if (!match) continue;
|
|
548
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
549
|
+
updates.push({
|
|
550
|
+
file: filePath,
|
|
551
|
+
oldSpecifier: specifier,
|
|
552
|
+
newSpecifier: newSpec,
|
|
553
|
+
line: decl.getStartLineNumber()
|
|
554
|
+
});
|
|
555
|
+
decl.setModuleSpecifier(newSpec);
|
|
355
556
|
}
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
file: relFile,
|
|
368
|
-
rule: "missing-test",
|
|
369
|
-
message: `No test file found. Expected \`${expectedTestFile}\`.`,
|
|
370
|
-
severity
|
|
557
|
+
for (const decl of sourceFile.getExportDeclarations()) {
|
|
558
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
559
|
+
if (!specifier || !specifier.startsWith(".")) continue;
|
|
560
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
561
|
+
if (!match) continue;
|
|
562
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
563
|
+
updates.push({
|
|
564
|
+
file: filePath,
|
|
565
|
+
oldSpecifier: specifier,
|
|
566
|
+
newSpecifier: newSpec,
|
|
567
|
+
line: decl.getStartLineNumber()
|
|
371
568
|
});
|
|
569
|
+
decl.setModuleSpecifier(newSpec);
|
|
570
|
+
}
|
|
571
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
572
|
+
if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
|
|
573
|
+
const args = call.getArguments();
|
|
574
|
+
if (args.length === 0) continue;
|
|
575
|
+
const arg = args[0];
|
|
576
|
+
if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
|
|
577
|
+
const specifier = arg.getText().slice(1, -1);
|
|
578
|
+
if (!specifier.startsWith(".")) continue;
|
|
579
|
+
const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
|
|
580
|
+
if (!match) continue;
|
|
581
|
+
const newSpec = computeNewSpecifier(specifier, match.newBare);
|
|
582
|
+
updates.push({
|
|
583
|
+
file: filePath,
|
|
584
|
+
oldSpecifier: specifier,
|
|
585
|
+
newSpecifier: newSpec,
|
|
586
|
+
line: call.getStartLineNumber()
|
|
587
|
+
});
|
|
588
|
+
const quote = arg.getText()[0];
|
|
589
|
+
arg.replaceWithText(`${quote}${newSpec}${quote}`);
|
|
372
590
|
}
|
|
373
591
|
}
|
|
374
|
-
|
|
592
|
+
if (updates.length > 0) {
|
|
593
|
+
await project.save();
|
|
594
|
+
}
|
|
595
|
+
return updates;
|
|
375
596
|
}
|
|
376
|
-
function
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
return [];
|
|
597
|
+
function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
|
|
598
|
+
const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
|
|
599
|
+
const resolved = path7.resolve(fromDir, cleanSpec);
|
|
600
|
+
for (const ext of extensions) {
|
|
601
|
+
const candidate = resolved + ext;
|
|
602
|
+
const stripped = stripExtension(candidate);
|
|
603
|
+
const match = renameMap.get(stripped);
|
|
604
|
+
if (match) return match;
|
|
385
605
|
}
|
|
606
|
+
return void 0;
|
|
386
607
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
608
|
+
|
|
609
|
+
// src/commands/fix-naming.ts
|
|
610
|
+
import * as fs7 from "fs";
|
|
611
|
+
import * as path8 from "path";
|
|
612
|
+
|
|
613
|
+
// src/commands/convert-name.ts
|
|
614
|
+
function splitIntoWords(name) {
|
|
615
|
+
const parts = name.split(/[-_]/);
|
|
616
|
+
const words = [];
|
|
617
|
+
for (const part of parts) {
|
|
618
|
+
if (part === "") continue;
|
|
619
|
+
let current = "";
|
|
620
|
+
for (let i = 0; i < part.length; i++) {
|
|
621
|
+
const ch = part[i];
|
|
622
|
+
const isUpper = ch >= "A" && ch <= "Z";
|
|
623
|
+
if (isUpper && current.length > 0) {
|
|
624
|
+
const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
|
|
625
|
+
const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
|
|
626
|
+
if (!prevIsUpper || nextIsLower) {
|
|
627
|
+
words.push(current.toLowerCase());
|
|
628
|
+
current = "";
|
|
408
629
|
}
|
|
409
630
|
}
|
|
631
|
+
current += ch;
|
|
410
632
|
}
|
|
633
|
+
if (current) words.push(current.toLowerCase());
|
|
634
|
+
}
|
|
635
|
+
return words;
|
|
636
|
+
}
|
|
637
|
+
function convertName(bare, target) {
|
|
638
|
+
const words = splitIntoWords(bare);
|
|
639
|
+
if (words.length === 0) return bare;
|
|
640
|
+
switch (target) {
|
|
641
|
+
case "kebab-case":
|
|
642
|
+
return words.join("-");
|
|
643
|
+
case "camelCase":
|
|
644
|
+
return words[0] + words.slice(1).map(capitalize).join("");
|
|
645
|
+
case "PascalCase":
|
|
646
|
+
return words.map(capitalize).join("");
|
|
647
|
+
case "snake_case":
|
|
648
|
+
return words.join("_");
|
|
649
|
+
default:
|
|
650
|
+
return bare;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function capitalize(word) {
|
|
654
|
+
if (word.length === 0) return word;
|
|
655
|
+
return word[0].toUpperCase() + word.slice(1);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/commands/fix-naming.ts
|
|
659
|
+
function computeRename(relPath, targetConvention, projectRoot) {
|
|
660
|
+
const filename = path8.basename(relPath);
|
|
661
|
+
const dir = path8.dirname(relPath);
|
|
662
|
+
const dotIndex = filename.indexOf(".");
|
|
663
|
+
if (dotIndex === -1) return null;
|
|
664
|
+
const bare = filename.slice(0, dotIndex);
|
|
665
|
+
const suffix = filename.slice(dotIndex);
|
|
666
|
+
const newBare = convertName(bare, targetConvention);
|
|
667
|
+
if (newBare === bare) return null;
|
|
668
|
+
const newFilename = newBare + suffix;
|
|
669
|
+
const newRelPath = path8.join(dir, newFilename);
|
|
670
|
+
const oldAbsPath = path8.join(projectRoot, relPath);
|
|
671
|
+
const newAbsPath = path8.join(projectRoot, newRelPath);
|
|
672
|
+
if (fs7.existsSync(newAbsPath)) return null;
|
|
673
|
+
return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
|
|
674
|
+
}
|
|
675
|
+
function executeRename(rename) {
|
|
676
|
+
if (fs7.existsSync(rename.newAbsPath)) return false;
|
|
677
|
+
fs7.renameSync(rename.oldAbsPath, rename.newAbsPath);
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
function deduplicateRenames(renames) {
|
|
681
|
+
const seen = /* @__PURE__ */ new Set();
|
|
682
|
+
const result = [];
|
|
683
|
+
for (const r of renames) {
|
|
684
|
+
if (seen.has(r.newAbsPath)) continue;
|
|
685
|
+
seen.add(r.newAbsPath);
|
|
686
|
+
result.push(r);
|
|
687
|
+
}
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/commands/fix-tests.ts
|
|
692
|
+
import * as fs8 from "fs";
|
|
693
|
+
import * as path9 from "path";
|
|
694
|
+
function generateTestStub(sourceRelPath, config, projectRoot) {
|
|
695
|
+
const { testPattern } = config.structure;
|
|
696
|
+
if (!testPattern) return null;
|
|
697
|
+
const basename6 = path9.basename(sourceRelPath);
|
|
698
|
+
const stem = basename6.slice(0, basename6.indexOf("."));
|
|
699
|
+
const testSuffix = testPattern.replace("*", "");
|
|
700
|
+
const testFilename = `${stem}${testSuffix}`;
|
|
701
|
+
const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
|
|
702
|
+
const testAbsPath = path9.join(dir, testFilename);
|
|
703
|
+
if (fs8.existsSync(testAbsPath)) return null;
|
|
704
|
+
return {
|
|
705
|
+
path: path9.relative(projectRoot, testAbsPath),
|
|
706
|
+
absPath: testAbsPath,
|
|
707
|
+
moduleName: stem
|
|
411
708
|
};
|
|
412
|
-
walk(projectRoot);
|
|
413
|
-
return files;
|
|
414
709
|
}
|
|
415
|
-
function
|
|
416
|
-
const
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
710
|
+
function writeTestStub(stub, config) {
|
|
711
|
+
const runner = config.stack.testRunner === "jest" ? "jest" : "vitest";
|
|
712
|
+
const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
|
|
713
|
+
const content = `${importLine}describe('${stub.moduleName}', () => {
|
|
714
|
+
it.todo('add tests');
|
|
715
|
+
});
|
|
716
|
+
`;
|
|
717
|
+
fs8.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
|
|
718
|
+
fs8.writeFileSync(stub.absPath, content);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// src/commands/fix.ts
|
|
722
|
+
var CONFIG_FILE3 = "viberails.config.json";
|
|
723
|
+
async function fixCommand(options, cwd) {
|
|
724
|
+
const startDir = cwd ?? process.cwd();
|
|
725
|
+
const projectRoot = findProjectRoot(startDir);
|
|
726
|
+
if (!projectRoot) {
|
|
727
|
+
console.error(`${chalk3.red("Error:")} No package.json found. Are you in a JS/TS project?`);
|
|
728
|
+
return 1;
|
|
729
|
+
}
|
|
730
|
+
const configPath = path10.join(projectRoot, CONFIG_FILE3);
|
|
731
|
+
if (!fs9.existsSync(configPath)) {
|
|
732
|
+
console.error(
|
|
733
|
+
`${chalk3.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
|
|
734
|
+
);
|
|
735
|
+
return 1;
|
|
736
|
+
}
|
|
737
|
+
const config = await loadConfig3(configPath);
|
|
738
|
+
if (!options.yes && !options.dryRun) {
|
|
739
|
+
const isDirty = checkGitDirty(projectRoot);
|
|
740
|
+
if (isDirty) {
|
|
741
|
+
console.log(
|
|
742
|
+
chalk3.yellow("Warning: You have uncommitted changes. Consider committing first.")
|
|
743
|
+
);
|
|
423
744
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
745
|
+
}
|
|
746
|
+
const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
|
|
747
|
+
const shouldFixTests = !options.rule || options.rule.includes("missing-test");
|
|
748
|
+
const allFiles = getAllSourceFiles(projectRoot, config);
|
|
749
|
+
const renames = [];
|
|
750
|
+
if (shouldFixNaming) {
|
|
751
|
+
for (const file of allFiles) {
|
|
752
|
+
const resolved = resolveConfigForFile(file, config);
|
|
753
|
+
if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
|
|
754
|
+
const violation = checkNaming(file, resolved.conventions);
|
|
755
|
+
if (!violation) continue;
|
|
756
|
+
const convention = getConventionValue(resolved.conventions.fileNaming);
|
|
757
|
+
if (!convention) continue;
|
|
758
|
+
const rename = computeRename(file, convention, projectRoot);
|
|
759
|
+
if (rename) renames.push(rename);
|
|
431
760
|
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
761
|
+
}
|
|
762
|
+
const dedupedRenames = deduplicateRenames(renames);
|
|
763
|
+
const testStubs = [];
|
|
764
|
+
if (shouldFixTests && config.rules.requireTests) {
|
|
765
|
+
const testViolations = checkMissingTests(projectRoot, config, "warn");
|
|
766
|
+
for (const v of testViolations) {
|
|
767
|
+
const stub = generateTestStub(v.file, config, projectRoot);
|
|
768
|
+
if (stub) testStubs.push(stub);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (dedupedRenames.length === 0 && testStubs.length === 0) {
|
|
772
|
+
console.log(`${chalk3.green("\u2713")} No fixable violations found.`);
|
|
773
|
+
return 0;
|
|
774
|
+
}
|
|
775
|
+
printPlan(dedupedRenames, testStubs);
|
|
776
|
+
if (options.dryRun) {
|
|
777
|
+
console.log(chalk3.dim("\nDry run \u2014 no changes applied."));
|
|
778
|
+
return 0;
|
|
779
|
+
}
|
|
780
|
+
if (!options.yes) {
|
|
781
|
+
const confirmed = await promptConfirm("Apply these fixes?");
|
|
782
|
+
if (!confirmed) {
|
|
783
|
+
console.log("Aborted.");
|
|
784
|
+
return 0;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
let renameCount = 0;
|
|
788
|
+
for (const rename of dedupedRenames) {
|
|
789
|
+
if (executeRename(rename)) {
|
|
790
|
+
renameCount++;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
let importUpdateCount = 0;
|
|
794
|
+
if (renameCount > 0) {
|
|
795
|
+
const appliedRenames = dedupedRenames.filter((r) => fs9.existsSync(r.newAbsPath));
|
|
796
|
+
const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
|
|
797
|
+
importUpdateCount = updates.length;
|
|
798
|
+
}
|
|
799
|
+
let stubCount = 0;
|
|
800
|
+
for (const stub of testStubs) {
|
|
801
|
+
if (!fs9.existsSync(stub.absPath)) {
|
|
802
|
+
writeTestStub(stub, config);
|
|
803
|
+
stubCount++;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
console.log("");
|
|
807
|
+
if (renameCount > 0) {
|
|
808
|
+
console.log(`${chalk3.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
|
|
809
|
+
}
|
|
810
|
+
if (importUpdateCount > 0) {
|
|
811
|
+
console.log(
|
|
812
|
+
`${chalk3.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (stubCount > 0) {
|
|
816
|
+
console.log(`${chalk3.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
|
|
817
|
+
}
|
|
818
|
+
return 0;
|
|
435
819
|
}
|
|
436
|
-
function
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
} else if (pattern.startsWith("**/")) {
|
|
442
|
-
const suffix = pattern.slice(3);
|
|
443
|
-
if (relPath.endsWith(suffix)) return true;
|
|
444
|
-
} else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
|
|
445
|
-
return true;
|
|
820
|
+
function printPlan(renames, stubs) {
|
|
821
|
+
if (renames.length > 0) {
|
|
822
|
+
console.log(chalk3.bold("\nFile renames:"));
|
|
823
|
+
for (const r of renames) {
|
|
824
|
+
console.log(` ${chalk3.red(r.oldPath)} \u2192 ${chalk3.green(r.newPath)}`);
|
|
446
825
|
}
|
|
447
826
|
}
|
|
448
|
-
|
|
827
|
+
if (stubs.length > 0) {
|
|
828
|
+
console.log(chalk3.bold("\nTest stubs to create:"));
|
|
829
|
+
for (const s of stubs) {
|
|
830
|
+
console.log(` ${chalk3.green("+")} ${s.path}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function checkGitDirty(projectRoot) {
|
|
835
|
+
try {
|
|
836
|
+
const output = execSync2("git status --porcelain", {
|
|
837
|
+
cwd: projectRoot,
|
|
838
|
+
encoding: "utf-8"
|
|
839
|
+
});
|
|
840
|
+
return output.trim().length > 0;
|
|
841
|
+
} catch {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function getConventionValue(convention) {
|
|
846
|
+
if (typeof convention === "string") return convention;
|
|
847
|
+
if (convention && typeof convention === "object" && "value" in convention) {
|
|
848
|
+
return convention.value;
|
|
849
|
+
}
|
|
850
|
+
return void 0;
|
|
851
|
+
}
|
|
852
|
+
function promptConfirm(question) {
|
|
853
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
854
|
+
return new Promise((resolve4) => {
|
|
855
|
+
rl.question(`${question} (y/N) `, (answer) => {
|
|
856
|
+
rl.close();
|
|
857
|
+
resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
|
|
858
|
+
});
|
|
859
|
+
});
|
|
449
860
|
}
|
|
450
861
|
|
|
451
862
|
// src/commands/init.ts
|
|
452
|
-
import * as
|
|
453
|
-
import * as
|
|
863
|
+
import * as fs11 from "fs";
|
|
864
|
+
import * as path12 from "path";
|
|
454
865
|
import { generateConfig } from "@viberails/config";
|
|
455
866
|
import { scan } from "@viberails/scanner";
|
|
456
|
-
import
|
|
867
|
+
import chalk5 from "chalk";
|
|
457
868
|
|
|
458
869
|
// src/display.ts
|
|
459
870
|
import { FRAMEWORK_NAMES, LIBRARY_NAMES, ROLE_DESCRIPTIONS, STYLING_NAMES } from "@viberails/types";
|
|
460
|
-
import
|
|
871
|
+
import chalk4 from "chalk";
|
|
461
872
|
var CONVENTION_LABELS = {
|
|
462
873
|
fileNaming: "File naming",
|
|
463
874
|
componentNaming: "Component naming",
|
|
@@ -475,53 +886,128 @@ function confidenceLabel(convention) {
|
|
|
475
886
|
}
|
|
476
887
|
return `${pct}% \u2014 medium confidence, suggested only`;
|
|
477
888
|
}
|
|
889
|
+
function formatPackageSummary(pkg) {
|
|
890
|
+
const parts = [];
|
|
891
|
+
if (pkg.stack.framework) {
|
|
892
|
+
parts.push(formatItem(pkg.stack.framework, FRAMEWORK_NAMES));
|
|
893
|
+
}
|
|
894
|
+
if (pkg.stack.styling) {
|
|
895
|
+
parts.push(formatItem(pkg.stack.styling, STYLING_NAMES));
|
|
896
|
+
}
|
|
897
|
+
const files = `${pkg.statistics.totalFiles} files`;
|
|
898
|
+
const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
|
|
899
|
+
return ` ${pkg.relativePath} \u2014 ${detail}`;
|
|
900
|
+
}
|
|
901
|
+
function displayMonorepoResults(scanResult) {
|
|
902
|
+
const { stack, packages } = scanResult;
|
|
903
|
+
console.log(`
|
|
904
|
+
${chalk4.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
|
|
905
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.language)}`);
|
|
906
|
+
if (stack.packageManager) {
|
|
907
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
908
|
+
}
|
|
909
|
+
if (stack.linter) {
|
|
910
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
911
|
+
}
|
|
912
|
+
if (stack.testRunner) {
|
|
913
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
914
|
+
}
|
|
915
|
+
console.log("");
|
|
916
|
+
for (const pkg of packages) {
|
|
917
|
+
console.log(formatPackageSummary(pkg));
|
|
918
|
+
}
|
|
919
|
+
const packagesWithDirs = packages.filter(
|
|
920
|
+
(pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
|
|
921
|
+
);
|
|
922
|
+
if (packagesWithDirs.length > 0) {
|
|
923
|
+
console.log(`
|
|
924
|
+
${chalk4.bold("Structure:")}`);
|
|
925
|
+
for (const pkg of packagesWithDirs) {
|
|
926
|
+
const meaningfulDirs = pkg.structure.directories.filter((d) => d.role !== "unknown");
|
|
927
|
+
if (meaningfulDirs.length === 0) continue;
|
|
928
|
+
console.log(` ${pkg.relativePath}:`);
|
|
929
|
+
for (const dir of meaningfulDirs) {
|
|
930
|
+
const label = ROLE_DESCRIPTIONS[dir.role] ?? dir.role;
|
|
931
|
+
const files = dir.fileCount === 1 ? "1 file" : `${dir.fileCount} files`;
|
|
932
|
+
console.log(` ${chalk4.green("\u2713")} ${dir.path} \u2014 ${label} (${files})`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
const conventionEntries = Object.entries(scanResult.conventions);
|
|
937
|
+
if (conventionEntries.length > 0) {
|
|
938
|
+
console.log(`
|
|
939
|
+
${chalk4.bold("Conventions:")}`);
|
|
940
|
+
for (const [key, convention] of conventionEntries) {
|
|
941
|
+
if (convention.confidence === "low") continue;
|
|
942
|
+
const label = CONVENTION_LABELS[key] ?? key;
|
|
943
|
+
const pkgValues = packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
|
|
944
|
+
const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
|
|
945
|
+
if (allSame || pkgValues.length <= 1) {
|
|
946
|
+
const ind = convention.confidence === "high" ? chalk4.green("\u2713") : chalk4.yellow("~");
|
|
947
|
+
const detail = chalk4.dim(`(${confidenceLabel(convention)})`);
|
|
948
|
+
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
949
|
+
} else {
|
|
950
|
+
console.log(` ${chalk4.yellow("~")} ${label}: varies by package`);
|
|
951
|
+
for (const pv of pkgValues) {
|
|
952
|
+
const pct = Math.round(pv.convention.consistency);
|
|
953
|
+
console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
console.log("");
|
|
959
|
+
}
|
|
478
960
|
function displayScanResults(scanResult) {
|
|
961
|
+
if (scanResult.packages.length > 1) {
|
|
962
|
+
displayMonorepoResults(scanResult);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
479
965
|
const { stack, conventions } = scanResult;
|
|
480
966
|
console.log(`
|
|
481
|
-
${
|
|
967
|
+
${chalk4.bold("Detected:")}`);
|
|
482
968
|
if (stack.framework) {
|
|
483
|
-
console.log(` ${
|
|
969
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.framework, FRAMEWORK_NAMES)}`);
|
|
484
970
|
}
|
|
485
|
-
console.log(` ${
|
|
971
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.language)}`);
|
|
486
972
|
if (stack.styling) {
|
|
487
|
-
console.log(` ${
|
|
973
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.styling, STYLING_NAMES)}`);
|
|
488
974
|
}
|
|
489
975
|
if (stack.backend) {
|
|
490
|
-
console.log(` ${
|
|
976
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.backend, FRAMEWORK_NAMES)}`);
|
|
491
977
|
}
|
|
492
978
|
if (stack.linter) {
|
|
493
|
-
console.log(` ${
|
|
979
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.linter)}`);
|
|
494
980
|
}
|
|
495
981
|
if (stack.testRunner) {
|
|
496
|
-
console.log(` ${
|
|
982
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.testRunner)}`);
|
|
497
983
|
}
|
|
498
984
|
if (stack.packageManager) {
|
|
499
|
-
console.log(` ${
|
|
985
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(stack.packageManager)}`);
|
|
500
986
|
}
|
|
501
987
|
if (stack.libraries.length > 0) {
|
|
502
988
|
for (const lib of stack.libraries) {
|
|
503
|
-
console.log(` ${
|
|
989
|
+
console.log(` ${chalk4.green("\u2713")} ${formatItem(lib, LIBRARY_NAMES)}`);
|
|
504
990
|
}
|
|
505
991
|
}
|
|
506
992
|
const meaningfulDirs = scanResult.structure.directories.filter((d) => d.role !== "unknown");
|
|
507
993
|
if (meaningfulDirs.length > 0) {
|
|
508
994
|
console.log(`
|
|
509
|
-
${
|
|
995
|
+
${chalk4.bold("Structure:")}`);
|
|
510
996
|
for (const dir of meaningfulDirs) {
|
|
511
997
|
const label = ROLE_DESCRIPTIONS[dir.role] ?? dir.role;
|
|
512
998
|
const files = dir.fileCount === 1 ? "1 file" : `${dir.fileCount} files`;
|
|
513
|
-
console.log(` ${
|
|
999
|
+
console.log(` ${chalk4.green("\u2713")} ${dir.path} \u2014 ${label} (${files})`);
|
|
514
1000
|
}
|
|
515
1001
|
}
|
|
516
1002
|
const conventionEntries = Object.entries(conventions);
|
|
517
1003
|
if (conventionEntries.length > 0) {
|
|
518
1004
|
console.log(`
|
|
519
|
-
${
|
|
1005
|
+
${chalk4.bold("Conventions:")}`);
|
|
520
1006
|
for (const [key, convention] of conventionEntries) {
|
|
521
1007
|
if (convention.confidence === "low") continue;
|
|
522
1008
|
const label = CONVENTION_LABELS[key] ?? key;
|
|
523
|
-
const ind = convention.confidence === "high" ?
|
|
524
|
-
const detail =
|
|
1009
|
+
const ind = convention.confidence === "high" ? chalk4.green("\u2713") : chalk4.yellow("~");
|
|
1010
|
+
const detail = chalk4.dim(`(${confidenceLabel(convention)})`);
|
|
525
1011
|
console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
|
|
526
1012
|
}
|
|
527
1013
|
}
|
|
@@ -529,28 +1015,33 @@ ${chalk3.bold("Conventions:")}`);
|
|
|
529
1015
|
}
|
|
530
1016
|
|
|
531
1017
|
// src/utils/write-generated-files.ts
|
|
532
|
-
import * as
|
|
533
|
-
import * as
|
|
1018
|
+
import * as fs10 from "fs";
|
|
1019
|
+
import * as path11 from "path";
|
|
534
1020
|
import { generateContext } from "@viberails/context";
|
|
535
1021
|
var CONTEXT_DIR = ".viberails";
|
|
536
1022
|
var CONTEXT_FILE = "context.md";
|
|
537
1023
|
var SCAN_RESULT_FILE = "scan-result.json";
|
|
538
1024
|
function writeGeneratedFiles(projectRoot, config, scanResult) {
|
|
539
|
-
const contextDir =
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1025
|
+
const contextDir = path11.join(projectRoot, CONTEXT_DIR);
|
|
1026
|
+
try {
|
|
1027
|
+
if (!fs10.existsSync(contextDir)) {
|
|
1028
|
+
fs10.mkdirSync(contextDir, { recursive: true });
|
|
1029
|
+
}
|
|
1030
|
+
const context = generateContext(config);
|
|
1031
|
+
fs10.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
|
|
1032
|
+
fs10.writeFileSync(
|
|
1033
|
+
path11.join(contextDir, SCAN_RESULT_FILE),
|
|
1034
|
+
`${JSON.stringify(scanResult, null, 2)}
|
|
548
1035
|
`
|
|
549
|
-
|
|
1036
|
+
);
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1039
|
+
throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
|
|
1040
|
+
}
|
|
550
1041
|
}
|
|
551
1042
|
|
|
552
1043
|
// src/commands/init.ts
|
|
553
|
-
var
|
|
1044
|
+
var CONFIG_FILE4 = "viberails.config.json";
|
|
554
1045
|
function filterHighConfidence(conventions) {
|
|
555
1046
|
const filtered = {};
|
|
556
1047
|
for (const [key, value] of Object.entries(conventions)) {
|
|
@@ -571,19 +1062,19 @@ async function initCommand(options, cwd) {
|
|
|
571
1062
|
"No package.json found in this directory or any parent.\n\nMake sure you are inside a JavaScript or TypeScript project, then run:\n npx viberails"
|
|
572
1063
|
);
|
|
573
1064
|
}
|
|
574
|
-
const configPath =
|
|
575
|
-
if (
|
|
1065
|
+
const configPath = path12.join(projectRoot, CONFIG_FILE4);
|
|
1066
|
+
if (fs11.existsSync(configPath)) {
|
|
576
1067
|
console.log(
|
|
577
|
-
|
|
1068
|
+
chalk5.yellow("!") + " viberails is already initialized in this project.\n Run " + chalk5.cyan("viberails sync") + " to update the generated files."
|
|
578
1069
|
);
|
|
579
1070
|
return;
|
|
580
1071
|
}
|
|
581
|
-
console.log(
|
|
1072
|
+
console.log(chalk5.dim("Scanning project..."));
|
|
582
1073
|
const scanResult = await scan(projectRoot);
|
|
583
1074
|
displayScanResults(scanResult);
|
|
584
1075
|
if (scanResult.statistics.totalFiles === 0) {
|
|
585
1076
|
console.log(
|
|
586
|
-
|
|
1077
|
+
chalk5.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + chalk5.cyan("viberails sync") + " after adding source files.\n"
|
|
587
1078
|
);
|
|
588
1079
|
}
|
|
589
1080
|
if (!options.yes) {
|
|
@@ -603,7 +1094,7 @@ async function initCommand(options, cwd) {
|
|
|
603
1094
|
shouldInfer = await confirm("Infer boundary rules from import patterns?");
|
|
604
1095
|
}
|
|
605
1096
|
if (shouldInfer) {
|
|
606
|
-
console.log(
|
|
1097
|
+
console.log(chalk5.dim("Building import graph..."));
|
|
607
1098
|
const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
|
|
608
1099
|
const packages = resolveWorkspacePackages(projectRoot, config.workspace);
|
|
609
1100
|
const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
|
|
@@ -611,69 +1102,69 @@ async function initCommand(options, cwd) {
|
|
|
611
1102
|
if (inferred.length > 0) {
|
|
612
1103
|
config.boundaries = inferred;
|
|
613
1104
|
config.rules.enforceBoundaries = true;
|
|
614
|
-
console.log(` ${
|
|
1105
|
+
console.log(` ${chalk5.green("\u2713")} Inferred ${inferred.length} boundary rules`);
|
|
615
1106
|
}
|
|
616
1107
|
}
|
|
617
1108
|
}
|
|
618
|
-
|
|
1109
|
+
fs11.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
619
1110
|
`);
|
|
620
1111
|
writeGeneratedFiles(projectRoot, config, scanResult);
|
|
621
1112
|
updateGitignore(projectRoot);
|
|
622
1113
|
setupPreCommitHook(projectRoot);
|
|
623
1114
|
console.log(`
|
|
624
|
-
${
|
|
625
|
-
console.log(` ${
|
|
626
|
-
console.log(` ${
|
|
627
|
-
console.log(` ${
|
|
1115
|
+
${chalk5.bold("Created:")}`);
|
|
1116
|
+
console.log(` ${chalk5.green("\u2713")} ${CONFIG_FILE4}`);
|
|
1117
|
+
console.log(` ${chalk5.green("\u2713")} .viberails/context.md`);
|
|
1118
|
+
console.log(` ${chalk5.green("\u2713")} .viberails/scan-result.json`);
|
|
628
1119
|
console.log(`
|
|
629
|
-
${
|
|
630
|
-
console.log(` 1. Review ${
|
|
1120
|
+
${chalk5.bold("Next steps:")}`);
|
|
1121
|
+
console.log(` 1. Review ${chalk5.cyan("viberails.config.json")} and adjust rules`);
|
|
631
1122
|
console.log(
|
|
632
|
-
` 2. Commit ${
|
|
1123
|
+
` 2. Commit ${chalk5.cyan("viberails.config.json")} and ${chalk5.cyan(".viberails/context.md")}`
|
|
633
1124
|
);
|
|
634
|
-
console.log(` 3. Run ${
|
|
1125
|
+
console.log(` 3. Run ${chalk5.cyan("viberails check")} to verify your project passes`);
|
|
635
1126
|
}
|
|
636
1127
|
function updateGitignore(projectRoot) {
|
|
637
|
-
const gitignorePath =
|
|
1128
|
+
const gitignorePath = path12.join(projectRoot, ".gitignore");
|
|
638
1129
|
let content = "";
|
|
639
|
-
if (
|
|
640
|
-
content =
|
|
1130
|
+
if (fs11.existsSync(gitignorePath)) {
|
|
1131
|
+
content = fs11.readFileSync(gitignorePath, "utf-8");
|
|
641
1132
|
}
|
|
642
1133
|
if (!content.includes(".viberails/scan-result.json")) {
|
|
643
1134
|
const block = "\n# viberails\n.viberails/scan-result.json\n";
|
|
644
|
-
|
|
1135
|
+
fs11.writeFileSync(gitignorePath, `${content.trimEnd()}
|
|
645
1136
|
${block}`);
|
|
646
1137
|
}
|
|
647
1138
|
}
|
|
648
1139
|
function setupPreCommitHook(projectRoot) {
|
|
649
|
-
const lefthookPath =
|
|
650
|
-
if (
|
|
1140
|
+
const lefthookPath = path12.join(projectRoot, "lefthook.yml");
|
|
1141
|
+
if (fs11.existsSync(lefthookPath)) {
|
|
651
1142
|
addLefthookPreCommit(lefthookPath);
|
|
652
|
-
console.log(` ${
|
|
1143
|
+
console.log(` ${chalk5.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
|
|
653
1144
|
return;
|
|
654
1145
|
}
|
|
655
|
-
const huskyDir =
|
|
656
|
-
if (
|
|
1146
|
+
const huskyDir = path12.join(projectRoot, ".husky");
|
|
1147
|
+
if (fs11.existsSync(huskyDir)) {
|
|
657
1148
|
writeHuskyPreCommit(huskyDir);
|
|
658
|
-
console.log(` ${
|
|
1149
|
+
console.log(` ${chalk5.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
|
|
659
1150
|
return;
|
|
660
1151
|
}
|
|
661
|
-
const gitDir =
|
|
662
|
-
if (
|
|
663
|
-
const hooksDir =
|
|
664
|
-
if (!
|
|
665
|
-
|
|
1152
|
+
const gitDir = path12.join(projectRoot, ".git");
|
|
1153
|
+
if (fs11.existsSync(gitDir)) {
|
|
1154
|
+
const hooksDir = path12.join(gitDir, "hooks");
|
|
1155
|
+
if (!fs11.existsSync(hooksDir)) {
|
|
1156
|
+
fs11.mkdirSync(hooksDir, { recursive: true });
|
|
666
1157
|
}
|
|
667
1158
|
writeGitHookPreCommit(hooksDir);
|
|
668
|
-
console.log(` ${
|
|
1159
|
+
console.log(` ${chalk5.green("\u2713")} .git/hooks/pre-commit`);
|
|
669
1160
|
}
|
|
670
1161
|
}
|
|
671
1162
|
function writeGitHookPreCommit(hooksDir) {
|
|
672
|
-
const hookPath =
|
|
673
|
-
if (
|
|
674
|
-
const existing =
|
|
1163
|
+
const hookPath = path12.join(hooksDir, "pre-commit");
|
|
1164
|
+
if (fs11.existsSync(hookPath)) {
|
|
1165
|
+
const existing = fs11.readFileSync(hookPath, "utf-8");
|
|
675
1166
|
if (existing.includes("viberails")) return;
|
|
676
|
-
|
|
1167
|
+
fs11.writeFileSync(
|
|
677
1168
|
hookPath,
|
|
678
1169
|
`${existing.trimEnd()}
|
|
679
1170
|
|
|
@@ -690,37 +1181,37 @@ npx viberails check --staged
|
|
|
690
1181
|
"npx viberails check --staged",
|
|
691
1182
|
""
|
|
692
1183
|
].join("\n");
|
|
693
|
-
|
|
1184
|
+
fs11.writeFileSync(hookPath, script, { mode: 493 });
|
|
694
1185
|
}
|
|
695
1186
|
function addLefthookPreCommit(lefthookPath) {
|
|
696
|
-
const content =
|
|
1187
|
+
const content = fs11.readFileSync(lefthookPath, "utf-8");
|
|
697
1188
|
if (content.includes("viberails")) return;
|
|
698
1189
|
const addition = ["", " viberails:", " run: npx viberails check --staged"].join("\n");
|
|
699
|
-
|
|
1190
|
+
fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
|
|
700
1191
|
${addition}
|
|
701
1192
|
`);
|
|
702
1193
|
}
|
|
703
1194
|
function writeHuskyPreCommit(huskyDir) {
|
|
704
|
-
const hookPath =
|
|
705
|
-
if (
|
|
706
|
-
const existing =
|
|
1195
|
+
const hookPath = path12.join(huskyDir, "pre-commit");
|
|
1196
|
+
if (fs11.existsSync(hookPath)) {
|
|
1197
|
+
const existing = fs11.readFileSync(hookPath, "utf-8");
|
|
707
1198
|
if (!existing.includes("viberails")) {
|
|
708
|
-
|
|
1199
|
+
fs11.writeFileSync(hookPath, `${existing.trimEnd()}
|
|
709
1200
|
npx viberails check --staged
|
|
710
1201
|
`);
|
|
711
1202
|
}
|
|
712
1203
|
return;
|
|
713
1204
|
}
|
|
714
|
-
|
|
1205
|
+
fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
|
|
715
1206
|
}
|
|
716
1207
|
|
|
717
1208
|
// src/commands/sync.ts
|
|
718
|
-
import * as
|
|
719
|
-
import * as
|
|
720
|
-
import { loadConfig as
|
|
1209
|
+
import * as fs12 from "fs";
|
|
1210
|
+
import * as path13 from "path";
|
|
1211
|
+
import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
|
|
721
1212
|
import { scan as scan2 } from "@viberails/scanner";
|
|
722
|
-
import
|
|
723
|
-
var
|
|
1213
|
+
import chalk6 from "chalk";
|
|
1214
|
+
var CONFIG_FILE5 = "viberails.config.json";
|
|
724
1215
|
async function syncCommand(cwd) {
|
|
725
1216
|
const startDir = cwd ?? process.cwd();
|
|
726
1217
|
const projectRoot = findProjectRoot(startDir);
|
|
@@ -729,23 +1220,23 @@ async function syncCommand(cwd) {
|
|
|
729
1220
|
"No package.json found in this directory or any parent.\n\nMake sure you are inside a JavaScript or TypeScript project, then run:\n npx viberails"
|
|
730
1221
|
);
|
|
731
1222
|
}
|
|
732
|
-
const configPath =
|
|
733
|
-
const existing = await
|
|
734
|
-
console.log(
|
|
1223
|
+
const configPath = path13.join(projectRoot, CONFIG_FILE5);
|
|
1224
|
+
const existing = await loadConfig4(configPath);
|
|
1225
|
+
console.log(chalk6.dim("Scanning project..."));
|
|
735
1226
|
const scanResult = await scan2(projectRoot);
|
|
736
1227
|
const merged = mergeConfig(existing, scanResult);
|
|
737
|
-
|
|
1228
|
+
fs12.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
|
|
738
1229
|
`);
|
|
739
1230
|
writeGeneratedFiles(projectRoot, merged, scanResult);
|
|
740
1231
|
console.log(`
|
|
741
|
-
${
|
|
742
|
-
console.log(` ${
|
|
743
|
-
console.log(` ${
|
|
744
|
-
console.log(` ${
|
|
1232
|
+
${chalk6.bold("Synced:")}`);
|
|
1233
|
+
console.log(` ${chalk6.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
|
|
1234
|
+
console.log(` ${chalk6.green("\u2713")} .viberails/context.md \u2014 regenerated`);
|
|
1235
|
+
console.log(` ${chalk6.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
|
|
745
1236
|
}
|
|
746
1237
|
|
|
747
1238
|
// src/index.ts
|
|
748
|
-
var VERSION = "0.
|
|
1239
|
+
var VERSION = "0.2.0";
|
|
749
1240
|
var program = new Command();
|
|
750
1241
|
program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
|
|
751
1242
|
program.command("init", { isDefault: true }).description("Scan your project and set up enforcement guardrails").option("-y, --yes", "Non-interactive mode (use defaults, high-confidence only)").action(async (options) => {
|
|
@@ -753,7 +1244,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
|
|
|
753
1244
|
await initCommand(options);
|
|
754
1245
|
} catch (err) {
|
|
755
1246
|
const message = err instanceof Error ? err.message : String(err);
|
|
756
|
-
console.error(`${
|
|
1247
|
+
console.error(`${chalk7.red("Error:")} ${message}`);
|
|
757
1248
|
process.exit(1);
|
|
758
1249
|
}
|
|
759
1250
|
});
|
|
@@ -762,7 +1253,7 @@ program.command("sync").description("Re-scan and update generated files").action
|
|
|
762
1253
|
await syncCommand();
|
|
763
1254
|
} catch (err) {
|
|
764
1255
|
const message = err instanceof Error ? err.message : String(err);
|
|
765
|
-
console.error(`${
|
|
1256
|
+
console.error(`${chalk7.red("Error:")} ${message}`);
|
|
766
1257
|
process.exit(1);
|
|
767
1258
|
}
|
|
768
1259
|
});
|
|
@@ -775,7 +1266,17 @@ program.command("check").description("Check files against enforced rules").optio
|
|
|
775
1266
|
process.exit(exitCode);
|
|
776
1267
|
} catch (err) {
|
|
777
1268
|
const message = err instanceof Error ? err.message : String(err);
|
|
778
|
-
console.error(`${
|
|
1269
|
+
console.error(`${chalk7.red("Error:")} ${message}`);
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
program.command("fix").description("Auto-fix file naming violations and generate missing test stubs").option("--dry-run", "Show planned fixes without applying them").option("--rule <rules...>", "Fix only specific rules (file-naming, missing-test)").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
|
|
1274
|
+
try {
|
|
1275
|
+
const exitCode = await fixCommand(options);
|
|
1276
|
+
process.exit(exitCode);
|
|
1277
|
+
} catch (err) {
|
|
1278
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1279
|
+
console.error(`${chalk7.red("Error:")} ${message}`);
|
|
779
1280
|
process.exit(1);
|
|
780
1281
|
}
|
|
781
1282
|
});
|
|
@@ -784,7 +1285,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
|
|
|
784
1285
|
await boundariesCommand(options);
|
|
785
1286
|
} catch (err) {
|
|
786
1287
|
const message = err instanceof Error ? err.message : String(err);
|
|
787
|
-
console.error(`${
|
|
1288
|
+
console.error(`${chalk7.red("Error:")} ${message}`);
|
|
788
1289
|
process.exit(1);
|
|
789
1290
|
}
|
|
790
1291
|
});
|