viberails 0.2.1 → 0.2.3

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
@@ -256,6 +256,23 @@ function resolveIgnoreForFile(relPath, config) {
256
256
  var import_node_child_process = require("child_process");
257
257
  var fs4 = __toESM(require("fs"), 1);
258
258
  var path4 = __toESM(require("path"), 1);
259
+ var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
260
+ "node_modules",
261
+ ".git",
262
+ "dist",
263
+ "build",
264
+ ".next",
265
+ ".expo",
266
+ ".output",
267
+ ".svelte-kit",
268
+ ".turbo",
269
+ "coverage",
270
+ "public",
271
+ "vendor",
272
+ "__generated__",
273
+ "generated",
274
+ ".viberails"
275
+ ]);
259
276
  var SOURCE_EXTS = /* @__PURE__ */ new Set([
260
277
  ".ts",
261
278
  ".tsx",
@@ -275,12 +292,19 @@ var NAMING_PATTERNS = {
275
292
  };
276
293
  function isIgnored(relPath, ignorePatterns) {
277
294
  for (const pattern of ignorePatterns) {
278
- if (pattern.endsWith("/**")) {
295
+ const startsGlob = pattern.startsWith("**/");
296
+ const endsGlob = pattern.endsWith("/**");
297
+ if (startsGlob && endsGlob) {
298
+ const middle = pattern.slice(3, -3);
299
+ if (relPath.startsWith(`${middle}/`) || relPath.includes(`/${middle}/`) || relPath === middle) {
300
+ return true;
301
+ }
302
+ } else if (endsGlob) {
279
303
  const prefix = pattern.slice(0, -3);
280
304
  if (relPath.startsWith(`${prefix}/`) || relPath === prefix) return true;
281
- } else if (pattern.startsWith("**/")) {
305
+ } else if (startsGlob) {
282
306
  const suffix = pattern.slice(3);
283
- if (relPath.endsWith(suffix)) return true;
307
+ if (relPath.endsWith(suffix) || relPath === suffix) return true;
284
308
  } else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
285
309
  return true;
286
310
  }
@@ -304,7 +328,7 @@ function checkNaming(relPath, conventions) {
304
328
  const filename = path4.basename(relPath);
305
329
  const ext = path4.extname(filename);
306
330
  if (!SOURCE_EXTS.has(ext)) return void 0;
307
- if (filename.startsWith("index.") || filename.includes(".config.") || filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith(".")) {
331
+ if (filename.startsWith("index.") || filename.includes(".config.") || filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith(".") || filename.startsWith("_") || filename.startsWith("+") || filename.startsWith("$") || filename.startsWith("[")) {
308
332
  return void 0;
309
333
  }
310
334
  const bare = filename.slice(0, filename.indexOf("."));
@@ -337,7 +361,7 @@ function getAllSourceFiles(projectRoot, config) {
337
361
  for (const entry of entries) {
338
362
  const rel = path4.relative(projectRoot, path4.join(dir, entry.name));
339
363
  if (entry.isDirectory()) {
340
- if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") {
364
+ if (ALWAYS_SKIP_DIRS.has(entry.name)) {
341
365
  continue;
342
366
  }
343
367
  if (isIgnored(rel, config.ignore)) continue;
@@ -400,13 +424,13 @@ function checkMissingTests(projectRoot, config, severity) {
400
424
  const testSuffix = testPattern.replace("*", "");
401
425
  const sourceFiles = collectSourceFiles(srcPath, projectRoot);
402
426
  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")) {
427
+ const basename7 = path5.basename(relFile);
428
+ if (basename7.includes(".test.") || basename7.includes(".spec.") || basename7.startsWith("index.") || basename7.endsWith(".d.ts")) {
405
429
  continue;
406
430
  }
407
- const ext = path5.extname(basename6);
431
+ const ext = path5.extname(basename7);
408
432
  if (!SOURCE_EXTS2.has(ext)) continue;
409
- const stem = basename6.slice(0, basename6.indexOf("."));
433
+ const stem = basename7.slice(0, basename7.indexOf("."));
410
434
  const expectedTestFile = `${stem}${testSuffix}`;
411
435
  const dir = path5.dirname(path5.join(projectRoot, relFile));
412
436
  const colocatedTest = path5.join(dir, expectedTestFile);
@@ -427,6 +451,50 @@ function checkMissingTests(projectRoot, config, severity) {
427
451
 
428
452
  // src/commands/check.ts
429
453
  var CONFIG_FILE2 = "viberails.config.json";
454
+ function isTestFile(relPath) {
455
+ const filename = path6.basename(relPath);
456
+ return filename.includes(".test.") || filename.includes(".spec.") || filename.startsWith("test.") || filename.startsWith("spec.") || relPath.includes("__tests__/") || relPath.includes("__test__/");
457
+ }
458
+ function printGroupedViolations(violations, limit) {
459
+ const groups = /* @__PURE__ */ new Map();
460
+ for (const v of violations) {
461
+ const existing = groups.get(v.rule) ?? [];
462
+ existing.push(v);
463
+ groups.set(v.rule, existing);
464
+ }
465
+ const ruleOrder = ["file-size", "file-naming", "missing-test", "boundary-violation"];
466
+ const sortedKeys = [...groups.keys()].sort(
467
+ (a, b) => (ruleOrder.indexOf(a) === -1 ? 99 : ruleOrder.indexOf(a)) - (ruleOrder.indexOf(b) === -1 ? 99 : ruleOrder.indexOf(b))
468
+ );
469
+ let totalShown = 0;
470
+ const totalLimit = limit ?? Number.POSITIVE_INFINITY;
471
+ for (const rule of sortedKeys) {
472
+ const group = groups.get(rule);
473
+ if (!group) continue;
474
+ const remaining = totalLimit - totalShown;
475
+ if (remaining <= 0) break;
476
+ const toShow = group.slice(0, remaining);
477
+ const hidden = group.length - toShow.length;
478
+ for (const v of toShow) {
479
+ const icon = v.severity === "error" ? import_chalk2.default.red("\u2717") : import_chalk2.default.yellow("!");
480
+ console.log(`${icon} ${import_chalk2.default.dim(v.rule)} ${v.file}: ${v.message}`);
481
+ }
482
+ totalShown += toShow.length;
483
+ if (hidden > 0) {
484
+ console.log(import_chalk2.default.dim(` ... and ${hidden} more ${rule} violations`));
485
+ }
486
+ }
487
+ }
488
+ function printSummary(violations) {
489
+ const counts = /* @__PURE__ */ new Map();
490
+ for (const v of violations) {
491
+ counts.set(v.rule, (counts.get(v.rule) ?? 0) + 1);
492
+ }
493
+ const word = violations.length === 1 ? "violation" : "violations";
494
+ const parts = [...counts.entries()].map(([rule, count]) => `${count} ${rule}`);
495
+ console.log(`
496
+ ${violations.length} ${word} found (${parts.join(", ")}).`);
497
+ }
430
498
  async function checkCommand(options, cwd) {
431
499
  const startDir = cwd ?? process.cwd();
432
500
  const projectRoot = findProjectRoot(startDir);
@@ -463,13 +531,15 @@ async function checkCommand(options, cwd) {
463
531
  if (isIgnored(relPath, effectiveIgnore)) continue;
464
532
  if (!fs6.existsSync(absPath)) continue;
465
533
  const resolved = resolveConfigForFile(relPath, config);
466
- if (resolved.rules.maxFileLines > 0) {
534
+ const testFile = isTestFile(relPath);
535
+ const maxLines = testFile ? resolved.rules.maxTestFileLines : resolved.rules.maxFileLines;
536
+ if (maxLines > 0) {
467
537
  const lines = countFileLines(absPath);
468
- if (lines !== null && lines > resolved.rules.maxFileLines) {
538
+ if (lines !== null && lines > maxLines) {
469
539
  violations.push({
470
540
  file: relPath,
471
541
  rule: "file-size",
472
- message: `${lines} lines (max ${resolved.rules.maxFileLines}). Split into focused modules.`,
542
+ message: `${lines} lines (max ${maxLines}). Split into focused modules.`,
473
543
  severity
474
544
  });
475
545
  }
@@ -517,13 +587,10 @@ async function checkCommand(options, cwd) {
517
587
  console.log(`${import_chalk2.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
518
588
  return 0;
519
589
  }
520
- for (const v of violations) {
521
- const icon = v.severity === "error" ? import_chalk2.default.red("\u2717") : import_chalk2.default.yellow("!");
522
- console.log(`${icon} ${import_chalk2.default.dim(v.rule)} ${v.file}: ${v.message}`);
590
+ if (!options.quiet) {
591
+ printGroupedViolations(violations, options.limit);
523
592
  }
524
- const word = violations.length === 1 ? "violation" : "violations";
525
- console.log(`
526
- ${violations.length} ${word} found.`);
593
+ printSummary(violations);
527
594
  if (config.enforcement === "enforce") {
528
595
  console.log(import_chalk2.default.red("Fix violations before committing."));
529
596
  return 1;
@@ -771,8 +838,8 @@ var path9 = __toESM(require("path"), 1);
771
838
  function generateTestStub(sourceRelPath, config, projectRoot) {
772
839
  const { testPattern } = config.structure;
773
840
  if (!testPattern) return null;
774
- const basename6 = path9.basename(sourceRelPath);
775
- const stem = basename6.slice(0, basename6.indexOf("."));
841
+ const basename7 = path9.basename(sourceRelPath);
842
+ const stem = basename7.slice(0, basename7.indexOf("."));
776
843
  const testSuffix = testPattern.replace("*", "");
777
844
  const testFilename = `${stem}${testSuffix}`;
778
845
  const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
@@ -812,7 +879,7 @@ async function fixCommand(options, cwd) {
812
879
  return 1;
813
880
  }
814
881
  const config = await (0, import_config3.loadConfig)(configPath);
815
- if (!options.yes && !options.dryRun) {
882
+ if (!options.dryRun) {
816
883
  const isDirty = checkGitDirty(projectRoot);
817
884
  if (isDirty) {
818
885
  console.log(
@@ -1343,7 +1410,7 @@ ${import_chalk9.default.bold("Synced:")}`);
1343
1410
  }
1344
1411
 
1345
1412
  // src/index.ts
1346
- var VERSION = "0.2.1";
1413
+ var VERSION = "0.2.3";
1347
1414
  var program = new import_commander.Command();
1348
1415
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1349
1416
  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) => {
@@ -1364,19 +1431,21 @@ program.command("sync").description("Re-scan and update generated files").action
1364
1431
  process.exit(1);
1365
1432
  }
1366
1433
  });
1367
- program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").action(async (options) => {
1368
- try {
1369
- const exitCode = await checkCommand({
1370
- ...options,
1371
- noBoundaries: options.boundaries === false
1372
- });
1373
- process.exit(exitCode);
1374
- } catch (err) {
1375
- const message = err instanceof Error ? err.message : String(err);
1376
- console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1377
- process.exit(1);
1434
+ program.command("check").description("Check files against enforced rules").option("--staged", "Check only staged files (for pre-commit hooks)").option("--files <files...>", "Check specific files").option("--no-boundaries", "Skip boundary checking").option("--quiet", "Show only summary counts, not individual violations").option("--limit <n>", "Maximum number of violations to display", Number.parseInt).action(
1435
+ async (options) => {
1436
+ try {
1437
+ const exitCode = await checkCommand({
1438
+ ...options,
1439
+ noBoundaries: options.boundaries === false
1440
+ });
1441
+ process.exit(exitCode);
1442
+ } catch (err) {
1443
+ const message = err instanceof Error ? err.message : String(err);
1444
+ console.error(`${import_chalk10.default.red("Error:")} ${message}`);
1445
+ process.exit(1);
1446
+ }
1378
1447
  }
1379
- });
1448
+ );
1380
1449
  program.command("fix").description("Auto-fix file naming violations and generate missing test stubs").option("--dry-run", "Show planned fixes without applying them").option("--rule <rules...>", "Fix only specific rules (file-naming, missing-test)").option("-y, --yes", "Skip confirmation prompt").action(async (options) => {
1381
1450
  try {
1382
1451
  const exitCode = await fixCommand(options);