viberails 0.1.0 → 0.2.1

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