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 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_chalk7 = __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,378 @@ ${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++;
354
- }
355
- return count;
356
- } catch {
357
- return null;
358
- }
533
+
534
+ // src/commands/fix.ts
535
+ var import_node_child_process2 = require("child_process");
536
+ var fs9 = __toESM(require("fs"), 1);
537
+ var path10 = __toESM(require("path"), 1);
538
+ var import_node_readline = require("readline");
539
+ var import_config3 = require("@viberails/config");
540
+ var import_chalk3 = __toESM(require("chalk"), 1);
541
+
542
+ // src/commands/fix-imports.ts
543
+ var path7 = __toESM(require("path"), 1);
544
+ function stripExtension(filePath) {
545
+ return filePath.replace(/\.(tsx?|jsx?|mjs|cjs)$/, "");
359
546
  }
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.`;
547
+ function computeNewSpecifier(oldSpecifier, newBare) {
548
+ const hasJsExt = oldSpecifier.endsWith(".js");
549
+ const base = hasJsExt ? oldSpecifier.slice(0, -3) : oldSpecifier;
550
+ const dir = base.lastIndexOf("/");
551
+ const prefix = dir >= 0 ? base.slice(0, dir + 1) : "";
552
+ const newSpec = prefix + newBare;
553
+ return hasJsExt ? `${newSpec}.js` : newSpec;
373
554
  }
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;
555
+ async function updateImportsAfterRenames(renames, projectRoot) {
556
+ if (renames.length === 0) return [];
557
+ const { Project, SyntaxKind } = await import("ts-morph");
558
+ const renameMap = /* @__PURE__ */ new Map();
559
+ for (const r of renames) {
560
+ const oldStripped = stripExtension(r.oldAbsPath);
561
+ const newFilename = path7.basename(r.newPath);
562
+ const newName = newFilename.slice(0, newFilename.indexOf("."));
563
+ renameMap.set(oldStripped, { newBare: newName });
564
+ }
565
+ const project = new Project({
566
+ tsConfigFilePath: void 0,
567
+ skipAddingFilesFromTsConfig: true
568
+ });
569
+ project.addSourceFilesAtPaths(path7.join(projectRoot, "**/*.{ts,tsx,js,jsx,mjs,cjs}"));
570
+ const updates = [];
571
+ const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
572
+ for (const sourceFile of project.getSourceFiles()) {
573
+ const filePath = sourceFile.getFilePath();
574
+ if (filePath.includes("/node_modules/") || filePath.includes("/dist/")) continue;
575
+ const fileDir = path7.dirname(filePath);
576
+ for (const decl of sourceFile.getImportDeclarations()) {
577
+ const specifier = decl.getModuleSpecifierValue();
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: decl.getStartLineNumber()
587
+ });
588
+ decl.setModuleSpecifier(newSpec);
388
589
  }
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
590
+ for (const decl of sourceFile.getExportDeclarations()) {
591
+ const specifier = decl.getModuleSpecifierValue();
592
+ if (!specifier || !specifier.startsWith(".")) continue;
593
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
594
+ if (!match) continue;
595
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
596
+ updates.push({
597
+ file: filePath,
598
+ oldSpecifier: specifier,
599
+ newSpecifier: newSpec,
600
+ line: decl.getStartLineNumber()
404
601
  });
602
+ decl.setModuleSpecifier(newSpec);
603
+ }
604
+ for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
605
+ if (call.getExpression().getKind() !== SyntaxKind.ImportKeyword) continue;
606
+ const args = call.getArguments();
607
+ if (args.length === 0) continue;
608
+ const arg = args[0];
609
+ if (arg.getKind() !== SyntaxKind.StringLiteral) continue;
610
+ const specifier = arg.getText().slice(1, -1);
611
+ if (!specifier.startsWith(".")) continue;
612
+ const match = resolveToRenamedFile(specifier, fileDir, renameMap, extensions);
613
+ if (!match) continue;
614
+ const newSpec = computeNewSpecifier(specifier, match.newBare);
615
+ updates.push({
616
+ file: filePath,
617
+ oldSpecifier: specifier,
618
+ newSpecifier: newSpec,
619
+ line: call.getStartLineNumber()
620
+ });
621
+ const quote = arg.getText()[0];
622
+ arg.replaceWithText(`${quote}${newSpec}${quote}`);
405
623
  }
406
624
  }
407
- return violations;
625
+ if (updates.length > 0) {
626
+ await project.save();
627
+ }
628
+ return updates;
408
629
  }
409
- function getStagedFiles(projectRoot) {
410
- try {
411
- const output = (0, import_node_child_process.execSync)("git diff --cached --name-only --diff-filter=ACM", {
412
- cwd: projectRoot,
413
- encoding: "utf-8"
414
- });
415
- return output.trim().split("\n").filter(Boolean);
416
- } catch {
417
- return [];
630
+ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
631
+ const cleanSpec = specifier.endsWith(".js") ? specifier.slice(0, -3) : specifier;
632
+ const resolved = path7.resolve(fromDir, cleanSpec);
633
+ for (const ext of extensions) {
634
+ const candidate = resolved + ext;
635
+ const stripped = stripExtension(candidate);
636
+ const match = renameMap.get(stripped);
637
+ if (match) return match;
418
638
  }
639
+ return void 0;
419
640
  }
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;
428
- }
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);
641
+
642
+ // src/commands/fix-naming.ts
643
+ var fs7 = __toESM(require("fs"), 1);
644
+ var path8 = __toESM(require("path"), 1);
645
+
646
+ // src/commands/convert-name.ts
647
+ function splitIntoWords(name) {
648
+ const parts = name.split(/[-_]/);
649
+ const words = [];
650
+ for (const part of parts) {
651
+ if (part === "") continue;
652
+ let current = "";
653
+ for (let i = 0; i < part.length; i++) {
654
+ const ch = part[i];
655
+ const isUpper = ch >= "A" && ch <= "Z";
656
+ if (isUpper && current.length > 0) {
657
+ const prevIsUpper = current[current.length - 1] >= "A" && current[current.length - 1] <= "Z";
658
+ const nextIsLower = i + 1 < part.length && part[i + 1] >= "a" && part[i + 1] <= "z";
659
+ if (!prevIsUpper || nextIsLower) {
660
+ words.push(current.toLowerCase());
661
+ current = "";
441
662
  }
442
663
  }
664
+ current += ch;
443
665
  }
666
+ if (current) words.push(current.toLowerCase());
667
+ }
668
+ return words;
669
+ }
670
+ function convertName(bare, target) {
671
+ const words = splitIntoWords(bare);
672
+ if (words.length === 0) return bare;
673
+ switch (target) {
674
+ case "kebab-case":
675
+ return words.join("-");
676
+ case "camelCase":
677
+ return words[0] + words.slice(1).map(capitalize).join("");
678
+ case "PascalCase":
679
+ return words.map(capitalize).join("");
680
+ case "snake_case":
681
+ return words.join("_");
682
+ default:
683
+ return bare;
684
+ }
685
+ }
686
+ function capitalize(word) {
687
+ if (word.length === 0) return word;
688
+ return word[0].toUpperCase() + word.slice(1);
689
+ }
690
+
691
+ // src/commands/fix-naming.ts
692
+ function computeRename(relPath, targetConvention, projectRoot) {
693
+ const filename = path8.basename(relPath);
694
+ const dir = path8.dirname(relPath);
695
+ const dotIndex = filename.indexOf(".");
696
+ if (dotIndex === -1) return null;
697
+ const bare = filename.slice(0, dotIndex);
698
+ const suffix = filename.slice(dotIndex);
699
+ const newBare = convertName(bare, targetConvention);
700
+ if (newBare === bare) return null;
701
+ const newFilename = newBare + suffix;
702
+ const newRelPath = path8.join(dir, newFilename);
703
+ const oldAbsPath = path8.join(projectRoot, relPath);
704
+ const newAbsPath = path8.join(projectRoot, newRelPath);
705
+ if (fs7.existsSync(newAbsPath)) return null;
706
+ return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
707
+ }
708
+ function executeRename(rename) {
709
+ if (fs7.existsSync(rename.newAbsPath)) return false;
710
+ fs7.renameSync(rename.oldAbsPath, rename.newAbsPath);
711
+ return true;
712
+ }
713
+ function deduplicateRenames(renames) {
714
+ const seen = /* @__PURE__ */ new Set();
715
+ const result = [];
716
+ for (const r of renames) {
717
+ if (seen.has(r.newAbsPath)) continue;
718
+ seen.add(r.newAbsPath);
719
+ result.push(r);
720
+ }
721
+ return result;
722
+ }
723
+
724
+ // src/commands/fix-tests.ts
725
+ var fs8 = __toESM(require("fs"), 1);
726
+ var path9 = __toESM(require("path"), 1);
727
+ function generateTestStub(sourceRelPath, config, projectRoot) {
728
+ const { testPattern } = config.structure;
729
+ if (!testPattern) return null;
730
+ const basename6 = path9.basename(sourceRelPath);
731
+ const stem = basename6.slice(0, basename6.indexOf("."));
732
+ const testSuffix = testPattern.replace("*", "");
733
+ const testFilename = `${stem}${testSuffix}`;
734
+ const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
735
+ const testAbsPath = path9.join(dir, testFilename);
736
+ if (fs8.existsSync(testAbsPath)) return null;
737
+ return {
738
+ path: path9.relative(projectRoot, testAbsPath),
739
+ absPath: testAbsPath,
740
+ moduleName: stem
444
741
  };
445
- walk(projectRoot);
446
- return files;
447
742
  }
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;
743
+ function writeTestStub(stub, config) {
744
+ const runner = config.stack.testRunner === "jest" ? "jest" : "vitest";
745
+ const importLine = runner === "jest" ? "" : "import { describe, it, expect } from 'vitest';\n\n";
746
+ const content = `${importLine}describe('${stub.moduleName}', () => {
747
+ it.todo('add tests');
748
+ });
749
+ `;
750
+ fs8.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
751
+ fs8.writeFileSync(stub.absPath, content);
752
+ }
753
+
754
+ // src/commands/fix.ts
755
+ var CONFIG_FILE3 = "viberails.config.json";
756
+ async function fixCommand(options, cwd) {
757
+ const startDir = cwd ?? process.cwd();
758
+ const projectRoot = findProjectRoot(startDir);
759
+ if (!projectRoot) {
760
+ console.error(`${import_chalk3.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
761
+ return 1;
762
+ }
763
+ const configPath = path10.join(projectRoot, CONFIG_FILE3);
764
+ if (!fs9.existsSync(configPath)) {
765
+ console.error(
766
+ `${import_chalk3.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
767
+ );
768
+ return 1;
769
+ }
770
+ const config = await (0, import_config3.loadConfig)(configPath);
771
+ if (!options.yes && !options.dryRun) {
772
+ const isDirty = checkGitDirty(projectRoot);
773
+ if (isDirty) {
774
+ console.log(
775
+ import_chalk3.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
776
+ );
456
777
  }
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
- }
778
+ }
779
+ const shouldFixNaming = !options.rule || options.rule.includes("file-naming");
780
+ const shouldFixTests = !options.rule || options.rule.includes("missing-test");
781
+ const allFiles = getAllSourceFiles(projectRoot, config);
782
+ const renames = [];
783
+ if (shouldFixNaming) {
784
+ for (const file of allFiles) {
785
+ const resolved = resolveConfigForFile(file, config);
786
+ if (!resolved.rules.enforceNaming || !resolved.conventions.fileNaming) continue;
787
+ const violation = checkNaming(file, resolved.conventions);
788
+ if (!violation) continue;
789
+ const convention = getConventionValue(resolved.conventions.fileNaming);
790
+ if (!convention) continue;
791
+ const rename = computeRename(file, convention, projectRoot);
792
+ if (rename) renames.push(rename);
464
793
  }
465
- };
466
- walk(dir);
467
- return files;
794
+ }
795
+ const dedupedRenames = deduplicateRenames(renames);
796
+ const testStubs = [];
797
+ if (shouldFixTests && config.rules.requireTests) {
798
+ const testViolations = checkMissingTests(projectRoot, config, "warn");
799
+ for (const v of testViolations) {
800
+ const stub = generateTestStub(v.file, config, projectRoot);
801
+ if (stub) testStubs.push(stub);
802
+ }
803
+ }
804
+ if (dedupedRenames.length === 0 && testStubs.length === 0) {
805
+ console.log(`${import_chalk3.default.green("\u2713")} No fixable violations found.`);
806
+ return 0;
807
+ }
808
+ printPlan(dedupedRenames, testStubs);
809
+ if (options.dryRun) {
810
+ console.log(import_chalk3.default.dim("\nDry run \u2014 no changes applied."));
811
+ return 0;
812
+ }
813
+ if (!options.yes) {
814
+ const confirmed = await promptConfirm("Apply these fixes?");
815
+ if (!confirmed) {
816
+ console.log("Aborted.");
817
+ return 0;
818
+ }
819
+ }
820
+ let renameCount = 0;
821
+ for (const rename of dedupedRenames) {
822
+ if (executeRename(rename)) {
823
+ renameCount++;
824
+ }
825
+ }
826
+ let importUpdateCount = 0;
827
+ if (renameCount > 0) {
828
+ const appliedRenames = dedupedRenames.filter((r) => fs9.existsSync(r.newAbsPath));
829
+ const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
830
+ importUpdateCount = updates.length;
831
+ }
832
+ let stubCount = 0;
833
+ for (const stub of testStubs) {
834
+ if (!fs9.existsSync(stub.absPath)) {
835
+ writeTestStub(stub, config);
836
+ stubCount++;
837
+ }
838
+ }
839
+ console.log("");
840
+ if (renameCount > 0) {
841
+ console.log(`${import_chalk3.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
842
+ }
843
+ if (importUpdateCount > 0) {
844
+ console.log(
845
+ `${import_chalk3.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
846
+ );
847
+ }
848
+ if (stubCount > 0) {
849
+ console.log(`${import_chalk3.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
850
+ }
851
+ return 0;
468
852
  }
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;
853
+ function printPlan(renames, stubs) {
854
+ if (renames.length > 0) {
855
+ console.log(import_chalk3.default.bold("\nFile renames:"));
856
+ for (const r of renames) {
857
+ console.log(` ${import_chalk3.default.red(r.oldPath)} \u2192 ${import_chalk3.default.green(r.newPath)}`);
479
858
  }
480
859
  }
481
- return false;
860
+ if (stubs.length > 0) {
861
+ console.log(import_chalk3.default.bold("\nTest stubs to create:"));
862
+ for (const s of stubs) {
863
+ console.log(` ${import_chalk3.default.green("+")} ${s.path}`);
864
+ }
865
+ }
866
+ }
867
+ function checkGitDirty(projectRoot) {
868
+ try {
869
+ const output = (0, import_node_child_process2.execSync)("git status --porcelain", {
870
+ cwd: projectRoot,
871
+ encoding: "utf-8"
872
+ });
873
+ return output.trim().length > 0;
874
+ } catch {
875
+ return false;
876
+ }
877
+ }
878
+ function getConventionValue(convention) {
879
+ if (typeof convention === "string") return convention;
880
+ if (convention && typeof convention === "object" && "value" in convention) {
881
+ return convention.value;
882
+ }
883
+ return void 0;
884
+ }
885
+ function promptConfirm(question) {
886
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
887
+ return new Promise((resolve4) => {
888
+ rl.question(`${question} (y/N) `, (answer) => {
889
+ rl.close();
890
+ resolve4(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
891
+ });
892
+ });
482
893
  }
483
894
 
484
895
  // 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");
896
+ var fs11 = __toESM(require("fs"), 1);
897
+ var path12 = __toESM(require("path"), 1);
898
+ var import_config4 = require("@viberails/config");
488
899
  var import_scanner = require("@viberails/scanner");
489
- var import_chalk4 = __toESM(require("chalk"), 1);
900
+ var import_chalk5 = __toESM(require("chalk"), 1);
490
901
 
491
902
  // src/display.ts
492
903
  var import_types = require("@viberails/types");
493
- var import_chalk3 = __toESM(require("chalk"), 1);
904
+ var import_chalk4 = __toESM(require("chalk"), 1);
494
905
  var CONVENTION_LABELS = {
495
906
  fileNaming: "File naming",
496
907
  componentNaming: "Component naming",
@@ -508,53 +919,128 @@ function confidenceLabel(convention) {
508
919
  }
509
920
  return `${pct}% \u2014 medium confidence, suggested only`;
510
921
  }
922
+ function formatPackageSummary(pkg) {
923
+ const parts = [];
924
+ if (pkg.stack.framework) {
925
+ parts.push(formatItem(pkg.stack.framework, import_types.FRAMEWORK_NAMES));
926
+ }
927
+ if (pkg.stack.styling) {
928
+ parts.push(formatItem(pkg.stack.styling, import_types.STYLING_NAMES));
929
+ }
930
+ const files = `${pkg.statistics.totalFiles} files`;
931
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
932
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
933
+ }
934
+ function displayMonorepoResults(scanResult) {
935
+ const { stack, packages } = scanResult;
936
+ console.log(`
937
+ ${import_chalk4.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
938
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.language)}`);
939
+ if (stack.packageManager) {
940
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
941
+ }
942
+ if (stack.linter) {
943
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)}`);
944
+ }
945
+ if (stack.testRunner) {
946
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
947
+ }
948
+ console.log("");
949
+ for (const pkg of packages) {
950
+ console.log(formatPackageSummary(pkg));
951
+ }
952
+ const packagesWithDirs = packages.filter(
953
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
954
+ );
955
+ if (packagesWithDirs.length > 0) {
956
+ console.log(`
957
+ ${import_chalk4.default.bold("Structure:")}`);
958
+ for (const pkg of packagesWithDirs) {
959
+ const meaningfulDirs = pkg.structure.directories.filter((d) => d.role !== "unknown");
960
+ if (meaningfulDirs.length === 0) continue;
961
+ console.log(` ${pkg.relativePath}:`);
962
+ for (const dir of meaningfulDirs) {
963
+ const label = import_types.ROLE_DESCRIPTIONS[dir.role] ?? dir.role;
964
+ const files = dir.fileCount === 1 ? "1 file" : `${dir.fileCount} files`;
965
+ console.log(` ${import_chalk4.default.green("\u2713")} ${dir.path} \u2014 ${label} (${files})`);
966
+ }
967
+ }
968
+ }
969
+ const conventionEntries = Object.entries(scanResult.conventions);
970
+ if (conventionEntries.length > 0) {
971
+ console.log(`
972
+ ${import_chalk4.default.bold("Conventions:")}`);
973
+ for (const [key, convention] of conventionEntries) {
974
+ if (convention.confidence === "low") continue;
975
+ const label = CONVENTION_LABELS[key] ?? key;
976
+ const pkgValues = packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
977
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
978
+ if (allSame || pkgValues.length <= 1) {
979
+ const ind = convention.confidence === "high" ? import_chalk4.default.green("\u2713") : import_chalk4.default.yellow("~");
980
+ const detail = import_chalk4.default.dim(`(${confidenceLabel(convention)})`);
981
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
982
+ } else {
983
+ console.log(` ${import_chalk4.default.yellow("~")} ${label}: varies by package`);
984
+ for (const pv of pkgValues) {
985
+ const pct = Math.round(pv.convention.consistency);
986
+ console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
987
+ }
988
+ }
989
+ }
990
+ }
991
+ console.log("");
992
+ }
511
993
  function displayScanResults(scanResult) {
994
+ if (scanResult.packages.length > 1) {
995
+ displayMonorepoResults(scanResult);
996
+ return;
997
+ }
512
998
  const { stack, conventions } = scanResult;
513
999
  console.log(`
514
- ${import_chalk3.default.bold("Detected:")}`);
1000
+ ${import_chalk4.default.bold("Detected:")}`);
515
1001
  if (stack.framework) {
516
- console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.framework, import_types.FRAMEWORK_NAMES)}`);
1002
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.framework, import_types.FRAMEWORK_NAMES)}`);
517
1003
  }
518
- console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.language)}`);
1004
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.language)}`);
519
1005
  if (stack.styling) {
520
- console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.styling, import_types.STYLING_NAMES)}`);
1006
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.styling, import_types.STYLING_NAMES)}`);
521
1007
  }
522
1008
  if (stack.backend) {
523
- console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.backend, import_types.FRAMEWORK_NAMES)}`);
1009
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.backend, import_types.FRAMEWORK_NAMES)}`);
524
1010
  }
525
1011
  if (stack.linter) {
526
- console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.linter)}`);
1012
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.linter)}`);
527
1013
  }
528
1014
  if (stack.testRunner) {
529
- console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1015
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
530
1016
  }
531
1017
  if (stack.packageManager) {
532
- console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1018
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
533
1019
  }
534
1020
  if (stack.libraries.length > 0) {
535
1021
  for (const lib of stack.libraries) {
536
- console.log(` ${import_chalk3.default.green("\u2713")} ${formatItem(lib, import_types.LIBRARY_NAMES)}`);
1022
+ console.log(` ${import_chalk4.default.green("\u2713")} ${formatItem(lib, import_types.LIBRARY_NAMES)}`);
537
1023
  }
538
1024
  }
539
1025
  const meaningfulDirs = scanResult.structure.directories.filter((d) => d.role !== "unknown");
540
1026
  if (meaningfulDirs.length > 0) {
541
1027
  console.log(`
542
- ${import_chalk3.default.bold("Structure:")}`);
1028
+ ${import_chalk4.default.bold("Structure:")}`);
543
1029
  for (const dir of meaningfulDirs) {
544
1030
  const label = import_types.ROLE_DESCRIPTIONS[dir.role] ?? dir.role;
545
1031
  const files = dir.fileCount === 1 ? "1 file" : `${dir.fileCount} files`;
546
- console.log(` ${import_chalk3.default.green("\u2713")} ${dir.path} \u2014 ${label} (${files})`);
1032
+ console.log(` ${import_chalk4.default.green("\u2713")} ${dir.path} \u2014 ${label} (${files})`);
547
1033
  }
548
1034
  }
549
1035
  const conventionEntries = Object.entries(conventions);
550
1036
  if (conventionEntries.length > 0) {
551
1037
  console.log(`
552
- ${import_chalk3.default.bold("Conventions:")}`);
1038
+ ${import_chalk4.default.bold("Conventions:")}`);
553
1039
  for (const [key, convention] of conventionEntries) {
554
1040
  if (convention.confidence === "low") continue;
555
1041
  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)})`);
1042
+ const ind = convention.confidence === "high" ? import_chalk4.default.green("\u2713") : import_chalk4.default.yellow("~");
1043
+ const detail = import_chalk4.default.dim(`(${confidenceLabel(convention)})`);
558
1044
  console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
559
1045
  }
560
1046
  }
@@ -562,28 +1048,33 @@ ${import_chalk3.default.bold("Conventions:")}`);
562
1048
  }
563
1049
 
564
1050
  // src/utils/write-generated-files.ts
565
- var fs5 = __toESM(require("fs"), 1);
566
- var path5 = __toESM(require("path"), 1);
1051
+ var fs10 = __toESM(require("fs"), 1);
1052
+ var path11 = __toESM(require("path"), 1);
567
1053
  var import_context = require("@viberails/context");
568
1054
  var CONTEXT_DIR = ".viberails";
569
1055
  var CONTEXT_FILE = "context.md";
570
1056
  var SCAN_RESULT_FILE = "scan-result.json";
571
1057
  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)}
1058
+ const contextDir = path11.join(projectRoot, CONTEXT_DIR);
1059
+ try {
1060
+ if (!fs10.existsSync(contextDir)) {
1061
+ fs10.mkdirSync(contextDir, { recursive: true });
1062
+ }
1063
+ const context = (0, import_context.generateContext)(config);
1064
+ fs10.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
1065
+ fs10.writeFileSync(
1066
+ path11.join(contextDir, SCAN_RESULT_FILE),
1067
+ `${JSON.stringify(scanResult, null, 2)}
581
1068
  `
582
- );
1069
+ );
1070
+ } catch (err) {
1071
+ const message = err instanceof Error ? err.message : String(err);
1072
+ throw new Error(`Failed to write generated files to ${contextDir}: ${message}`);
1073
+ }
583
1074
  }
584
1075
 
585
1076
  // src/commands/init.ts
586
- var CONFIG_FILE3 = "viberails.config.json";
1077
+ var CONFIG_FILE4 = "viberails.config.json";
587
1078
  function filterHighConfidence(conventions) {
588
1079
  const filtered = {};
589
1080
  for (const [key, value] of Object.entries(conventions)) {
@@ -604,19 +1095,19 @@ async function initCommand(options, cwd) {
604
1095
  "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
1096
  );
606
1097
  }
607
- const configPath = path6.join(projectRoot, CONFIG_FILE3);
608
- if (fs6.existsSync(configPath)) {
1098
+ const configPath = path12.join(projectRoot, CONFIG_FILE4);
1099
+ if (fs11.existsSync(configPath)) {
609
1100
  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."
1101
+ import_chalk5.default.yellow("!") + " viberails is already initialized in this project.\n Run " + import_chalk5.default.cyan("viberails sync") + " to update the generated files."
611
1102
  );
612
1103
  return;
613
1104
  }
614
- console.log(import_chalk4.default.dim("Scanning project..."));
1105
+ console.log(import_chalk5.default.dim("Scanning project..."));
615
1106
  const scanResult = await (0, import_scanner.scan)(projectRoot);
616
1107
  displayScanResults(scanResult);
617
1108
  if (scanResult.statistics.totalFiles === 0) {
618
1109
  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"
1110
+ import_chalk5.default.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + import_chalk5.default.cyan("viberails sync") + " after adding source files.\n"
620
1111
  );
621
1112
  }
622
1113
  if (!options.yes) {
@@ -626,7 +1117,7 @@ async function initCommand(options, cwd) {
626
1117
  return;
627
1118
  }
628
1119
  }
629
- const config = (0, import_config3.generateConfig)(scanResult);
1120
+ const config = (0, import_config4.generateConfig)(scanResult);
630
1121
  if (options.yes) {
631
1122
  config.conventions = filterHighConfidence(config.conventions);
632
1123
  }
@@ -636,7 +1127,7 @@ async function initCommand(options, cwd) {
636
1127
  shouldInfer = await confirm("Infer boundary rules from import patterns?");
637
1128
  }
638
1129
  if (shouldInfer) {
639
- console.log(import_chalk4.default.dim("Building import graph..."));
1130
+ console.log(import_chalk5.default.dim("Building import graph..."));
640
1131
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
641
1132
  const packages = resolveWorkspacePackages(projectRoot, config.workspace);
642
1133
  const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
@@ -644,69 +1135,69 @@ async function initCommand(options, cwd) {
644
1135
  if (inferred.length > 0) {
645
1136
  config.boundaries = inferred;
646
1137
  config.rules.enforceBoundaries = true;
647
- console.log(` ${import_chalk4.default.green("\u2713")} Inferred ${inferred.length} boundary rules`);
1138
+ console.log(` ${import_chalk5.default.green("\u2713")} Inferred ${inferred.length} boundary rules`);
648
1139
  }
649
1140
  }
650
1141
  }
651
- fs6.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1142
+ fs11.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
652
1143
  `);
653
1144
  writeGeneratedFiles(projectRoot, config, scanResult);
654
1145
  updateGitignore(projectRoot);
655
1146
  setupPreCommitHook(projectRoot);
656
1147
  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`);
1148
+ ${import_chalk5.default.bold("Created:")}`);
1149
+ console.log(` ${import_chalk5.default.green("\u2713")} ${CONFIG_FILE4}`);
1150
+ console.log(` ${import_chalk5.default.green("\u2713")} .viberails/context.md`);
1151
+ console.log(` ${import_chalk5.default.green("\u2713")} .viberails/scan-result.json`);
661
1152
  console.log(`
662
- ${import_chalk4.default.bold("Next steps:")}`);
663
- console.log(` 1. Review ${import_chalk4.default.cyan("viberails.config.json")} and adjust rules`);
1153
+ ${import_chalk5.default.bold("Next steps:")}`);
1154
+ console.log(` 1. Review ${import_chalk5.default.cyan("viberails.config.json")} and adjust rules`);
664
1155
  console.log(
665
- ` 2. Commit ${import_chalk4.default.cyan("viberails.config.json")} and ${import_chalk4.default.cyan(".viberails/context.md")}`
1156
+ ` 2. Commit ${import_chalk5.default.cyan("viberails.config.json")} and ${import_chalk5.default.cyan(".viberails/context.md")}`
666
1157
  );
667
- console.log(` 3. Run ${import_chalk4.default.cyan("viberails check")} to verify your project passes`);
1158
+ console.log(` 3. Run ${import_chalk5.default.cyan("viberails check")} to verify your project passes`);
668
1159
  }
669
1160
  function updateGitignore(projectRoot) {
670
- const gitignorePath = path6.join(projectRoot, ".gitignore");
1161
+ const gitignorePath = path12.join(projectRoot, ".gitignore");
671
1162
  let content = "";
672
- if (fs6.existsSync(gitignorePath)) {
673
- content = fs6.readFileSync(gitignorePath, "utf-8");
1163
+ if (fs11.existsSync(gitignorePath)) {
1164
+ content = fs11.readFileSync(gitignorePath, "utf-8");
674
1165
  }
675
1166
  if (!content.includes(".viberails/scan-result.json")) {
676
1167
  const block = "\n# viberails\n.viberails/scan-result.json\n";
677
- fs6.writeFileSync(gitignorePath, `${content.trimEnd()}
1168
+ fs11.writeFileSync(gitignorePath, `${content.trimEnd()}
678
1169
  ${block}`);
679
1170
  }
680
1171
  }
681
1172
  function setupPreCommitHook(projectRoot) {
682
- const lefthookPath = path6.join(projectRoot, "lefthook.yml");
683
- if (fs6.existsSync(lefthookPath)) {
1173
+ const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1174
+ if (fs11.existsSync(lefthookPath)) {
684
1175
  addLefthookPreCommit(lefthookPath);
685
- console.log(` ${import_chalk4.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1176
+ console.log(` ${import_chalk5.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
686
1177
  return;
687
1178
  }
688
- const huskyDir = path6.join(projectRoot, ".husky");
689
- if (fs6.existsSync(huskyDir)) {
1179
+ const huskyDir = path12.join(projectRoot, ".husky");
1180
+ if (fs11.existsSync(huskyDir)) {
690
1181
  writeHuskyPreCommit(huskyDir);
691
- console.log(` ${import_chalk4.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1182
+ console.log(` ${import_chalk5.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
692
1183
  return;
693
1184
  }
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 });
1185
+ const gitDir = path12.join(projectRoot, ".git");
1186
+ if (fs11.existsSync(gitDir)) {
1187
+ const hooksDir = path12.join(gitDir, "hooks");
1188
+ if (!fs11.existsSync(hooksDir)) {
1189
+ fs11.mkdirSync(hooksDir, { recursive: true });
699
1190
  }
700
1191
  writeGitHookPreCommit(hooksDir);
701
- console.log(` ${import_chalk4.default.green("\u2713")} .git/hooks/pre-commit`);
1192
+ console.log(` ${import_chalk5.default.green("\u2713")} .git/hooks/pre-commit`);
702
1193
  }
703
1194
  }
704
1195
  function writeGitHookPreCommit(hooksDir) {
705
- const hookPath = path6.join(hooksDir, "pre-commit");
706
- if (fs6.existsSync(hookPath)) {
707
- const existing = fs6.readFileSync(hookPath, "utf-8");
1196
+ const hookPath = path12.join(hooksDir, "pre-commit");
1197
+ if (fs11.existsSync(hookPath)) {
1198
+ const existing = fs11.readFileSync(hookPath, "utf-8");
708
1199
  if (existing.includes("viberails")) return;
709
- fs6.writeFileSync(
1200
+ fs11.writeFileSync(
710
1201
  hookPath,
711
1202
  `${existing.trimEnd()}
712
1203
 
@@ -723,37 +1214,37 @@ npx viberails check --staged
723
1214
  "npx viberails check --staged",
724
1215
  ""
725
1216
  ].join("\n");
726
- fs6.writeFileSync(hookPath, script, { mode: 493 });
1217
+ fs11.writeFileSync(hookPath, script, { mode: 493 });
727
1218
  }
728
1219
  function addLefthookPreCommit(lefthookPath) {
729
- const content = fs6.readFileSync(lefthookPath, "utf-8");
1220
+ const content = fs11.readFileSync(lefthookPath, "utf-8");
730
1221
  if (content.includes("viberails")) return;
731
1222
  const addition = ["", " viberails:", " run: npx viberails check --staged"].join("\n");
732
- fs6.writeFileSync(lefthookPath, `${content.trimEnd()}
1223
+ fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
733
1224
  ${addition}
734
1225
  `);
735
1226
  }
736
1227
  function writeHuskyPreCommit(huskyDir) {
737
- const hookPath = path6.join(huskyDir, "pre-commit");
738
- if (fs6.existsSync(hookPath)) {
739
- const existing = fs6.readFileSync(hookPath, "utf-8");
1228
+ const hookPath = path12.join(huskyDir, "pre-commit");
1229
+ if (fs11.existsSync(hookPath)) {
1230
+ const existing = fs11.readFileSync(hookPath, "utf-8");
740
1231
  if (!existing.includes("viberails")) {
741
- fs6.writeFileSync(hookPath, `${existing.trimEnd()}
1232
+ fs11.writeFileSync(hookPath, `${existing.trimEnd()}
742
1233
  npx viberails check --staged
743
1234
  `);
744
1235
  }
745
1236
  return;
746
1237
  }
747
- fs6.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1238
+ fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
748
1239
  }
749
1240
 
750
1241
  // 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");
1242
+ var fs12 = __toESM(require("fs"), 1);
1243
+ var path13 = __toESM(require("path"), 1);
1244
+ var import_config5 = require("@viberails/config");
754
1245
  var import_scanner2 = require("@viberails/scanner");
755
- var import_chalk5 = __toESM(require("chalk"), 1);
756
- var CONFIG_FILE4 = "viberails.config.json";
1246
+ var import_chalk6 = __toESM(require("chalk"), 1);
1247
+ var CONFIG_FILE5 = "viberails.config.json";
757
1248
  async function syncCommand(cwd) {
758
1249
  const startDir = cwd ?? process.cwd();
759
1250
  const projectRoot = findProjectRoot(startDir);
@@ -762,23 +1253,23 @@ async function syncCommand(cwd) {
762
1253
  "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
1254
  );
764
1255
  }
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..."));
1256
+ const configPath = path13.join(projectRoot, CONFIG_FILE5);
1257
+ const existing = await (0, import_config5.loadConfig)(configPath);
1258
+ console.log(import_chalk6.default.dim("Scanning project..."));
768
1259
  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)}
1260
+ const merged = (0, import_config5.mergeConfig)(existing, scanResult);
1261
+ fs12.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
771
1262
  `);
772
1263
  writeGeneratedFiles(projectRoot, merged, scanResult);
773
1264
  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`);
1265
+ ${import_chalk6.default.bold("Synced:")}`);
1266
+ console.log(` ${import_chalk6.default.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
1267
+ console.log(` ${import_chalk6.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1268
+ console.log(` ${import_chalk6.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
778
1269
  }
779
1270
 
780
1271
  // src/index.ts
781
- var VERSION = "0.1.0";
1272
+ var VERSION = "0.2.0";
782
1273
  var program = new import_commander.Command();
783
1274
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
784
1275
  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 +1277,7 @@ program.command("init", { isDefault: true }).description("Scan your project and
786
1277
  await initCommand(options);
787
1278
  } catch (err) {
788
1279
  const message = err instanceof Error ? err.message : String(err);
789
- console.error(`${import_chalk6.default.red("Error:")} ${message}`);
1280
+ console.error(`${import_chalk7.default.red("Error:")} ${message}`);
790
1281
  process.exit(1);
791
1282
  }
792
1283
  });
@@ -795,7 +1286,7 @@ program.command("sync").description("Re-scan and update generated files").action
795
1286
  await syncCommand();
796
1287
  } catch (err) {
797
1288
  const message = err instanceof Error ? err.message : String(err);
798
- console.error(`${import_chalk6.default.red("Error:")} ${message}`);
1289
+ console.error(`${import_chalk7.default.red("Error:")} ${message}`);
799
1290
  process.exit(1);
800
1291
  }
801
1292
  });
@@ -808,7 +1299,17 @@ program.command("check").description("Check files against enforced rules").optio
808
1299
  process.exit(exitCode);
809
1300
  } catch (err) {
810
1301
  const message = err instanceof Error ? err.message : String(err);
811
- console.error(`${import_chalk6.default.red("Error:")} ${message}`);
1302
+ console.error(`${import_chalk7.default.red("Error:")} ${message}`);
1303
+ process.exit(1);
1304
+ }
1305
+ });
1306
+ 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) => {
1307
+ try {
1308
+ const exitCode = await fixCommand(options);
1309
+ process.exit(exitCode);
1310
+ } catch (err) {
1311
+ const message = err instanceof Error ? err.message : String(err);
1312
+ console.error(`${import_chalk7.default.red("Error:")} ${message}`);
812
1313
  process.exit(1);
813
1314
  }
814
1315
  });
@@ -817,7 +1318,7 @@ program.command("boundaries").description("Display, infer, or inspect import bou
817
1318
  await boundariesCommand(options);
818
1319
  } catch (err) {
819
1320
  const message = err instanceof Error ? err.message : String(err);
820
- console.error(`${import_chalk6.default.red("Error:")} ${message}`);
1321
+ console.error(`${import_chalk7.default.red("Error:")} ${message}`);
821
1322
  process.exit(1);
822
1323
  }
823
1324
  });