viberails 0.3.3 → 0.4.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
@@ -56,50 +56,115 @@ async function promptInitDecision() {
56
56
  assertNotCancelled(result);
57
57
  return result;
58
58
  }
59
- async function promptRuleCustomization(defaults) {
60
- const maxFileLinesResult = await clack.text({
61
- message: "Maximum lines per source file?",
62
- placeholder: String(defaults.maxFileLines),
63
- initialValue: String(defaults.maxFileLines),
64
- validate: (v) => {
65
- const n = Number.parseInt(v, 10);
66
- if (Number.isNaN(n) || n < 1) return "Enter a positive number";
67
- }
68
- });
69
- assertNotCancelled(maxFileLinesResult);
70
- const requireTestsResult = await clack.confirm({
71
- message: "Require matching test files for source files?",
72
- initialValue: defaults.requireTests
73
- });
74
- assertNotCancelled(requireTestsResult);
75
- const namingLabel = defaults.fileNamingValue ? `Enforce file naming? (detected: ${defaults.fileNamingValue})` : "Enforce file naming?";
76
- const enforceNamingResult = await clack.confirm({
77
- message: namingLabel,
78
- initialValue: defaults.enforceNaming
79
- });
80
- assertNotCancelled(enforceNamingResult);
81
- const enforcementResult = await clack.select({
82
- message: "Enforcement mode",
83
- options: [
59
+ async function promptRuleMenu(defaults) {
60
+ const state = { ...defaults };
61
+ while (true) {
62
+ const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
63
+ const enforcementHint = state.enforcement === "warn" ? "warn \u2014 violations shown but commits allowed" : "enforce \u2014 commits blocked on violation";
64
+ const options = [
65
+ { value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
84
66
  {
85
- value: "warn",
86
- label: "warn",
87
- hint: "show violations but don't block commits (recommended)"
67
+ value: "requireTests",
68
+ label: "Require test files",
69
+ hint: state.requireTests ? "yes" : "no"
88
70
  },
89
- {
90
- value: "enforce",
91
- label: "enforce",
92
- hint: "block commits with violations"
93
- }
94
- ],
95
- initialValue: defaults.enforcement
96
- });
97
- assertNotCancelled(enforcementResult);
71
+ { value: "enforceNaming", label: "Enforce file naming", hint: namingHint },
72
+ { value: "enforcement", label: "Enforcement mode", hint: enforcementHint }
73
+ ];
74
+ if (state.packageOverrides && state.packageOverrides.length > 0) {
75
+ const count = state.packageOverrides.length;
76
+ options.push({
77
+ value: "packageOverrides",
78
+ label: "Per-package overrides",
79
+ hint: `${count} package${count > 1 ? "s" : ""} differ (view)`
80
+ });
81
+ }
82
+ options.push({ value: "done", label: "Done" });
83
+ const choice = await clack.select({
84
+ message: "Customize rules",
85
+ options
86
+ });
87
+ assertNotCancelled(choice);
88
+ if (choice === "done") break;
89
+ if (choice === "packageOverrides" && state.packageOverrides) {
90
+ const lines = state.packageOverrides.map((pkg) => {
91
+ const diffs = [];
92
+ if (pkg.conventions) {
93
+ for (const [key, val] of Object.entries(pkg.conventions)) {
94
+ const v = typeof val === "string" ? val : val?.value;
95
+ if (v) diffs.push(`${key}: ${v}`);
96
+ }
97
+ }
98
+ if (pkg.stack) {
99
+ for (const [key, val] of Object.entries(pkg.stack)) {
100
+ if (val) diffs.push(`${key}: ${val}`);
101
+ }
102
+ }
103
+ return `${pkg.path}
104
+ ${diffs.join(", ") || "minor differences"}`;
105
+ });
106
+ clack.note(
107
+ `${lines.join("\n\n")}
108
+
109
+ Edit the "packages" section in viberails.config.json to adjust.`,
110
+ "Per-package overrides"
111
+ );
112
+ continue;
113
+ }
114
+ if (choice === "maxFileLines") {
115
+ const result = await clack.text({
116
+ message: "Maximum lines per source file?",
117
+ initialValue: String(state.maxFileLines),
118
+ validate: (v) => {
119
+ const n = Number.parseInt(v, 10);
120
+ if (Number.isNaN(n) || n < 1) return "Enter a positive number";
121
+ }
122
+ });
123
+ assertNotCancelled(result);
124
+ state.maxFileLines = Number.parseInt(result, 10);
125
+ }
126
+ if (choice === "requireTests") {
127
+ const result = await clack.confirm({
128
+ message: "Require matching test files for source files?",
129
+ initialValue: state.requireTests
130
+ });
131
+ assertNotCancelled(result);
132
+ state.requireTests = result;
133
+ }
134
+ if (choice === "enforceNaming") {
135
+ const result = await clack.confirm({
136
+ message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
137
+ initialValue: state.enforceNaming
138
+ });
139
+ assertNotCancelled(result);
140
+ state.enforceNaming = result;
141
+ }
142
+ if (choice === "enforcement") {
143
+ const result = await clack.select({
144
+ message: "Enforcement mode",
145
+ options: [
146
+ {
147
+ value: "warn",
148
+ label: "warn",
149
+ hint: "show violations but don't block commits (recommended)"
150
+ },
151
+ {
152
+ value: "enforce",
153
+ label: "enforce",
154
+ hint: "block commits with violations"
155
+ }
156
+ ],
157
+ initialValue: state.enforcement
158
+ });
159
+ assertNotCancelled(result);
160
+ state.enforcement = result;
161
+ }
162
+ }
98
163
  return {
99
- maxFileLines: Number.parseInt(maxFileLinesResult, 10),
100
- requireTests: requireTestsResult,
101
- enforceNaming: enforceNamingResult,
102
- enforcement: enforcementResult
164
+ maxFileLines: state.maxFileLines,
165
+ requireTests: state.requireTests,
166
+ enforceNaming: state.enforceNaming,
167
+ enforcement: state.enforcement
103
168
  };
104
169
  }
105
170
  async function promptIntegrations(hookManager) {
@@ -560,7 +625,13 @@ async function checkCommand(options, cwd) {
560
625
  filesToCheck = getAllSourceFiles(projectRoot, config);
561
626
  }
562
627
  if (filesToCheck.length === 0) {
563
- console.log(`${chalk2.green("\u2713")} No files to check.`);
628
+ if (options.format === "json") {
629
+ console.log(
630
+ JSON.stringify({ violations: [], checkedFiles: 0, enforcement: config.enforcement })
631
+ );
632
+ } else {
633
+ console.log(`${chalk2.green("\u2713")} No files to check.`);
634
+ }
564
635
  return 0;
565
636
  }
566
637
  const violations = [];
@@ -622,7 +693,9 @@ async function checkCommand(options, cwd) {
622
693
  });
623
694
  }
624
695
  const elapsed = Date.now() - startTime;
625
- console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
696
+ if (options.format !== "json") {
697
+ console.log(chalk2.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
698
+ }
626
699
  }
627
700
  if (options.format === "json") {
628
701
  console.log(
@@ -649,8 +722,54 @@ async function checkCommand(options, cwd) {
649
722
  return 0;
650
723
  }
651
724
 
725
+ // src/commands/check-hook.ts
726
+ import * as fs7 from "fs";
727
+ function parseHookFilePath(input) {
728
+ try {
729
+ if (!input.trim()) return void 0;
730
+ const parsed = JSON.parse(input);
731
+ return parsed?.tool_input?.file_path ?? void 0;
732
+ } catch {
733
+ return void 0;
734
+ }
735
+ }
736
+ function readStdin() {
737
+ try {
738
+ return fs7.readFileSync(0, "utf-8");
739
+ } catch {
740
+ return "";
741
+ }
742
+ }
743
+ async function hookCheckCommand(cwd) {
744
+ try {
745
+ const filePath = parseHookFilePath(readStdin());
746
+ if (!filePath) return 0;
747
+ const originalWrite = process.stdout.write.bind(process.stdout);
748
+ let captured = "";
749
+ process.stdout.write = (chunk) => {
750
+ captured += typeof chunk === "string" ? chunk : chunk.toString();
751
+ return true;
752
+ };
753
+ try {
754
+ await checkCommand({ files: [filePath], format: "json" }, cwd);
755
+ } finally {
756
+ process.stdout.write = originalWrite;
757
+ }
758
+ if (!captured.trim()) return 0;
759
+ const result = JSON.parse(captured);
760
+ if (result.violations?.length > 0) {
761
+ process.stderr.write(`${captured.trim()}
762
+ `);
763
+ return 2;
764
+ }
765
+ return 0;
766
+ } catch {
767
+ return 0;
768
+ }
769
+ }
770
+
652
771
  // src/commands/fix.ts
653
- import * as fs9 from "fs";
772
+ import * as fs10 from "fs";
654
773
  import * as path10 from "path";
655
774
  import { loadConfig as loadConfig3 } from "@viberails/config";
656
775
  import chalk4 from "chalk";
@@ -793,7 +912,7 @@ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
793
912
  }
794
913
 
795
914
  // src/commands/fix-naming.ts
796
- import * as fs7 from "fs";
915
+ import * as fs8 from "fs";
797
916
  import * as path8 from "path";
798
917
 
799
918
  // src/commands/convert-name.ts
@@ -855,12 +974,12 @@ function computeRename(relPath, targetConvention, projectRoot) {
855
974
  const newRelPath = path8.join(dir, newFilename);
856
975
  const oldAbsPath = path8.join(projectRoot, relPath);
857
976
  const newAbsPath = path8.join(projectRoot, newRelPath);
858
- if (fs7.existsSync(newAbsPath)) return null;
977
+ if (fs8.existsSync(newAbsPath)) return null;
859
978
  return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
860
979
  }
861
980
  function executeRename(rename) {
862
- if (fs7.existsSync(rename.newAbsPath)) return false;
863
- fs7.renameSync(rename.oldAbsPath, rename.newAbsPath);
981
+ if (fs8.existsSync(rename.newAbsPath)) return false;
982
+ fs8.renameSync(rename.oldAbsPath, rename.newAbsPath);
864
983
  return true;
865
984
  }
866
985
  function deduplicateRenames(renames) {
@@ -875,7 +994,7 @@ function deduplicateRenames(renames) {
875
994
  }
876
995
 
877
996
  // src/commands/fix-tests.ts
878
- import * as fs8 from "fs";
997
+ import * as fs9 from "fs";
879
998
  import * as path9 from "path";
880
999
  function generateTestStub(sourceRelPath, config, projectRoot) {
881
1000
  const { testPattern } = config.structure;
@@ -886,7 +1005,7 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
886
1005
  const testFilename = `${stem}${testSuffix}`;
887
1006
  const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
888
1007
  const testAbsPath = path9.join(dir, testFilename);
889
- if (fs8.existsSync(testAbsPath)) return null;
1008
+ if (fs9.existsSync(testAbsPath)) return null;
890
1009
  return {
891
1010
  path: path9.relative(projectRoot, testAbsPath),
892
1011
  absPath: testAbsPath,
@@ -900,8 +1019,8 @@ function writeTestStub(stub, config) {
900
1019
  it.todo('add tests');
901
1020
  });
902
1021
  `;
903
- fs8.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
904
- fs8.writeFileSync(stub.absPath, content);
1022
+ fs9.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
1023
+ fs9.writeFileSync(stub.absPath, content);
905
1024
  }
906
1025
 
907
1026
  // src/commands/fix.ts
@@ -914,7 +1033,7 @@ async function fixCommand(options, cwd) {
914
1033
  return 1;
915
1034
  }
916
1035
  const configPath = path10.join(projectRoot, CONFIG_FILE3);
917
- if (!fs9.existsSync(configPath)) {
1036
+ if (!fs10.existsSync(configPath)) {
918
1037
  console.error(
919
1038
  `${chalk4.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
920
1039
  );
@@ -978,13 +1097,13 @@ async function fixCommand(options, cwd) {
978
1097
  }
979
1098
  let importUpdateCount = 0;
980
1099
  if (renameCount > 0) {
981
- const appliedRenames = dedupedRenames.filter((r) => fs9.existsSync(r.newAbsPath));
1100
+ const appliedRenames = dedupedRenames.filter((r) => fs10.existsSync(r.newAbsPath));
982
1101
  const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
983
1102
  importUpdateCount = updates.length;
984
1103
  }
985
1104
  let stubCount = 0;
986
1105
  for (const stub of testStubs) {
987
- if (!fs9.existsSync(stub.absPath)) {
1106
+ if (!fs10.existsSync(stub.absPath)) {
988
1107
  writeTestStub(stub, config);
989
1108
  stubCount++;
990
1109
  }
@@ -1005,7 +1124,7 @@ async function fixCommand(options, cwd) {
1005
1124
  }
1006
1125
 
1007
1126
  // src/commands/init.ts
1008
- import * as fs12 from "fs";
1127
+ import * as fs13 from "fs";
1009
1128
  import * as path13 from "path";
1010
1129
  import * as clack2 from "@clack/prompts";
1011
1130
  import { generateConfig } from "@viberails/config";
@@ -1439,7 +1558,7 @@ function formatScanResultsText(scanResult, config) {
1439
1558
  }
1440
1559
 
1441
1560
  // src/utils/write-generated-files.ts
1442
- import * as fs10 from "fs";
1561
+ import * as fs11 from "fs";
1443
1562
  import * as path11 from "path";
1444
1563
  import { generateContext } from "@viberails/context";
1445
1564
  var CONTEXT_DIR = ".viberails";
@@ -1448,12 +1567,12 @@ var SCAN_RESULT_FILE = "scan-result.json";
1448
1567
  function writeGeneratedFiles(projectRoot, config, scanResult) {
1449
1568
  const contextDir = path11.join(projectRoot, CONTEXT_DIR);
1450
1569
  try {
1451
- if (!fs10.existsSync(contextDir)) {
1452
- fs10.mkdirSync(contextDir, { recursive: true });
1570
+ if (!fs11.existsSync(contextDir)) {
1571
+ fs11.mkdirSync(contextDir, { recursive: true });
1453
1572
  }
1454
1573
  const context = generateContext(config);
1455
- fs10.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
1456
- fs10.writeFileSync(
1574
+ fs11.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
1575
+ fs11.writeFileSync(
1457
1576
  path11.join(contextDir, SCAN_RESULT_FILE),
1458
1577
  `${JSON.stringify(scanResult, null, 2)}
1459
1578
  `
@@ -1465,27 +1584,28 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
1465
1584
  }
1466
1585
 
1467
1586
  // src/commands/init-hooks.ts
1468
- import * as fs11 from "fs";
1587
+ import * as fs12 from "fs";
1469
1588
  import * as path12 from "path";
1470
1589
  import chalk7 from "chalk";
1590
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1471
1591
  function setupPreCommitHook(projectRoot) {
1472
1592
  const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1473
- if (fs11.existsSync(lefthookPath)) {
1593
+ if (fs12.existsSync(lefthookPath)) {
1474
1594
  addLefthookPreCommit(lefthookPath);
1475
1595
  console.log(` ${chalk7.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1476
1596
  return;
1477
1597
  }
1478
1598
  const huskyDir = path12.join(projectRoot, ".husky");
1479
- if (fs11.existsSync(huskyDir)) {
1599
+ if (fs12.existsSync(huskyDir)) {
1480
1600
  writeHuskyPreCommit(huskyDir);
1481
1601
  console.log(` ${chalk7.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1482
1602
  return;
1483
1603
  }
1484
1604
  const gitDir = path12.join(projectRoot, ".git");
1485
- if (fs11.existsSync(gitDir)) {
1605
+ if (fs12.existsSync(gitDir)) {
1486
1606
  const hooksDir = path12.join(gitDir, "hooks");
1487
- if (!fs11.existsSync(hooksDir)) {
1488
- fs11.mkdirSync(hooksDir, { recursive: true });
1607
+ if (!fs12.existsSync(hooksDir)) {
1608
+ fs12.mkdirSync(hooksDir, { recursive: true });
1489
1609
  }
1490
1610
  writeGitHookPreCommit(hooksDir);
1491
1611
  console.log(` ${chalk7.green("\u2713")} .git/hooks/pre-commit`);
@@ -1493,10 +1613,10 @@ function setupPreCommitHook(projectRoot) {
1493
1613
  }
1494
1614
  function writeGitHookPreCommit(hooksDir) {
1495
1615
  const hookPath = path12.join(hooksDir, "pre-commit");
1496
- if (fs11.existsSync(hookPath)) {
1497
- const existing = fs11.readFileSync(hookPath, "utf-8");
1616
+ if (fs12.existsSync(hookPath)) {
1617
+ const existing = fs12.readFileSync(hookPath, "utf-8");
1498
1618
  if (existing.includes("viberails")) return;
1499
- fs11.writeFileSync(
1619
+ fs12.writeFileSync(
1500
1620
  hookPath,
1501
1621
  `${existing.trimEnd()}
1502
1622
 
@@ -1513,71 +1633,51 @@ npx viberails check --staged
1513
1633
  "npx viberails check --staged",
1514
1634
  ""
1515
1635
  ].join("\n");
1516
- fs11.writeFileSync(hookPath, script, { mode: 493 });
1636
+ fs12.writeFileSync(hookPath, script, { mode: 493 });
1517
1637
  }
1518
1638
  function addLefthookPreCommit(lefthookPath) {
1519
- const content = fs11.readFileSync(lefthookPath, "utf-8");
1639
+ const content = fs12.readFileSync(lefthookPath, "utf-8");
1520
1640
  if (content.includes("viberails")) return;
1521
- const hasPreCommit = /^pre-commit:/m.test(content);
1522
- if (hasPreCommit) {
1523
- const commandBlock = ["", " viberails:", " run: npx viberails check --staged"].join(
1524
- "\n"
1525
- );
1526
- const updated = `${content.trimEnd()}
1527
- ${commandBlock}
1528
- `;
1529
- fs11.writeFileSync(lefthookPath, updated);
1530
- } else {
1531
- const section = [
1532
- "",
1533
- "pre-commit:",
1534
- " commands:",
1535
- " viberails:",
1536
- " run: npx viberails check --staged"
1537
- ].join("\n");
1538
- fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
1539
- ${section}
1540
- `);
1641
+ const doc = parseYaml(content) ?? {};
1642
+ if (!doc["pre-commit"]) {
1643
+ doc["pre-commit"] = { commands: {} };
1644
+ }
1645
+ if (!doc["pre-commit"].commands) {
1646
+ doc["pre-commit"].commands = {};
1541
1647
  }
1648
+ doc["pre-commit"].commands.viberails = {
1649
+ run: "npx viberails check --staged"
1650
+ };
1651
+ fs12.writeFileSync(lefthookPath, stringifyYaml(doc));
1542
1652
  }
1543
1653
  function detectHookManager(projectRoot) {
1544
- if (fs11.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
1545
- if (fs11.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
1546
- if (fs11.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
1654
+ if (fs12.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
1655
+ if (fs12.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
1656
+ if (fs12.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
1547
1657
  return void 0;
1548
1658
  }
1549
1659
  function setupClaudeCodeHook(projectRoot) {
1550
1660
  const claudeDir = path12.join(projectRoot, ".claude");
1551
- if (!fs11.existsSync(claudeDir)) {
1552
- fs11.mkdirSync(claudeDir, { recursive: true });
1661
+ if (!fs12.existsSync(claudeDir)) {
1662
+ fs12.mkdirSync(claudeDir, { recursive: true });
1553
1663
  }
1554
1664
  const settingsPath = path12.join(claudeDir, "settings.json");
1555
1665
  let settings = {};
1556
- if (fs11.existsSync(settingsPath)) {
1666
+ if (fs12.existsSync(settingsPath)) {
1557
1667
  try {
1558
- settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
1668
+ settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
1559
1669
  } catch {
1560
1670
  console.warn(
1561
- ` ${chalk7.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
1671
+ ` ${chalk7.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
1562
1672
  );
1563
- settings = {};
1673
+ console.warn(` Fix the JSON manually, then re-run ${chalk7.cyan("viberails init --force")}`);
1674
+ return;
1564
1675
  }
1565
1676
  }
1566
1677
  const hooks = settings.hooks ?? {};
1567
1678
  const existing = hooks.PostToolUse ?? [];
1568
1679
  if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
1569
- const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
1570
- const checkAndReport = [
1571
- `FILE=$(${extractFile})`,
1572
- 'if [ -z "$FILE" ]; then exit 0; fi',
1573
- 'OUTPUT=$(npx viberails check --files "$FILE" --format json 2>&1)',
1574
- `if echo "$OUTPUT" | node -e "process.exit(JSON.parse(require('fs').readFileSync(0,'utf8')).violations?.length?0:1)" 2>/dev/null; then`,
1575
- ' echo "$OUTPUT" >&2',
1576
- " exit 2",
1577
- "fi",
1578
- "exit 0"
1579
- ].join("\n");
1580
- const hookCommand = checkAndReport;
1680
+ const hookCommand = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --hook; else npx viberails check --hook; fi";
1581
1681
  hooks.PostToolUse = [
1582
1682
  ...existing,
1583
1683
  {
@@ -1591,34 +1691,34 @@ function setupClaudeCodeHook(projectRoot) {
1591
1691
  }
1592
1692
  ];
1593
1693
  settings.hooks = hooks;
1594
- fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1694
+ fs12.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1595
1695
  `);
1596
1696
  console.log(` ${chalk7.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1597
1697
  }
1598
1698
  function setupClaudeMdReference(projectRoot) {
1599
1699
  const claudeMdPath = path12.join(projectRoot, "CLAUDE.md");
1600
1700
  let content = "";
1601
- if (fs11.existsSync(claudeMdPath)) {
1602
- content = fs11.readFileSync(claudeMdPath, "utf-8");
1701
+ if (fs12.existsSync(claudeMdPath)) {
1702
+ content = fs12.readFileSync(claudeMdPath, "utf-8");
1603
1703
  }
1604
1704
  if (content.includes("@.viberails/context.md")) return;
1605
1705
  const ref = "\n@.viberails/context.md\n";
1606
1706
  const prefix = content.length === 0 ? "" : content.trimEnd();
1607
- fs11.writeFileSync(claudeMdPath, prefix + ref);
1707
+ fs12.writeFileSync(claudeMdPath, prefix + ref);
1608
1708
  console.log(` ${chalk7.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
1609
1709
  }
1610
1710
  function writeHuskyPreCommit(huskyDir) {
1611
1711
  const hookPath = path12.join(huskyDir, "pre-commit");
1612
- if (fs11.existsSync(hookPath)) {
1613
- const existing = fs11.readFileSync(hookPath, "utf-8");
1712
+ if (fs12.existsSync(hookPath)) {
1713
+ const existing = fs12.readFileSync(hookPath, "utf-8");
1614
1714
  if (!existing.includes("viberails")) {
1615
- fs11.writeFileSync(hookPath, `${existing.trimEnd()}
1715
+ fs12.writeFileSync(hookPath, `${existing.trimEnd()}
1616
1716
  npx viberails check --staged
1617
1717
  `);
1618
1718
  }
1619
1719
  return;
1620
1720
  }
1621
- fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1721
+ fs12.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1622
1722
  }
1623
1723
 
1624
1724
  // src/commands/init.ts
@@ -1639,10 +1739,6 @@ function getConventionStr3(cv) {
1639
1739
  if (!cv) return void 0;
1640
1740
  return typeof cv === "string" ? cv : cv.value;
1641
1741
  }
1642
- function hasConventionOverrides(config) {
1643
- if (!config.packages || config.packages.length === 0) return false;
1644
- return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
1645
- }
1646
1742
  async function initCommand(options, cwd) {
1647
1743
  const startDir = cwd ?? process.cwd();
1648
1744
  const projectRoot = findProjectRoot(startDir);
@@ -1652,7 +1748,7 @@ async function initCommand(options, cwd) {
1652
1748
  );
1653
1749
  }
1654
1750
  const configPath = path13.join(projectRoot, CONFIG_FILE4);
1655
- if (fs12.existsSync(configPath) && !options.force) {
1751
+ if (fs13.existsSync(configPath) && !options.force) {
1656
1752
  console.log(
1657
1753
  `${chalk8.yellow("!")} viberails is already initialized.
1658
1754
  Run ${chalk8.cyan("viberails sync")} to update, or ${chalk8.cyan("viberails init --force")} to start fresh.`
@@ -1682,7 +1778,7 @@ async function initCommand(options, cwd) {
1682
1778
  console.log(` Inferred ${denyCount} boundary rules`);
1683
1779
  }
1684
1780
  }
1685
- fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
1781
+ fs13.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
1686
1782
  `);
1687
1783
  writeGeneratedFiles(projectRoot, config2, scanResult2);
1688
1784
  updateGitignore(projectRoot);
@@ -1709,27 +1805,18 @@ Created:`);
1709
1805
  clack2.note(resultsText, "Scan results");
1710
1806
  const decision = await promptInitDecision();
1711
1807
  if (decision === "customize") {
1712
- clack2.note(
1713
- "Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
1714
- "Rules"
1715
- );
1716
- const overrides = await promptRuleCustomization({
1808
+ const overrides = await promptRuleMenu({
1717
1809
  maxFileLines: config.rules.maxFileLines,
1718
1810
  requireTests: config.rules.requireTests,
1719
1811
  enforceNaming: config.rules.enforceNaming,
1720
1812
  enforcement: config.enforcement,
1721
- fileNamingValue: getConventionStr3(config.conventions.fileNaming)
1813
+ fileNamingValue: getConventionStr3(config.conventions.fileNaming),
1814
+ packageOverrides: config.packages
1722
1815
  });
1723
1816
  config.rules.maxFileLines = overrides.maxFileLines;
1724
1817
  config.rules.requireTests = overrides.requireTests;
1725
1818
  config.rules.enforceNaming = overrides.enforceNaming;
1726
1819
  config.enforcement = overrides.enforcement;
1727
- if (config.workspace?.packages && config.workspace.packages.length > 0) {
1728
- clack2.note(
1729
- 'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
1730
- "Per-package overrides"
1731
- );
1732
- }
1733
1820
  }
1734
1821
  if (config.workspace?.packages && config.workspace.packages.length > 0) {
1735
1822
  clack2.note(
@@ -1759,13 +1846,7 @@ Created:`);
1759
1846
  }
1760
1847
  const hookManager = detectHookManager(projectRoot);
1761
1848
  const integrations = await promptIntegrations(hookManager);
1762
- if (hasConventionOverrides(config)) {
1763
- clack2.note(
1764
- "Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
1765
- "Per-package conventions"
1766
- );
1767
- }
1768
- fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1849
+ fs13.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1769
1850
  `);
1770
1851
  writeGeneratedFiles(projectRoot, config, scanResult);
1771
1852
  updateGitignore(projectRoot);
@@ -1776,8 +1857,7 @@ Created:`);
1776
1857
  ];
1777
1858
  if (integrations.preCommitHook) {
1778
1859
  setupPreCommitHook(projectRoot);
1779
- const hookMgr = detectHookManager(projectRoot);
1780
- if (hookMgr) {
1860
+ if (hookManager === "Lefthook") {
1781
1861
  createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
1782
1862
  }
1783
1863
  }
@@ -1796,19 +1876,19 @@ ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
1796
1876
  function updateGitignore(projectRoot) {
1797
1877
  const gitignorePath = path13.join(projectRoot, ".gitignore");
1798
1878
  let content = "";
1799
- if (fs12.existsSync(gitignorePath)) {
1800
- content = fs12.readFileSync(gitignorePath, "utf-8");
1879
+ if (fs13.existsSync(gitignorePath)) {
1880
+ content = fs13.readFileSync(gitignorePath, "utf-8");
1801
1881
  }
1802
1882
  if (!content.includes(".viberails/scan-result.json")) {
1803
1883
  const block = "\n# viberails\n.viberails/scan-result.json\n";
1804
1884
  const prefix = content.length === 0 ? "" : `${content.trimEnd()}
1805
1885
  `;
1806
- fs12.writeFileSync(gitignorePath, `${prefix}${block}`);
1886
+ fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
1807
1887
  }
1808
1888
  }
1809
1889
 
1810
1890
  // src/commands/sync.ts
1811
- import * as fs13 from "fs";
1891
+ import * as fs14 from "fs";
1812
1892
  import * as path14 from "path";
1813
1893
  import { loadConfig as loadConfig4, mergeConfig } from "@viberails/config";
1814
1894
  import { scan as scan2 } from "@viberails/scanner";
@@ -1943,7 +2023,7 @@ var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
1943
2023
  function loadPreviousStats(projectRoot) {
1944
2024
  const scanResultPath = path14.join(projectRoot, SCAN_RESULT_FILE2);
1945
2025
  try {
1946
- const raw = fs13.readFileSync(scanResultPath, "utf-8");
2026
+ const raw = fs14.readFileSync(scanResultPath, "utf-8");
1947
2027
  const parsed = JSON.parse(raw);
1948
2028
  if (parsed?.statistics?.totalFiles !== void 0) {
1949
2029
  return parsed.statistics;
@@ -1982,7 +2062,7 @@ ${chalk9.bold("Changes:")}`);
1982
2062
  console.log(` ${chalk9.dim(statsDelta)}`);
1983
2063
  }
1984
2064
  }
1985
- fs13.writeFileSync(configPath, `${mergedJson}
2065
+ fs14.writeFileSync(configPath, `${mergedJson}
1986
2066
  `);
1987
2067
  writeGeneratedFiles(projectRoot, merged, scanResult);
1988
2068
  console.log(`
@@ -1997,7 +2077,7 @@ ${chalk9.bold("Synced:")}`);
1997
2077
  }
1998
2078
 
1999
2079
  // src/index.ts
2000
- var VERSION = "0.3.3";
2080
+ var VERSION = "0.4.0";
2001
2081
  var program = new Command();
2002
2082
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
2003
2083
  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)").option("-f, --force", "Re-initialize, replacing existing config").action(async (options) => {
@@ -2018,9 +2098,13 @@ program.command("sync").description("Re-scan and update generated files").action
2018
2098
  process.exit(1);
2019
2099
  }
2020
2100
  });
2021
- 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).option("--format <format>", "Output format: text (default) or json").action(
2101
+ 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).option("--format <format>", "Output format: text (default) or json").option("--hook", "Claude Code hook mode: read file from stdin, output to stderr").action(
2022
2102
  async (options) => {
2023
2103
  try {
2104
+ if (options.hook) {
2105
+ const exitCode2 = await hookCheckCommand();
2106
+ process.exit(exitCode2);
2107
+ }
2024
2108
  const exitCode = await checkCommand({
2025
2109
  ...options,
2026
2110
  noBoundaries: options.boundaries === false,