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.cjs CHANGED
@@ -89,50 +89,115 @@ async function promptInitDecision() {
89
89
  assertNotCancelled(result);
90
90
  return result;
91
91
  }
92
- async function promptRuleCustomization(defaults) {
93
- const maxFileLinesResult = await clack.text({
94
- message: "Maximum lines per source file?",
95
- placeholder: String(defaults.maxFileLines),
96
- initialValue: String(defaults.maxFileLines),
97
- validate: (v) => {
98
- const n = Number.parseInt(v, 10);
99
- if (Number.isNaN(n) || n < 1) return "Enter a positive number";
100
- }
101
- });
102
- assertNotCancelled(maxFileLinesResult);
103
- const requireTestsResult = await clack.confirm({
104
- message: "Require matching test files for source files?",
105
- initialValue: defaults.requireTests
106
- });
107
- assertNotCancelled(requireTestsResult);
108
- const namingLabel = defaults.fileNamingValue ? `Enforce file naming? (detected: ${defaults.fileNamingValue})` : "Enforce file naming?";
109
- const enforceNamingResult = await clack.confirm({
110
- message: namingLabel,
111
- initialValue: defaults.enforceNaming
112
- });
113
- assertNotCancelled(enforceNamingResult);
114
- const enforcementResult = await clack.select({
115
- message: "Enforcement mode",
116
- options: [
92
+ async function promptRuleMenu(defaults) {
93
+ const state = { ...defaults };
94
+ while (true) {
95
+ const namingHint = state.enforceNaming ? `yes${state.fileNamingValue ? ` (${state.fileNamingValue})` : ""}` : "no";
96
+ const enforcementHint = state.enforcement === "warn" ? "warn \u2014 violations shown but commits allowed" : "enforce \u2014 commits blocked on violation";
97
+ const options = [
98
+ { value: "maxFileLines", label: "Max file lines", hint: String(state.maxFileLines) },
117
99
  {
118
- value: "warn",
119
- label: "warn",
120
- hint: "show violations but don't block commits (recommended)"
100
+ value: "requireTests",
101
+ label: "Require test files",
102
+ hint: state.requireTests ? "yes" : "no"
121
103
  },
122
- {
123
- value: "enforce",
124
- label: "enforce",
125
- hint: "block commits with violations"
126
- }
127
- ],
128
- initialValue: defaults.enforcement
129
- });
130
- assertNotCancelled(enforcementResult);
104
+ { value: "enforceNaming", label: "Enforce file naming", hint: namingHint },
105
+ { value: "enforcement", label: "Enforcement mode", hint: enforcementHint }
106
+ ];
107
+ if (state.packageOverrides && state.packageOverrides.length > 0) {
108
+ const count = state.packageOverrides.length;
109
+ options.push({
110
+ value: "packageOverrides",
111
+ label: "Per-package overrides",
112
+ hint: `${count} package${count > 1 ? "s" : ""} differ (view)`
113
+ });
114
+ }
115
+ options.push({ value: "done", label: "Done" });
116
+ const choice = await clack.select({
117
+ message: "Customize rules",
118
+ options
119
+ });
120
+ assertNotCancelled(choice);
121
+ if (choice === "done") break;
122
+ if (choice === "packageOverrides" && state.packageOverrides) {
123
+ const lines = state.packageOverrides.map((pkg) => {
124
+ const diffs = [];
125
+ if (pkg.conventions) {
126
+ for (const [key, val] of Object.entries(pkg.conventions)) {
127
+ const v = typeof val === "string" ? val : val?.value;
128
+ if (v) diffs.push(`${key}: ${v}`);
129
+ }
130
+ }
131
+ if (pkg.stack) {
132
+ for (const [key, val] of Object.entries(pkg.stack)) {
133
+ if (val) diffs.push(`${key}: ${val}`);
134
+ }
135
+ }
136
+ return `${pkg.path}
137
+ ${diffs.join(", ") || "minor differences"}`;
138
+ });
139
+ clack.note(
140
+ `${lines.join("\n\n")}
141
+
142
+ Edit the "packages" section in viberails.config.json to adjust.`,
143
+ "Per-package overrides"
144
+ );
145
+ continue;
146
+ }
147
+ if (choice === "maxFileLines") {
148
+ const result = await clack.text({
149
+ message: "Maximum lines per source file?",
150
+ initialValue: String(state.maxFileLines),
151
+ validate: (v) => {
152
+ const n = Number.parseInt(v, 10);
153
+ if (Number.isNaN(n) || n < 1) return "Enter a positive number";
154
+ }
155
+ });
156
+ assertNotCancelled(result);
157
+ state.maxFileLines = Number.parseInt(result, 10);
158
+ }
159
+ if (choice === "requireTests") {
160
+ const result = await clack.confirm({
161
+ message: "Require matching test files for source files?",
162
+ initialValue: state.requireTests
163
+ });
164
+ assertNotCancelled(result);
165
+ state.requireTests = result;
166
+ }
167
+ if (choice === "enforceNaming") {
168
+ const result = await clack.confirm({
169
+ message: state.fileNamingValue ? `Enforce file naming? (detected: ${state.fileNamingValue})` : "Enforce file naming?",
170
+ initialValue: state.enforceNaming
171
+ });
172
+ assertNotCancelled(result);
173
+ state.enforceNaming = result;
174
+ }
175
+ if (choice === "enforcement") {
176
+ const result = await clack.select({
177
+ message: "Enforcement mode",
178
+ options: [
179
+ {
180
+ value: "warn",
181
+ label: "warn",
182
+ hint: "show violations but don't block commits (recommended)"
183
+ },
184
+ {
185
+ value: "enforce",
186
+ label: "enforce",
187
+ hint: "block commits with violations"
188
+ }
189
+ ],
190
+ initialValue: state.enforcement
191
+ });
192
+ assertNotCancelled(result);
193
+ state.enforcement = result;
194
+ }
195
+ }
131
196
  return {
132
- maxFileLines: Number.parseInt(maxFileLinesResult, 10),
133
- requireTests: requireTestsResult,
134
- enforceNaming: enforceNamingResult,
135
- enforcement: enforcementResult
197
+ maxFileLines: state.maxFileLines,
198
+ requireTests: state.requireTests,
199
+ enforceNaming: state.enforceNaming,
200
+ enforcement: state.enforcement
136
201
  };
137
202
  }
138
203
  async function promptIntegrations(hookManager) {
@@ -593,7 +658,13 @@ async function checkCommand(options, cwd) {
593
658
  filesToCheck = getAllSourceFiles(projectRoot, config);
594
659
  }
595
660
  if (filesToCheck.length === 0) {
596
- console.log(`${import_chalk2.default.green("\u2713")} No files to check.`);
661
+ if (options.format === "json") {
662
+ console.log(
663
+ JSON.stringify({ violations: [], checkedFiles: 0, enforcement: config.enforcement })
664
+ );
665
+ } else {
666
+ console.log(`${import_chalk2.default.green("\u2713")} No files to check.`);
667
+ }
597
668
  return 0;
598
669
  }
599
670
  const violations = [];
@@ -655,7 +726,9 @@ async function checkCommand(options, cwd) {
655
726
  });
656
727
  }
657
728
  const elapsed = Date.now() - startTime;
658
- console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
729
+ if (options.format !== "json") {
730
+ console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
731
+ }
659
732
  }
660
733
  if (options.format === "json") {
661
734
  console.log(
@@ -682,8 +755,54 @@ async function checkCommand(options, cwd) {
682
755
  return 0;
683
756
  }
684
757
 
758
+ // src/commands/check-hook.ts
759
+ var fs7 = __toESM(require("fs"), 1);
760
+ function parseHookFilePath(input) {
761
+ try {
762
+ if (!input.trim()) return void 0;
763
+ const parsed = JSON.parse(input);
764
+ return parsed?.tool_input?.file_path ?? void 0;
765
+ } catch {
766
+ return void 0;
767
+ }
768
+ }
769
+ function readStdin() {
770
+ try {
771
+ return fs7.readFileSync(0, "utf-8");
772
+ } catch {
773
+ return "";
774
+ }
775
+ }
776
+ async function hookCheckCommand(cwd) {
777
+ try {
778
+ const filePath = parseHookFilePath(readStdin());
779
+ if (!filePath) return 0;
780
+ const originalWrite = process.stdout.write.bind(process.stdout);
781
+ let captured = "";
782
+ process.stdout.write = (chunk) => {
783
+ captured += typeof chunk === "string" ? chunk : chunk.toString();
784
+ return true;
785
+ };
786
+ try {
787
+ await checkCommand({ files: [filePath], format: "json" }, cwd);
788
+ } finally {
789
+ process.stdout.write = originalWrite;
790
+ }
791
+ if (!captured.trim()) return 0;
792
+ const result = JSON.parse(captured);
793
+ if (result.violations?.length > 0) {
794
+ process.stderr.write(`${captured.trim()}
795
+ `);
796
+ return 2;
797
+ }
798
+ return 0;
799
+ } catch {
800
+ return 0;
801
+ }
802
+ }
803
+
685
804
  // src/commands/fix.ts
686
- var fs9 = __toESM(require("fs"), 1);
805
+ var fs10 = __toESM(require("fs"), 1);
687
806
  var path10 = __toESM(require("path"), 1);
688
807
  var import_config3 = require("@viberails/config");
689
808
  var import_chalk4 = __toESM(require("chalk"), 1);
@@ -826,7 +945,7 @@ function resolveToRenamedFile(specifier, fromDir, renameMap, extensions) {
826
945
  }
827
946
 
828
947
  // src/commands/fix-naming.ts
829
- var fs7 = __toESM(require("fs"), 1);
948
+ var fs8 = __toESM(require("fs"), 1);
830
949
  var path8 = __toESM(require("path"), 1);
831
950
 
832
951
  // src/commands/convert-name.ts
@@ -888,12 +1007,12 @@ function computeRename(relPath, targetConvention, projectRoot) {
888
1007
  const newRelPath = path8.join(dir, newFilename);
889
1008
  const oldAbsPath = path8.join(projectRoot, relPath);
890
1009
  const newAbsPath = path8.join(projectRoot, newRelPath);
891
- if (fs7.existsSync(newAbsPath)) return null;
1010
+ if (fs8.existsSync(newAbsPath)) return null;
892
1011
  return { oldPath: relPath, newPath: newRelPath, oldAbsPath, newAbsPath };
893
1012
  }
894
1013
  function executeRename(rename) {
895
- if (fs7.existsSync(rename.newAbsPath)) return false;
896
- fs7.renameSync(rename.oldAbsPath, rename.newAbsPath);
1014
+ if (fs8.existsSync(rename.newAbsPath)) return false;
1015
+ fs8.renameSync(rename.oldAbsPath, rename.newAbsPath);
897
1016
  return true;
898
1017
  }
899
1018
  function deduplicateRenames(renames) {
@@ -908,7 +1027,7 @@ function deduplicateRenames(renames) {
908
1027
  }
909
1028
 
910
1029
  // src/commands/fix-tests.ts
911
- var fs8 = __toESM(require("fs"), 1);
1030
+ var fs9 = __toESM(require("fs"), 1);
912
1031
  var path9 = __toESM(require("path"), 1);
913
1032
  function generateTestStub(sourceRelPath, config, projectRoot) {
914
1033
  const { testPattern } = config.structure;
@@ -919,7 +1038,7 @@ function generateTestStub(sourceRelPath, config, projectRoot) {
919
1038
  const testFilename = `${stem}${testSuffix}`;
920
1039
  const dir = path9.dirname(path9.join(projectRoot, sourceRelPath));
921
1040
  const testAbsPath = path9.join(dir, testFilename);
922
- if (fs8.existsSync(testAbsPath)) return null;
1041
+ if (fs9.existsSync(testAbsPath)) return null;
923
1042
  return {
924
1043
  path: path9.relative(projectRoot, testAbsPath),
925
1044
  absPath: testAbsPath,
@@ -933,8 +1052,8 @@ function writeTestStub(stub, config) {
933
1052
  it.todo('add tests');
934
1053
  });
935
1054
  `;
936
- fs8.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
937
- fs8.writeFileSync(stub.absPath, content);
1055
+ fs9.mkdirSync(path9.dirname(stub.absPath), { recursive: true });
1056
+ fs9.writeFileSync(stub.absPath, content);
938
1057
  }
939
1058
 
940
1059
  // src/commands/fix.ts
@@ -947,7 +1066,7 @@ async function fixCommand(options, cwd) {
947
1066
  return 1;
948
1067
  }
949
1068
  const configPath = path10.join(projectRoot, CONFIG_FILE3);
950
- if (!fs9.existsSync(configPath)) {
1069
+ if (!fs10.existsSync(configPath)) {
951
1070
  console.error(
952
1071
  `${import_chalk4.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
953
1072
  );
@@ -1011,13 +1130,13 @@ async function fixCommand(options, cwd) {
1011
1130
  }
1012
1131
  let importUpdateCount = 0;
1013
1132
  if (renameCount > 0) {
1014
- const appliedRenames = dedupedRenames.filter((r) => fs9.existsSync(r.newAbsPath));
1133
+ const appliedRenames = dedupedRenames.filter((r) => fs10.existsSync(r.newAbsPath));
1015
1134
  const updates = await updateImportsAfterRenames(appliedRenames, projectRoot);
1016
1135
  importUpdateCount = updates.length;
1017
1136
  }
1018
1137
  let stubCount = 0;
1019
1138
  for (const stub of testStubs) {
1020
- if (!fs9.existsSync(stub.absPath)) {
1139
+ if (!fs10.existsSync(stub.absPath)) {
1021
1140
  writeTestStub(stub, config);
1022
1141
  stubCount++;
1023
1142
  }
@@ -1038,7 +1157,7 @@ async function fixCommand(options, cwd) {
1038
1157
  }
1039
1158
 
1040
1159
  // src/commands/init.ts
1041
- var fs12 = __toESM(require("fs"), 1);
1160
+ var fs13 = __toESM(require("fs"), 1);
1042
1161
  var path13 = __toESM(require("path"), 1);
1043
1162
  var clack2 = __toESM(require("@clack/prompts"), 1);
1044
1163
  var import_config4 = require("@viberails/config");
@@ -1460,7 +1579,7 @@ function formatScanResultsText(scanResult, config) {
1460
1579
  }
1461
1580
 
1462
1581
  // src/utils/write-generated-files.ts
1463
- var fs10 = __toESM(require("fs"), 1);
1582
+ var fs11 = __toESM(require("fs"), 1);
1464
1583
  var path11 = __toESM(require("path"), 1);
1465
1584
  var import_context = require("@viberails/context");
1466
1585
  var CONTEXT_DIR = ".viberails";
@@ -1469,12 +1588,12 @@ var SCAN_RESULT_FILE = "scan-result.json";
1469
1588
  function writeGeneratedFiles(projectRoot, config, scanResult) {
1470
1589
  const contextDir = path11.join(projectRoot, CONTEXT_DIR);
1471
1590
  try {
1472
- if (!fs10.existsSync(contextDir)) {
1473
- fs10.mkdirSync(contextDir, { recursive: true });
1591
+ if (!fs11.existsSync(contextDir)) {
1592
+ fs11.mkdirSync(contextDir, { recursive: true });
1474
1593
  }
1475
1594
  const context = (0, import_context.generateContext)(config);
1476
- fs10.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
1477
- fs10.writeFileSync(
1595
+ fs11.writeFileSync(path11.join(contextDir, CONTEXT_FILE), context);
1596
+ fs11.writeFileSync(
1478
1597
  path11.join(contextDir, SCAN_RESULT_FILE),
1479
1598
  `${JSON.stringify(scanResult, null, 2)}
1480
1599
  `
@@ -1486,27 +1605,28 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
1486
1605
  }
1487
1606
 
1488
1607
  // src/commands/init-hooks.ts
1489
- var fs11 = __toESM(require("fs"), 1);
1608
+ var fs12 = __toESM(require("fs"), 1);
1490
1609
  var path12 = __toESM(require("path"), 1);
1491
1610
  var import_chalk7 = __toESM(require("chalk"), 1);
1611
+ var import_yaml = require("yaml");
1492
1612
  function setupPreCommitHook(projectRoot) {
1493
1613
  const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1494
- if (fs11.existsSync(lefthookPath)) {
1614
+ if (fs12.existsSync(lefthookPath)) {
1495
1615
  addLefthookPreCommit(lefthookPath);
1496
1616
  console.log(` ${import_chalk7.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1497
1617
  return;
1498
1618
  }
1499
1619
  const huskyDir = path12.join(projectRoot, ".husky");
1500
- if (fs11.existsSync(huskyDir)) {
1620
+ if (fs12.existsSync(huskyDir)) {
1501
1621
  writeHuskyPreCommit(huskyDir);
1502
1622
  console.log(` ${import_chalk7.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1503
1623
  return;
1504
1624
  }
1505
1625
  const gitDir = path12.join(projectRoot, ".git");
1506
- if (fs11.existsSync(gitDir)) {
1626
+ if (fs12.existsSync(gitDir)) {
1507
1627
  const hooksDir = path12.join(gitDir, "hooks");
1508
- if (!fs11.existsSync(hooksDir)) {
1509
- fs11.mkdirSync(hooksDir, { recursive: true });
1628
+ if (!fs12.existsSync(hooksDir)) {
1629
+ fs12.mkdirSync(hooksDir, { recursive: true });
1510
1630
  }
1511
1631
  writeGitHookPreCommit(hooksDir);
1512
1632
  console.log(` ${import_chalk7.default.green("\u2713")} .git/hooks/pre-commit`);
@@ -1514,10 +1634,10 @@ function setupPreCommitHook(projectRoot) {
1514
1634
  }
1515
1635
  function writeGitHookPreCommit(hooksDir) {
1516
1636
  const hookPath = path12.join(hooksDir, "pre-commit");
1517
- if (fs11.existsSync(hookPath)) {
1518
- const existing = fs11.readFileSync(hookPath, "utf-8");
1637
+ if (fs12.existsSync(hookPath)) {
1638
+ const existing = fs12.readFileSync(hookPath, "utf-8");
1519
1639
  if (existing.includes("viberails")) return;
1520
- fs11.writeFileSync(
1640
+ fs12.writeFileSync(
1521
1641
  hookPath,
1522
1642
  `${existing.trimEnd()}
1523
1643
 
@@ -1534,71 +1654,51 @@ npx viberails check --staged
1534
1654
  "npx viberails check --staged",
1535
1655
  ""
1536
1656
  ].join("\n");
1537
- fs11.writeFileSync(hookPath, script, { mode: 493 });
1657
+ fs12.writeFileSync(hookPath, script, { mode: 493 });
1538
1658
  }
1539
1659
  function addLefthookPreCommit(lefthookPath) {
1540
- const content = fs11.readFileSync(lefthookPath, "utf-8");
1660
+ const content = fs12.readFileSync(lefthookPath, "utf-8");
1541
1661
  if (content.includes("viberails")) return;
1542
- const hasPreCommit = /^pre-commit:/m.test(content);
1543
- if (hasPreCommit) {
1544
- const commandBlock = ["", " viberails:", " run: npx viberails check --staged"].join(
1545
- "\n"
1546
- );
1547
- const updated = `${content.trimEnd()}
1548
- ${commandBlock}
1549
- `;
1550
- fs11.writeFileSync(lefthookPath, updated);
1551
- } else {
1552
- const section = [
1553
- "",
1554
- "pre-commit:",
1555
- " commands:",
1556
- " viberails:",
1557
- " run: npx viberails check --staged"
1558
- ].join("\n");
1559
- fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
1560
- ${section}
1561
- `);
1662
+ const doc = (0, import_yaml.parse)(content) ?? {};
1663
+ if (!doc["pre-commit"]) {
1664
+ doc["pre-commit"] = { commands: {} };
1665
+ }
1666
+ if (!doc["pre-commit"].commands) {
1667
+ doc["pre-commit"].commands = {};
1562
1668
  }
1669
+ doc["pre-commit"].commands.viberails = {
1670
+ run: "npx viberails check --staged"
1671
+ };
1672
+ fs12.writeFileSync(lefthookPath, (0, import_yaml.stringify)(doc));
1563
1673
  }
1564
1674
  function detectHookManager(projectRoot) {
1565
- if (fs11.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
1566
- if (fs11.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
1567
- if (fs11.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
1675
+ if (fs12.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
1676
+ if (fs12.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
1677
+ if (fs12.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
1568
1678
  return void 0;
1569
1679
  }
1570
1680
  function setupClaudeCodeHook(projectRoot) {
1571
1681
  const claudeDir = path12.join(projectRoot, ".claude");
1572
- if (!fs11.existsSync(claudeDir)) {
1573
- fs11.mkdirSync(claudeDir, { recursive: true });
1682
+ if (!fs12.existsSync(claudeDir)) {
1683
+ fs12.mkdirSync(claudeDir, { recursive: true });
1574
1684
  }
1575
1685
  const settingsPath = path12.join(claudeDir, "settings.json");
1576
1686
  let settings = {};
1577
- if (fs11.existsSync(settingsPath)) {
1687
+ if (fs12.existsSync(settingsPath)) {
1578
1688
  try {
1579
- settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
1689
+ settings = JSON.parse(fs12.readFileSync(settingsPath, "utf-8"));
1580
1690
  } catch {
1581
1691
  console.warn(
1582
- ` ${import_chalk7.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
1692
+ ` ${import_chalk7.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 skipping hook setup`
1583
1693
  );
1584
- settings = {};
1694
+ console.warn(` Fix the JSON manually, then re-run ${import_chalk7.default.cyan("viberails init --force")}`);
1695
+ return;
1585
1696
  }
1586
1697
  }
1587
1698
  const hooks = settings.hooks ?? {};
1588
1699
  const existing = hooks.PostToolUse ?? [];
1589
1700
  if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
1590
- const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
1591
- const checkAndReport = [
1592
- `FILE=$(${extractFile})`,
1593
- 'if [ -z "$FILE" ]; then exit 0; fi',
1594
- 'OUTPUT=$(npx viberails check --files "$FILE" --format json 2>&1)',
1595
- `if echo "$OUTPUT" | node -e "process.exit(JSON.parse(require('fs').readFileSync(0,'utf8')).violations?.length?0:1)" 2>/dev/null; then`,
1596
- ' echo "$OUTPUT" >&2',
1597
- " exit 2",
1598
- "fi",
1599
- "exit 0"
1600
- ].join("\n");
1601
- const hookCommand = checkAndReport;
1701
+ const hookCommand = "if [ -x ./node_modules/.bin/viberails ]; then ./node_modules/.bin/viberails check --hook; else npx viberails check --hook; fi";
1602
1702
  hooks.PostToolUse = [
1603
1703
  ...existing,
1604
1704
  {
@@ -1612,34 +1712,34 @@ function setupClaudeCodeHook(projectRoot) {
1612
1712
  }
1613
1713
  ];
1614
1714
  settings.hooks = hooks;
1615
- fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1715
+ fs12.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1616
1716
  `);
1617
1717
  console.log(` ${import_chalk7.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1618
1718
  }
1619
1719
  function setupClaudeMdReference(projectRoot) {
1620
1720
  const claudeMdPath = path12.join(projectRoot, "CLAUDE.md");
1621
1721
  let content = "";
1622
- if (fs11.existsSync(claudeMdPath)) {
1623
- content = fs11.readFileSync(claudeMdPath, "utf-8");
1722
+ if (fs12.existsSync(claudeMdPath)) {
1723
+ content = fs12.readFileSync(claudeMdPath, "utf-8");
1624
1724
  }
1625
1725
  if (content.includes("@.viberails/context.md")) return;
1626
1726
  const ref = "\n@.viberails/context.md\n";
1627
1727
  const prefix = content.length === 0 ? "" : content.trimEnd();
1628
- fs11.writeFileSync(claudeMdPath, prefix + ref);
1728
+ fs12.writeFileSync(claudeMdPath, prefix + ref);
1629
1729
  console.log(` ${import_chalk7.default.green("\u2713")} CLAUDE.md \u2014 added @.viberails/context.md reference`);
1630
1730
  }
1631
1731
  function writeHuskyPreCommit(huskyDir) {
1632
1732
  const hookPath = path12.join(huskyDir, "pre-commit");
1633
- if (fs11.existsSync(hookPath)) {
1634
- const existing = fs11.readFileSync(hookPath, "utf-8");
1733
+ if (fs12.existsSync(hookPath)) {
1734
+ const existing = fs12.readFileSync(hookPath, "utf-8");
1635
1735
  if (!existing.includes("viberails")) {
1636
- fs11.writeFileSync(hookPath, `${existing.trimEnd()}
1736
+ fs12.writeFileSync(hookPath, `${existing.trimEnd()}
1637
1737
  npx viberails check --staged
1638
1738
  `);
1639
1739
  }
1640
1740
  return;
1641
1741
  }
1642
- fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1742
+ fs12.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1643
1743
  }
1644
1744
 
1645
1745
  // src/commands/init.ts
@@ -1660,10 +1760,6 @@ function getConventionStr3(cv) {
1660
1760
  if (!cv) return void 0;
1661
1761
  return typeof cv === "string" ? cv : cv.value;
1662
1762
  }
1663
- function hasConventionOverrides(config) {
1664
- if (!config.packages || config.packages.length === 0) return false;
1665
- return config.packages.some((pkg) => pkg.conventions && Object.keys(pkg.conventions).length > 0);
1666
- }
1667
1763
  async function initCommand(options, cwd) {
1668
1764
  const startDir = cwd ?? process.cwd();
1669
1765
  const projectRoot = findProjectRoot(startDir);
@@ -1673,7 +1769,7 @@ async function initCommand(options, cwd) {
1673
1769
  );
1674
1770
  }
1675
1771
  const configPath = path13.join(projectRoot, CONFIG_FILE4);
1676
- if (fs12.existsSync(configPath) && !options.force) {
1772
+ if (fs13.existsSync(configPath) && !options.force) {
1677
1773
  console.log(
1678
1774
  `${import_chalk8.default.yellow("!")} viberails is already initialized.
1679
1775
  Run ${import_chalk8.default.cyan("viberails sync")} to update, or ${import_chalk8.default.cyan("viberails init --force")} to start fresh.`
@@ -1703,7 +1799,7 @@ async function initCommand(options, cwd) {
1703
1799
  console.log(` Inferred ${denyCount} boundary rules`);
1704
1800
  }
1705
1801
  }
1706
- fs12.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
1802
+ fs13.writeFileSync(configPath, `${JSON.stringify(config2, null, 2)}
1707
1803
  `);
1708
1804
  writeGeneratedFiles(projectRoot, config2, scanResult2);
1709
1805
  updateGitignore(projectRoot);
@@ -1730,27 +1826,18 @@ Created:`);
1730
1826
  clack2.note(resultsText, "Scan results");
1731
1827
  const decision = await promptInitDecision();
1732
1828
  if (decision === "customize") {
1733
- clack2.note(
1734
- "Rules control what viberails checks for.\nYou can change these later in viberails.config.json.",
1735
- "Rules"
1736
- );
1737
- const overrides = await promptRuleCustomization({
1829
+ const overrides = await promptRuleMenu({
1738
1830
  maxFileLines: config.rules.maxFileLines,
1739
1831
  requireTests: config.rules.requireTests,
1740
1832
  enforceNaming: config.rules.enforceNaming,
1741
1833
  enforcement: config.enforcement,
1742
- fileNamingValue: getConventionStr3(config.conventions.fileNaming)
1834
+ fileNamingValue: getConventionStr3(config.conventions.fileNaming),
1835
+ packageOverrides: config.packages
1743
1836
  });
1744
1837
  config.rules.maxFileLines = overrides.maxFileLines;
1745
1838
  config.rules.requireTests = overrides.requireTests;
1746
1839
  config.rules.enforceNaming = overrides.enforceNaming;
1747
1840
  config.enforcement = overrides.enforcement;
1748
- if (config.workspace?.packages && config.workspace.packages.length > 0) {
1749
- clack2.note(
1750
- 'These rules apply globally. To customize per package,\nedit the "packages" section in viberails.config.json.',
1751
- "Per-package overrides"
1752
- );
1753
- }
1754
1841
  }
1755
1842
  if (config.workspace?.packages && config.workspace.packages.length > 0) {
1756
1843
  clack2.note(
@@ -1780,13 +1867,7 @@ Created:`);
1780
1867
  }
1781
1868
  const hookManager = detectHookManager(projectRoot);
1782
1869
  const integrations = await promptIntegrations(hookManager);
1783
- if (hasConventionOverrides(config)) {
1784
- clack2.note(
1785
- "Some packages use different conventions. Per-package\noverrides have been saved in viberails.config.json \u2014\nreview and adjust as needed.",
1786
- "Per-package conventions"
1787
- );
1788
- }
1789
- fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1870
+ fs13.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1790
1871
  `);
1791
1872
  writeGeneratedFiles(projectRoot, config, scanResult);
1792
1873
  updateGitignore(projectRoot);
@@ -1797,8 +1878,7 @@ Created:`);
1797
1878
  ];
1798
1879
  if (integrations.preCommitHook) {
1799
1880
  setupPreCommitHook(projectRoot);
1800
- const hookMgr = detectHookManager(projectRoot);
1801
- if (hookMgr) {
1881
+ if (hookManager === "Lefthook") {
1802
1882
  createdFiles.push(`lefthook.yml \u2014 added viberails pre-commit`);
1803
1883
  }
1804
1884
  }
@@ -1817,19 +1897,19 @@ ${createdFiles.map((f) => ` ${f}`).join("\n")}`);
1817
1897
  function updateGitignore(projectRoot) {
1818
1898
  const gitignorePath = path13.join(projectRoot, ".gitignore");
1819
1899
  let content = "";
1820
- if (fs12.existsSync(gitignorePath)) {
1821
- content = fs12.readFileSync(gitignorePath, "utf-8");
1900
+ if (fs13.existsSync(gitignorePath)) {
1901
+ content = fs13.readFileSync(gitignorePath, "utf-8");
1822
1902
  }
1823
1903
  if (!content.includes(".viberails/scan-result.json")) {
1824
1904
  const block = "\n# viberails\n.viberails/scan-result.json\n";
1825
1905
  const prefix = content.length === 0 ? "" : `${content.trimEnd()}
1826
1906
  `;
1827
- fs12.writeFileSync(gitignorePath, `${prefix}${block}`);
1907
+ fs13.writeFileSync(gitignorePath, `${prefix}${block}`);
1828
1908
  }
1829
1909
  }
1830
1910
 
1831
1911
  // src/commands/sync.ts
1832
- var fs13 = __toESM(require("fs"), 1);
1912
+ var fs14 = __toESM(require("fs"), 1);
1833
1913
  var path14 = __toESM(require("path"), 1);
1834
1914
  var import_config5 = require("@viberails/config");
1835
1915
  var import_scanner2 = require("@viberails/scanner");
@@ -1964,7 +2044,7 @@ var SCAN_RESULT_FILE2 = ".viberails/scan-result.json";
1964
2044
  function loadPreviousStats(projectRoot) {
1965
2045
  const scanResultPath = path14.join(projectRoot, SCAN_RESULT_FILE2);
1966
2046
  try {
1967
- const raw = fs13.readFileSync(scanResultPath, "utf-8");
2047
+ const raw = fs14.readFileSync(scanResultPath, "utf-8");
1968
2048
  const parsed = JSON.parse(raw);
1969
2049
  if (parsed?.statistics?.totalFiles !== void 0) {
1970
2050
  return parsed.statistics;
@@ -2003,7 +2083,7 @@ ${import_chalk9.default.bold("Changes:")}`);
2003
2083
  console.log(` ${import_chalk9.default.dim(statsDelta)}`);
2004
2084
  }
2005
2085
  }
2006
- fs13.writeFileSync(configPath, `${mergedJson}
2086
+ fs14.writeFileSync(configPath, `${mergedJson}
2007
2087
  `);
2008
2088
  writeGeneratedFiles(projectRoot, merged, scanResult);
2009
2089
  console.log(`
@@ -2018,7 +2098,7 @@ ${import_chalk9.default.bold("Synced:")}`);
2018
2098
  }
2019
2099
 
2020
2100
  // src/index.ts
2021
- var VERSION = "0.3.3";
2101
+ var VERSION = "0.4.0";
2022
2102
  var program = new import_commander.Command();
2023
2103
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
2024
2104
  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) => {
@@ -2039,9 +2119,13 @@ program.command("sync").description("Re-scan and update generated files").action
2039
2119
  process.exit(1);
2040
2120
  }
2041
2121
  });
2042
- 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(
2122
+ 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(
2043
2123
  async (options) => {
2044
2124
  try {
2125
+ if (options.hook) {
2126
+ const exitCode2 = await hookCheckCommand();
2127
+ process.exit(exitCode2);
2128
+ }
2045
2129
  const exitCode = await checkCommand({
2046
2130
  ...options,
2047
2131
  noBoundaries: options.boundaries === false,