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