viberails 0.3.0 → 0.3.1

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
@@ -41,7 +41,7 @@ var import_commander = require("commander");
41
41
  var fs3 = __toESM(require("fs"), 1);
42
42
  var path3 = __toESM(require("path"), 1);
43
43
  var import_config = require("@viberails/config");
44
- var import_chalk = __toESM(require("chalk"), 1);
44
+ var import_chalk2 = __toESM(require("chalk"), 1);
45
45
 
46
46
  // src/utils/find-project-root.ts
47
47
  var fs = __toESM(require("fs"), 1);
@@ -62,6 +62,7 @@ function findProjectRoot(startDir) {
62
62
 
63
63
  // src/utils/prompt.ts
64
64
  var readline = __toESM(require("readline"), 1);
65
+ var import_chalk = __toESM(require("chalk"), 1);
65
66
  async function confirm(message) {
66
67
  const rl = readline.createInterface({
67
68
  input: process.stdin,
@@ -75,6 +76,29 @@ async function confirm(message) {
75
76
  });
76
77
  });
77
78
  }
79
+ async function selectIntegrations(hookManager) {
80
+ const result = { preCommitHook: true, claudeCodeHook: true };
81
+ const hookLabel = hookManager ? `Pre-commit hook (detected: ${hookManager})` : "Pre-commit hook (git hook)";
82
+ console.log("Set up integrations:");
83
+ const rl = readline.createInterface({
84
+ input: process.stdin,
85
+ output: process.stdout
86
+ });
87
+ const askYn = (label, defaultYes) => new Promise((resolve4) => {
88
+ const hint = defaultYes ? "Y/n" : "y/N";
89
+ rl.question(` ${label}? (${hint}) `, (answer) => {
90
+ const trimmed = answer.trim().toLowerCase();
91
+ if (trimmed === "") resolve4(defaultYes);
92
+ else resolve4(trimmed === "y" || trimmed === "yes");
93
+ });
94
+ });
95
+ console.log(` ${import_chalk.default.dim("Runs viberails check automatically when you commit")}`);
96
+ result.preCommitHook = await askYn(hookLabel, true);
97
+ console.log(` ${import_chalk.default.dim("Checks files against your rules when Claude edits them")}`);
98
+ result.claudeCodeHook = await askYn("Claude Code hook", true);
99
+ rl.close();
100
+ return result;
101
+ }
78
102
 
79
103
  // src/utils/resolve-workspace-packages.ts
80
104
  var fs2 = __toESM(require("fs"), 1);
@@ -99,7 +123,7 @@ function resolveWorkspacePackages(projectRoot, workspace) {
99
123
  ];
100
124
  packages.push({ name, path: absPath, relativePath, internalDeps: allDeps });
101
125
  }
102
- const packageNames = new Set(packages.map((p3) => p3.name));
126
+ const packageNames = new Set(packages.map((p) => p.name));
103
127
  for (const pkg of packages) {
104
128
  pkg.internalDeps = pkg.internalDeps.filter((dep) => packageNames.has(dep));
105
129
  }
@@ -129,80 +153,68 @@ async function boundariesCommand(options, cwd) {
129
153
  }
130
154
  displayRules(config);
131
155
  }
132
- function countBoundaries(boundaries) {
133
- if (!boundaries) return 0;
134
- if (Array.isArray(boundaries)) return boundaries.length;
135
- return Object.values(boundaries).reduce((sum, denied) => sum + denied.length, 0);
136
- }
137
156
  function displayRules(config) {
138
- const total = countBoundaries(config.boundaries);
139
- if (total === 0) {
140
- console.log(import_chalk.default.yellow("No boundary rules configured."));
141
- console.log(`Run ${import_chalk.default.cyan("viberails boundaries --infer")} to generate rules.`);
157
+ if (!config.boundaries || config.boundaries.length === 0) {
158
+ console.log(import_chalk2.default.yellow("No boundary rules configured."));
159
+ console.log(`Run ${import_chalk2.default.cyan("viberails boundaries --infer")} to generate rules.`);
142
160
  return;
143
161
  }
162
+ const allowRules = config.boundaries.filter((r) => r.allow);
163
+ const denyRules = config.boundaries.filter((r) => !r.allow);
144
164
  console.log(`
145
- ${import_chalk.default.bold(`Boundary rules (${total} rules):`)}
165
+ ${import_chalk2.default.bold(`Boundary rules (${config.boundaries.length} rules):`)}
146
166
  `);
147
- if (Array.isArray(config.boundaries)) {
148
- const allowRules = config.boundaries.filter((r) => r.allow);
149
- const denyRules = config.boundaries.filter((r) => !r.allow);
150
- for (const r of allowRules) {
151
- console.log(` ${import_chalk.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
152
- }
153
- for (const r of denyRules) {
154
- const reason = r.reason ? import_chalk.default.dim(` (${r.reason})`) : "";
155
- console.log(` ${import_chalk.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
156
- }
157
- } else if (config.boundaries) {
158
- for (const [from, denied] of Object.entries(config.boundaries)) {
159
- for (const to of denied) {
160
- console.log(` ${import_chalk.default.red("\u2717")} ${from} \u2192 ${to}`);
161
- }
162
- }
167
+ for (const r of allowRules) {
168
+ console.log(` ${import_chalk2.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
169
+ }
170
+ for (const r of denyRules) {
171
+ const reason = r.reason ? import_chalk2.default.dim(` (${r.reason})`) : "";
172
+ console.log(` ${import_chalk2.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
163
173
  }
164
174
  console.log(
165
175
  `
166
- Enforcement: ${config.rules.enforceBoundaries ? import_chalk.default.green("on") : import_chalk.default.yellow("off")}`
176
+ Enforcement: ${config.rules.enforceBoundaries ? import_chalk2.default.green("on") : import_chalk2.default.yellow("off")}`
167
177
  );
168
178
  }
169
179
  async function inferAndDisplay(projectRoot, config, configPath) {
170
- console.log(import_chalk.default.dim("Analyzing imports..."));
180
+ console.log(import_chalk2.default.dim("Analyzing imports..."));
171
181
  const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
172
182
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
173
183
  const graph = await buildImportGraph(projectRoot, {
174
184
  packages,
175
185
  ignore: config.ignore
176
186
  });
177
- console.log(import_chalk.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
187
+ console.log(import_chalk2.default.dim(`${graph.nodes.length} files, ${graph.edges.length} edges`));
178
188
  const inferred = inferBoundaries(graph);
179
- const entries = Object.entries(inferred);
180
- if (entries.length === 0) {
181
- console.log(import_chalk.default.yellow("No boundary rules could be inferred."));
189
+ if (inferred.length === 0) {
190
+ console.log(import_chalk2.default.yellow("No boundary rules could be inferred."));
182
191
  return;
183
192
  }
184
- const totalRules = entries.reduce((sum, [, denied]) => sum + denied.length, 0);
193
+ const allow = inferred.filter((r) => r.allow);
194
+ const deny = inferred.filter((r) => !r.allow);
185
195
  console.log(`
186
- ${import_chalk.default.bold("Inferred boundary rules:")}
196
+ ${import_chalk2.default.bold("Inferred boundary rules:")}
187
197
  `);
188
- for (const [from, denied] of entries) {
189
- for (const to of denied) {
190
- console.log(` ${import_chalk.default.red("\u2717")} ${from} \u2192 ${to}`);
191
- }
198
+ for (const r of allow) {
199
+ console.log(` ${import_chalk2.default.green("\u2713")} ${r.from} \u2192 ${r.to}`);
200
+ }
201
+ for (const r of deny) {
202
+ const reason = r.reason ? import_chalk2.default.dim(` (${r.reason})`) : "";
203
+ console.log(` ${import_chalk2.default.red("\u2717")} ${r.from} \u2192 ${r.to}${reason}`);
192
204
  }
193
205
  console.log(`
194
- ${totalRules} deny rules`);
206
+ ${allow.length} allowed, ${deny.length} denied`);
195
207
  const shouldSave = await confirm("\nSave to viberails.config.json?");
196
208
  if (shouldSave) {
197
209
  config.boundaries = inferred;
198
210
  config.rules.enforceBoundaries = true;
199
211
  fs3.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
200
212
  `);
201
- console.log(`${import_chalk.default.green("\u2713")} Saved ${totalRules} rules`);
213
+ console.log(`${import_chalk2.default.green("\u2713")} Saved ${inferred.length} rules`);
202
214
  }
203
215
  }
204
216
  async function showGraph(projectRoot, config) {
205
- console.log(import_chalk.default.dim("Building import graph..."));
217
+ console.log(import_chalk2.default.dim("Building import graph..."));
206
218
  const { buildImportGraph } = await import("@viberails/graph");
207
219
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
208
220
  const graph = await buildImportGraph(projectRoot, {
@@ -210,20 +222,20 @@ async function showGraph(projectRoot, config) {
210
222
  ignore: config.ignore
211
223
  });
212
224
  console.log(`
213
- ${import_chalk.default.bold("Import dependency graph:")}
225
+ ${import_chalk2.default.bold("Import dependency graph:")}
214
226
  `);
215
227
  console.log(` ${graph.nodes.length} files, ${graph.edges.length} imports
216
228
  `);
217
229
  if (graph.packages.length > 0) {
218
230
  for (const pkg of graph.packages) {
219
231
  const deps = pkg.internalDeps.length > 0 ? `
220
- ${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` : import_chalk.default.dim(" (no internal deps)");
232
+ ${pkg.internalDeps.map((d) => ` \u2192 ${d}`).join("\n")}` : import_chalk2.default.dim(" (no internal deps)");
221
233
  console.log(` ${pkg.name}${deps}`);
222
234
  }
223
235
  }
224
236
  if (graph.cycles.length > 0) {
225
237
  console.log(`
226
- ${import_chalk.default.yellow("Cycles detected:")}`);
238
+ ${import_chalk2.default.yellow("Cycles detected:")}`);
227
239
  for (const cycle of graph.cycles) {
228
240
  const paths = cycle.map((f) => path3.relative(projectRoot, f));
229
241
  console.log(` ${paths.join(" \u2192 ")}`);
@@ -235,7 +247,7 @@ ${import_chalk.default.yellow("Cycles detected:")}`);
235
247
  var fs6 = __toESM(require("fs"), 1);
236
248
  var path6 = __toESM(require("path"), 1);
237
249
  var import_config2 = require("@viberails/config");
238
- var import_chalk2 = __toESM(require("chalk"), 1);
250
+ var import_chalk3 = __toESM(require("chalk"), 1);
239
251
 
240
252
  // src/commands/check-config.ts
241
253
  function resolveConfigForFile(relPath, config) {
@@ -268,6 +280,7 @@ function resolveIgnoreForFile(relPath, config) {
268
280
  var import_node_child_process = require("child_process");
269
281
  var fs4 = __toESM(require("fs"), 1);
270
282
  var path4 = __toESM(require("path"), 1);
283
+ var import_picomatch = __toESM(require("picomatch"), 1);
271
284
  var ALWAYS_SKIP_DIRS = /* @__PURE__ */ new Set([
272
285
  "node_modules",
273
286
  ".git",
@@ -303,25 +316,9 @@ var NAMING_PATTERNS = {
303
316
  snake_case: /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/
304
317
  };
305
318
  function isIgnored(relPath, ignorePatterns) {
306
- for (const pattern of ignorePatterns) {
307
- const startsGlob = pattern.startsWith("**/");
308
- const endsGlob = pattern.endsWith("/**");
309
- if (startsGlob && endsGlob) {
310
- const middle = pattern.slice(3, -3);
311
- if (relPath.startsWith(`${middle}/`) || relPath.includes(`/${middle}/`) || relPath === middle) {
312
- return true;
313
- }
314
- } else if (endsGlob) {
315
- const prefix = pattern.slice(0, -3);
316
- if (relPath.startsWith(`${prefix}/`) || relPath === prefix) return true;
317
- } else if (startsGlob) {
318
- const suffix = pattern.slice(3);
319
- if (relPath.endsWith(suffix) || relPath === suffix) return true;
320
- } else if (relPath === pattern || relPath.startsWith(`${pattern}/`)) {
321
- return true;
322
- }
323
- }
324
- return false;
319
+ if (ignorePatterns.length === 0) return false;
320
+ const isMatch = (0, import_picomatch.default)(ignorePatterns, { dot: true });
321
+ return isMatch(relPath);
325
322
  }
326
323
  function countFileLines(filePath) {
327
324
  try {
@@ -488,12 +485,12 @@ function printGroupedViolations(violations, limit) {
488
485
  const toShow = group.slice(0, remaining);
489
486
  const hidden = group.length - toShow.length;
490
487
  for (const v of toShow) {
491
- const icon = v.severity === "error" ? import_chalk2.default.red("\u2717") : import_chalk2.default.yellow("!");
492
- console.log(`${icon} ${import_chalk2.default.dim(v.rule)} ${v.file}: ${v.message}`);
488
+ const icon = v.severity === "error" ? import_chalk3.default.red("\u2717") : import_chalk3.default.yellow("!");
489
+ console.log(`${icon} ${import_chalk3.default.dim(v.rule)} ${v.file}: ${v.message}`);
493
490
  }
494
491
  totalShown += toShow.length;
495
492
  if (hidden > 0) {
496
- console.log(import_chalk2.default.dim(` ... and ${hidden} more ${rule} violations`));
493
+ console.log(import_chalk3.default.dim(` ... and ${hidden} more ${rule} violations`));
497
494
  }
498
495
  }
499
496
  }
@@ -511,13 +508,13 @@ async function checkCommand(options, cwd) {
511
508
  const startDir = cwd ?? process.cwd();
512
509
  const projectRoot = findProjectRoot(startDir);
513
510
  if (!projectRoot) {
514
- console.error(`${import_chalk2.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
511
+ console.error(`${import_chalk3.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
515
512
  return 1;
516
513
  }
517
514
  const configPath = path6.join(projectRoot, CONFIG_FILE2);
518
515
  if (!fs6.existsSync(configPath)) {
519
516
  console.error(
520
- `${import_chalk2.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
517
+ `${import_chalk3.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
521
518
  );
522
519
  return 1;
523
520
  }
@@ -531,7 +528,7 @@ async function checkCommand(options, cwd) {
531
528
  filesToCheck = getAllSourceFiles(projectRoot, config);
532
529
  }
533
530
  if (filesToCheck.length === 0) {
534
- console.log(`${import_chalk2.default.green("\u2713")} No files to check.`);
531
+ console.log(`${import_chalk3.default.green("\u2713")} No files to check.`);
535
532
  return 0;
536
533
  }
537
534
  const violations = [];
@@ -572,8 +569,7 @@ async function checkCommand(options, cwd) {
572
569
  const testViolations = checkMissingTests(projectRoot, config, severity);
573
570
  violations.push(...testViolations);
574
571
  }
575
- const hasBoundaries = config.boundaries ? Array.isArray(config.boundaries) ? config.boundaries.length > 0 : Object.keys(config.boundaries).length > 0 : false;
576
- if (config.rules.enforceBoundaries && hasBoundaries && !options.noBoundaries) {
572
+ if (config.rules.enforceBoundaries && config.boundaries && config.boundaries.length > 0 && !options.noBoundaries) {
577
573
  const startTime = Date.now();
578
574
  const { buildImportGraph, checkBoundaries } = await import("@viberails/graph");
579
575
  const packages = config.workspace ? resolveWorkspacePackages(projectRoot, config.workspace) : void 0;
@@ -594,10 +590,20 @@ async function checkCommand(options, cwd) {
594
590
  });
595
591
  }
596
592
  const elapsed = Date.now() - startTime;
597
- console.log(import_chalk2.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
593
+ console.log(import_chalk3.default.dim(` Boundary check: ${graph.nodes.length} files in ${elapsed}ms`));
594
+ }
595
+ if (options.format === "json") {
596
+ console.log(
597
+ JSON.stringify({
598
+ violations,
599
+ checkedFiles: filesToCheck.length,
600
+ enforcement: config.enforcement
601
+ })
602
+ );
603
+ return config.enforcement === "enforce" && violations.length > 0 ? 1 : 0;
598
604
  }
599
605
  if (violations.length === 0) {
600
- console.log(`${import_chalk2.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
606
+ console.log(`${import_chalk3.default.green("\u2713")} ${filesToCheck.length} files checked \u2014 no violations`);
601
607
  return 0;
602
608
  }
603
609
  if (!options.quiet) {
@@ -605,7 +611,7 @@ async function checkCommand(options, cwd) {
605
611
  }
606
612
  printSummary(violations);
607
613
  if (config.enforcement === "enforce") {
608
- console.log(import_chalk2.default.red("Fix violations before committing."));
614
+ console.log(import_chalk3.default.red("Fix violations before committing."));
609
615
  return 1;
610
616
  }
611
617
  return 0;
@@ -615,23 +621,23 @@ async function checkCommand(options, cwd) {
615
621
  var fs9 = __toESM(require("fs"), 1);
616
622
  var path10 = __toESM(require("path"), 1);
617
623
  var import_config3 = require("@viberails/config");
618
- var import_chalk4 = __toESM(require("chalk"), 1);
624
+ var import_chalk5 = __toESM(require("chalk"), 1);
619
625
 
620
626
  // src/commands/fix-helpers.ts
621
627
  var import_node_child_process2 = require("child_process");
622
628
  var import_node_readline = require("readline");
623
- var import_chalk3 = __toESM(require("chalk"), 1);
629
+ var import_chalk4 = __toESM(require("chalk"), 1);
624
630
  function printPlan(renames, stubs) {
625
631
  if (renames.length > 0) {
626
- console.log(import_chalk3.default.bold("\nFile renames:"));
632
+ console.log(import_chalk4.default.bold("\nFile renames:"));
627
633
  for (const r of renames) {
628
- console.log(` ${import_chalk3.default.red(r.oldPath)} \u2192 ${import_chalk3.default.green(r.newPath)}`);
634
+ console.log(` ${import_chalk4.default.red(r.oldPath)} \u2192 ${import_chalk4.default.green(r.newPath)}`);
629
635
  }
630
636
  }
631
637
  if (stubs.length > 0) {
632
- console.log(import_chalk3.default.bold("\nTest stubs to create:"));
638
+ console.log(import_chalk4.default.bold("\nTest stubs to create:"));
633
639
  for (const s of stubs) {
634
- console.log(` ${import_chalk3.default.green("+")} ${s.path}`);
640
+ console.log(` ${import_chalk4.default.green("+")} ${s.path}`);
635
641
  }
636
642
  }
637
643
  }
@@ -695,7 +701,8 @@ async function updateImportsAfterRenames(renames, projectRoot) {
695
701
  const extensions = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
696
702
  for (const sourceFile of project.getSourceFiles()) {
697
703
  const filePath = sourceFile.getFilePath();
698
- if (filePath.includes("/node_modules/") || filePath.includes("/dist/")) continue;
704
+ const segments = filePath.split(path7.sep);
705
+ if (segments.includes("node_modules") || segments.includes("dist")) continue;
699
706
  const fileDir = path7.dirname(filePath);
700
707
  for (const decl of sourceFile.getImportDeclarations()) {
701
708
  const specifier = decl.getModuleSpecifierValue();
@@ -881,13 +888,13 @@ async function fixCommand(options, cwd) {
881
888
  const startDir = cwd ?? process.cwd();
882
889
  const projectRoot = findProjectRoot(startDir);
883
890
  if (!projectRoot) {
884
- console.error(`${import_chalk4.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
891
+ console.error(`${import_chalk5.default.red("Error:")} No package.json found. Are you in a JS/TS project?`);
885
892
  return 1;
886
893
  }
887
894
  const configPath = path10.join(projectRoot, CONFIG_FILE3);
888
895
  if (!fs9.existsSync(configPath)) {
889
896
  console.error(
890
- `${import_chalk4.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
897
+ `${import_chalk5.default.red("Error:")} No viberails.config.json found. Run \`viberails init\` first.`
891
898
  );
892
899
  return 1;
893
900
  }
@@ -896,7 +903,7 @@ async function fixCommand(options, cwd) {
896
903
  const isDirty = checkGitDirty(projectRoot);
897
904
  if (isDirty) {
898
905
  console.log(
899
- import_chalk4.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
906
+ import_chalk5.default.yellow("Warning: You have uncommitted changes. Consider committing first.")
900
907
  );
901
908
  }
902
909
  }
@@ -926,12 +933,12 @@ async function fixCommand(options, cwd) {
926
933
  }
927
934
  }
928
935
  if (dedupedRenames.length === 0 && testStubs.length === 0) {
929
- console.log(`${import_chalk4.default.green("\u2713")} No fixable violations found.`);
936
+ console.log(`${import_chalk5.default.green("\u2713")} No fixable violations found.`);
930
937
  return 0;
931
938
  }
932
939
  printPlan(dedupedRenames, testStubs);
933
940
  if (options.dryRun) {
934
- console.log(import_chalk4.default.dim("\nDry run \u2014 no changes applied."));
941
+ console.log(import_chalk5.default.dim("\nDry run \u2014 no changes applied."));
935
942
  return 0;
936
943
  }
937
944
  if (!options.yes) {
@@ -962,15 +969,15 @@ async function fixCommand(options, cwd) {
962
969
  }
963
970
  console.log("");
964
971
  if (renameCount > 0) {
965
- console.log(`${import_chalk4.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
972
+ console.log(`${import_chalk5.default.green("\u2713")} Renamed ${renameCount} file${renameCount > 1 ? "s" : ""}`);
966
973
  }
967
974
  if (importUpdateCount > 0) {
968
975
  console.log(
969
- `${import_chalk4.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
976
+ `${import_chalk5.default.green("\u2713")} Updated ${importUpdateCount} import${importUpdateCount > 1 ? "s" : ""}`
970
977
  );
971
978
  }
972
979
  if (stubCount > 0) {
973
- console.log(`${import_chalk4.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
980
+ console.log(`${import_chalk5.default.green("\u2713")} Generated ${stubCount} test stub${stubCount > 1 ? "s" : ""}`);
974
981
  }
975
982
  return 0;
976
983
  }
@@ -978,11 +985,262 @@ async function fixCommand(options, cwd) {
978
985
  // src/commands/init.ts
979
986
  var fs12 = __toESM(require("fs"), 1);
980
987
  var path13 = __toESM(require("path"), 1);
981
- var p2 = __toESM(require("@clack/prompts"), 1);
982
988
  var import_config4 = require("@viberails/config");
983
989
  var import_scanner = require("@viberails/scanner");
984
990
  var import_chalk9 = __toESM(require("chalk"), 1);
985
991
 
992
+ // src/display.ts
993
+ var import_types3 = require("@viberails/types");
994
+ var import_chalk7 = __toESM(require("chalk"), 1);
995
+
996
+ // src/display-helpers.ts
997
+ var import_types = require("@viberails/types");
998
+ function groupByRole(directories) {
999
+ const map = /* @__PURE__ */ new Map();
1000
+ for (const dir of directories) {
1001
+ if (dir.role === "unknown") continue;
1002
+ const existing = map.get(dir.role);
1003
+ if (existing) {
1004
+ existing.dirs.push(dir);
1005
+ } else {
1006
+ map.set(dir.role, { dirs: [dir] });
1007
+ }
1008
+ }
1009
+ const groups = [];
1010
+ for (const [role, { dirs }] of map) {
1011
+ const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
1012
+ const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1013
+ groups.push({
1014
+ role,
1015
+ label,
1016
+ dirCount: dirs.length,
1017
+ totalFiles,
1018
+ singlePath: dirs.length === 1 ? dirs[0].path : void 0
1019
+ });
1020
+ }
1021
+ return groups;
1022
+ }
1023
+ function formatSummary(stats, packageCount) {
1024
+ const parts = [];
1025
+ if (packageCount && packageCount > 1) {
1026
+ parts.push(`${packageCount} packages`);
1027
+ }
1028
+ parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1029
+ parts.push(`${stats.totalLines.toLocaleString()} lines`);
1030
+ parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1031
+ return parts.join(" \xB7 ");
1032
+ }
1033
+ function formatExtensions(filesByExtension, maxEntries = 4) {
1034
+ return Object.entries(filesByExtension).sort(([, a], [, b]) => b - a).slice(0, maxEntries).map(([ext, count]) => `${ext} ${count}`).join(" \xB7 ");
1035
+ }
1036
+ function formatRoleGroup(group) {
1037
+ const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1038
+ if (group.singlePath) {
1039
+ return `${group.label} \u2014 ${group.singlePath} (${files})`;
1040
+ }
1041
+ const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1042
+ return `${group.label} \u2014 ${dirs} (${files})`;
1043
+ }
1044
+
1045
+ // src/display-monorepo.ts
1046
+ var import_types2 = require("@viberails/types");
1047
+ var import_chalk6 = __toESM(require("chalk"), 1);
1048
+ function formatPackageSummary(pkg) {
1049
+ const parts = [];
1050
+ if (pkg.stack.framework) {
1051
+ parts.push(formatItem(pkg.stack.framework, import_types2.FRAMEWORK_NAMES));
1052
+ }
1053
+ if (pkg.stack.styling) {
1054
+ parts.push(formatItem(pkg.stack.styling, import_types2.STYLING_NAMES));
1055
+ }
1056
+ const files = `${pkg.statistics.totalFiles} files`;
1057
+ const detail = parts.length > 0 ? `${parts.join(", ")} (${files})` : `(${files})`;
1058
+ return ` ${pkg.relativePath} \u2014 ${detail}`;
1059
+ }
1060
+ function displayMonorepoResults(scanResult) {
1061
+ const { stack, packages } = scanResult;
1062
+ console.log(`
1063
+ ${import_chalk6.default.bold(`Detected: (monorepo, ${packages.length} packages)`)}`);
1064
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.language)}`);
1065
+ if (stack.packageManager) {
1066
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1067
+ }
1068
+ if (stack.linter) {
1069
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.linter)}`);
1070
+ }
1071
+ if (stack.formatter) {
1072
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1073
+ }
1074
+ if (stack.testRunner) {
1075
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1076
+ }
1077
+ console.log("");
1078
+ for (const pkg of packages) {
1079
+ console.log(formatPackageSummary(pkg));
1080
+ }
1081
+ const packagesWithDirs = packages.filter(
1082
+ (pkg) => pkg.structure.directories.some((d) => d.role !== "unknown")
1083
+ );
1084
+ if (packagesWithDirs.length > 0) {
1085
+ console.log(`
1086
+ ${import_chalk6.default.bold("Structure:")}`);
1087
+ for (const pkg of packagesWithDirs) {
1088
+ const groups = groupByRole(pkg.structure.directories);
1089
+ if (groups.length === 0) continue;
1090
+ console.log(` ${pkg.relativePath}:`);
1091
+ for (const group of groups) {
1092
+ console.log(` ${import_chalk6.default.green("\u2713")} ${formatRoleGroup(group)}`);
1093
+ }
1094
+ }
1095
+ }
1096
+ displayConventions(scanResult);
1097
+ displaySummarySection(scanResult);
1098
+ console.log("");
1099
+ }
1100
+
1101
+ // src/display.ts
1102
+ var CONVENTION_LABELS = {
1103
+ fileNaming: "File naming",
1104
+ componentNaming: "Component naming",
1105
+ hookNaming: "Hook naming",
1106
+ importAlias: "Import alias"
1107
+ };
1108
+ function formatItem(item, nameMap) {
1109
+ const name = nameMap?.[item.name] ?? item.name;
1110
+ return item.version ? `${name} ${item.version}` : name;
1111
+ }
1112
+ function confidenceLabel(convention) {
1113
+ const pct = Math.round(convention.consistency);
1114
+ if (convention.confidence === "high") {
1115
+ return `${pct}% \u2014 high confidence, will enforce`;
1116
+ }
1117
+ return `${pct}% \u2014 medium confidence, suggested only`;
1118
+ }
1119
+ function displayConventions(scanResult) {
1120
+ const conventionEntries = Object.entries(scanResult.conventions);
1121
+ if (conventionEntries.length === 0) return;
1122
+ console.log(`
1123
+ ${import_chalk7.default.bold("Conventions:")}`);
1124
+ for (const [key, convention] of conventionEntries) {
1125
+ if (convention.confidence === "low") continue;
1126
+ const label = CONVENTION_LABELS[key] ?? key;
1127
+ if (scanResult.packages.length > 1) {
1128
+ const pkgValues = scanResult.packages.filter((pkg) => pkg.conventions[key] && pkg.conventions[key].confidence !== "low").map((pkg) => ({ relativePath: pkg.relativePath, convention: pkg.conventions[key] }));
1129
+ const allSame = pkgValues.every((pv) => pv.convention.value === convention.value);
1130
+ if (allSame || pkgValues.length <= 1) {
1131
+ const ind = convention.confidence === "high" ? import_chalk7.default.green("\u2713") : import_chalk7.default.yellow("~");
1132
+ const detail = import_chalk7.default.dim(`(${confidenceLabel(convention)})`);
1133
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1134
+ } else {
1135
+ console.log(` ${import_chalk7.default.yellow("~")} ${label}: varies by package`);
1136
+ for (const pv of pkgValues) {
1137
+ const pct = Math.round(pv.convention.consistency);
1138
+ console.log(` ${pv.relativePath}: ${pv.convention.value} (${pct}%)`);
1139
+ }
1140
+ }
1141
+ } else {
1142
+ const ind = convention.confidence === "high" ? import_chalk7.default.green("\u2713") : import_chalk7.default.yellow("~");
1143
+ const detail = import_chalk7.default.dim(`(${confidenceLabel(convention)})`);
1144
+ console.log(` ${ind} ${label}: ${convention.value} ${detail}`);
1145
+ }
1146
+ }
1147
+ }
1148
+ function displaySummarySection(scanResult) {
1149
+ const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1150
+ console.log(`
1151
+ ${import_chalk7.default.bold("Summary:")}`);
1152
+ console.log(` ${formatSummary(scanResult.statistics, pkgCount)}`);
1153
+ const ext = formatExtensions(scanResult.statistics.filesByExtension);
1154
+ if (ext) {
1155
+ console.log(` ${ext}`);
1156
+ }
1157
+ }
1158
+ function displayScanResults(scanResult) {
1159
+ if (scanResult.packages.length > 1) {
1160
+ displayMonorepoResults(scanResult);
1161
+ return;
1162
+ }
1163
+ const { stack } = scanResult;
1164
+ console.log(`
1165
+ ${import_chalk7.default.bold("Detected:")}`);
1166
+ if (stack.framework) {
1167
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.framework, import_types3.FRAMEWORK_NAMES)}`);
1168
+ }
1169
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.language)}`);
1170
+ if (stack.styling) {
1171
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.styling, import_types3.STYLING_NAMES)}`);
1172
+ }
1173
+ if (stack.backend) {
1174
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.backend, import_types3.FRAMEWORK_NAMES)}`);
1175
+ }
1176
+ if (stack.orm) {
1177
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.orm, import_types3.ORM_NAMES)}`);
1178
+ }
1179
+ if (stack.linter) {
1180
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.linter)}`);
1181
+ }
1182
+ if (stack.formatter) {
1183
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.formatter)}`);
1184
+ }
1185
+ if (stack.testRunner) {
1186
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.testRunner)}`);
1187
+ }
1188
+ if (stack.packageManager) {
1189
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(stack.packageManager)}`);
1190
+ }
1191
+ if (stack.libraries.length > 0) {
1192
+ for (const lib of stack.libraries) {
1193
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatItem(lib, import_types3.LIBRARY_NAMES)}`);
1194
+ }
1195
+ }
1196
+ const groups = groupByRole(scanResult.structure.directories);
1197
+ if (groups.length > 0) {
1198
+ console.log(`
1199
+ ${import_chalk7.default.bold("Structure:")}`);
1200
+ for (const group of groups) {
1201
+ console.log(` ${import_chalk7.default.green("\u2713")} ${formatRoleGroup(group)}`);
1202
+ }
1203
+ }
1204
+ displayConventions(scanResult);
1205
+ displaySummarySection(scanResult);
1206
+ console.log("");
1207
+ }
1208
+ function getConventionStr(cv) {
1209
+ return typeof cv === "string" ? cv : cv.value;
1210
+ }
1211
+ function displayRulesPreview(config) {
1212
+ console.log(`${import_chalk7.default.bold("Rules:")}`);
1213
+ console.log(` ${import_chalk7.default.dim("\u2022")} Max file size: ${config.rules.maxFileLines} lines`);
1214
+ if (config.rules.requireTests && config.structure.testPattern) {
1215
+ console.log(
1216
+ ` ${import_chalk7.default.dim("\u2022")} Require test files: yes (${config.structure.testPattern})`
1217
+ );
1218
+ } else if (config.rules.requireTests) {
1219
+ console.log(` ${import_chalk7.default.dim("\u2022")} Require test files: yes`);
1220
+ } else {
1221
+ console.log(` ${import_chalk7.default.dim("\u2022")} Require test files: no`);
1222
+ }
1223
+ if (config.rules.enforceNaming && config.conventions.fileNaming) {
1224
+ console.log(
1225
+ ` ${import_chalk7.default.dim("\u2022")} Enforce file naming: ${getConventionStr(config.conventions.fileNaming)}`
1226
+ );
1227
+ } else {
1228
+ console.log(` ${import_chalk7.default.dim("\u2022")} Enforce file naming: no`);
1229
+ }
1230
+ console.log(
1231
+ ` ${import_chalk7.default.dim("\u2022")} Enforce boundaries: ${config.rules.enforceBoundaries ? "yes" : "no"}`
1232
+ );
1233
+ console.log("");
1234
+ if (config.enforcement === "enforce") {
1235
+ console.log(`${import_chalk7.default.bold("Enforcement mode:")} enforce (violations will block commits)`);
1236
+ } else {
1237
+ console.log(
1238
+ `${import_chalk7.default.bold("Enforcement mode:")} warn (violations shown but won't block commits)`
1239
+ );
1240
+ }
1241
+ console.log("");
1242
+ }
1243
+
986
1244
  // src/utils/write-generated-files.ts
987
1245
  var fs10 = __toESM(require("fs"), 1);
988
1246
  var path11 = __toESM(require("path"), 1);
@@ -1012,60 +1270,18 @@ function writeGeneratedFiles(projectRoot, config, scanResult) {
1012
1270
  // src/commands/init-hooks.ts
1013
1271
  var fs11 = __toESM(require("fs"), 1);
1014
1272
  var path12 = __toESM(require("path"), 1);
1015
- var import_chalk5 = __toESM(require("chalk"), 1);
1016
- function setupClaudeCodeHook(projectRoot) {
1017
- const claudeDir = path12.join(projectRoot, ".claude");
1018
- if (!fs11.existsSync(claudeDir)) {
1019
- fs11.mkdirSync(claudeDir, { recursive: true });
1020
- }
1021
- const settingsPath = path12.join(claudeDir, "settings.json");
1022
- let settings = {};
1023
- if (fs11.existsSync(settingsPath)) {
1024
- try {
1025
- settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
1026
- } catch {
1027
- }
1028
- }
1029
- const hooks = settings.hooks ?? {};
1030
- const postToolUse = hooks.PostToolUse;
1031
- if (Array.isArray(postToolUse)) {
1032
- const hasViberails = postToolUse.some(
1033
- (entry) => typeof entry === "object" && entry !== null && Array.isArray(entry.hooks) && entry.hooks.some(
1034
- (h) => typeof h === "object" && h !== null && typeof h.command === "string" && h.command.includes("viberails")
1035
- )
1036
- );
1037
- if (hasViberails) return;
1038
- }
1039
- const viberailsHook = {
1040
- matcher: "Edit|Write",
1041
- hooks: [
1042
- {
1043
- type: "command",
1044
- command: "jq -r '.tool_input.file_path' | xargs npx viberails check --files"
1045
- }
1046
- ]
1047
- };
1048
- if (!hooks.PostToolUse) {
1049
- hooks.PostToolUse = [viberailsHook];
1050
- } else if (Array.isArray(hooks.PostToolUse)) {
1051
- hooks.PostToolUse.push(viberailsHook);
1052
- }
1053
- settings.hooks = hooks;
1054
- fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1055
- `);
1056
- console.log(` ${import_chalk5.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1057
- }
1273
+ var import_chalk8 = __toESM(require("chalk"), 1);
1058
1274
  function setupPreCommitHook(projectRoot) {
1059
1275
  const lefthookPath = path12.join(projectRoot, "lefthook.yml");
1060
1276
  if (fs11.existsSync(lefthookPath)) {
1061
1277
  addLefthookPreCommit(lefthookPath);
1062
- console.log(` ${import_chalk5.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1278
+ console.log(` ${import_chalk8.default.green("\u2713")} lefthook.yml \u2014 added viberails pre-commit`);
1063
1279
  return;
1064
1280
  }
1065
1281
  const huskyDir = path12.join(projectRoot, ".husky");
1066
1282
  if (fs11.existsSync(huskyDir)) {
1067
1283
  writeHuskyPreCommit(huskyDir);
1068
- console.log(` ${import_chalk5.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1284
+ console.log(` ${import_chalk8.default.green("\u2713")} .husky/pre-commit \u2014 added viberails check`);
1069
1285
  return;
1070
1286
  }
1071
1287
  const gitDir = path12.join(projectRoot, ".git");
@@ -1075,7 +1291,7 @@ function setupPreCommitHook(projectRoot) {
1075
1291
  fs11.mkdirSync(hooksDir, { recursive: true });
1076
1292
  }
1077
1293
  writeGitHookPreCommit(hooksDir);
1078
- console.log(` ${import_chalk5.default.green("\u2713")} .git/hooks/pre-commit`);
1294
+ console.log(` ${import_chalk8.default.green("\u2713")} .git/hooks/pre-commit`);
1079
1295
  }
1080
1296
  }
1081
1297
  function writeGitHookPreCommit(hooksDir) {
@@ -1105,10 +1321,72 @@ npx viberails check --staged
1105
1321
  function addLefthookPreCommit(lefthookPath) {
1106
1322
  const content = fs11.readFileSync(lefthookPath, "utf-8");
1107
1323
  if (content.includes("viberails")) return;
1108
- const addition = ["", " viberails:", " run: npx viberails check --staged"].join("\n");
1109
- fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
1110
- ${addition}
1324
+ const hasPreCommit = /^pre-commit:/m.test(content);
1325
+ if (hasPreCommit) {
1326
+ const commandBlock = ["", " viberails:", " run: npx viberails check --staged"].join(
1327
+ "\n"
1328
+ );
1329
+ const updated = `${content.trimEnd()}
1330
+ ${commandBlock}
1331
+ `;
1332
+ fs11.writeFileSync(lefthookPath, updated);
1333
+ } else {
1334
+ const section = [
1335
+ "",
1336
+ "pre-commit:",
1337
+ " commands:",
1338
+ " viberails:",
1339
+ " run: npx viberails check --staged"
1340
+ ].join("\n");
1341
+ fs11.writeFileSync(lefthookPath, `${content.trimEnd()}
1342
+ ${section}
1343
+ `);
1344
+ }
1345
+ }
1346
+ function detectHookManager(projectRoot) {
1347
+ if (fs11.existsSync(path12.join(projectRoot, "lefthook.yml"))) return "Lefthook";
1348
+ if (fs11.existsSync(path12.join(projectRoot, ".husky"))) return "Husky";
1349
+ if (fs11.existsSync(path12.join(projectRoot, ".git"))) return "git hook";
1350
+ return void 0;
1351
+ }
1352
+ function setupClaudeCodeHook(projectRoot) {
1353
+ const claudeDir = path12.join(projectRoot, ".claude");
1354
+ if (!fs11.existsSync(claudeDir)) {
1355
+ fs11.mkdirSync(claudeDir, { recursive: true });
1356
+ }
1357
+ const settingsPath = path12.join(claudeDir, "settings.json");
1358
+ let settings = {};
1359
+ if (fs11.existsSync(settingsPath)) {
1360
+ try {
1361
+ settings = JSON.parse(fs11.readFileSync(settingsPath, "utf-8"));
1362
+ } catch {
1363
+ console.warn(
1364
+ ` ${import_chalk8.default.yellow("!")} .claude/settings.json contains invalid JSON \u2014 resetting to add hook`
1365
+ );
1366
+ settings = {};
1367
+ }
1368
+ }
1369
+ const hooks = settings.hooks ?? {};
1370
+ const existing = hooks.PostToolUse ?? [];
1371
+ if (existing.some((h) => JSON.stringify(h).includes("viberails"))) return;
1372
+ const extractFile = `node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync(0,'utf8')).tool_input?.file_path??'')}catch{}"`;
1373
+ const hookCommand = `FILE=$(${extractFile}) && [ -n "$FILE" ] && npx viberails check --files "$FILE" --format json; exit 0`;
1374
+ hooks.PostToolUse = [
1375
+ ...existing,
1376
+ {
1377
+ matcher: "Edit|Write",
1378
+ hooks: [
1379
+ {
1380
+ type: "command",
1381
+ command: hookCommand
1382
+ }
1383
+ ]
1384
+ }
1385
+ ];
1386
+ settings.hooks = hooks;
1387
+ fs11.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}
1111
1388
  `);
1389
+ console.log(` ${import_chalk8.default.green("\u2713")} .claude/settings.json \u2014 added viberails PostToolUse hook`);
1112
1390
  }
1113
1391
  function writeHuskyPreCommit(huskyDir) {
1114
1392
  const hookPath = path12.join(huskyDir, "pre-commit");
@@ -1124,187 +1402,6 @@ npx viberails check --staged
1124
1402
  fs11.writeFileSync(hookPath, "#!/bin/sh\nnpx viberails check --staged\n", { mode: 493 });
1125
1403
  }
1126
1404
 
1127
- // src/commands/init-wizard.ts
1128
- var p = __toESM(require("@clack/prompts"), 1);
1129
- var import_types4 = require("@viberails/types");
1130
- var import_chalk8 = __toESM(require("chalk"), 1);
1131
-
1132
- // src/display.ts
1133
- var import_types3 = require("@viberails/types");
1134
- var import_chalk7 = __toESM(require("chalk"), 1);
1135
-
1136
- // src/display-helpers.ts
1137
- var import_types = require("@viberails/types");
1138
- function groupByRole(directories) {
1139
- const map = /* @__PURE__ */ new Map();
1140
- for (const dir of directories) {
1141
- if (dir.role === "unknown") continue;
1142
- const existing = map.get(dir.role);
1143
- if (existing) {
1144
- existing.dirs.push(dir);
1145
- } else {
1146
- map.set(dir.role, { dirs: [dir] });
1147
- }
1148
- }
1149
- const groups = [];
1150
- for (const [role, { dirs }] of map) {
1151
- const label = import_types.ROLE_DESCRIPTIONS[role] ?? role;
1152
- const totalFiles = dirs.reduce((sum, d) => sum + d.fileCount, 0);
1153
- groups.push({
1154
- role,
1155
- label,
1156
- dirCount: dirs.length,
1157
- totalFiles,
1158
- singlePath: dirs.length === 1 ? dirs[0].path : void 0
1159
- });
1160
- }
1161
- return groups;
1162
- }
1163
- function formatSummary(stats, packageCount) {
1164
- const parts = [];
1165
- if (packageCount && packageCount > 1) {
1166
- parts.push(`${packageCount} packages`);
1167
- }
1168
- parts.push(`${stats.totalFiles.toLocaleString()} source files`);
1169
- parts.push(`${stats.totalLines.toLocaleString()} lines`);
1170
- parts.push(`avg ${Math.round(stats.averageFileLines)} lines/file`);
1171
- return parts.join(" \xB7 ");
1172
- }
1173
- function formatRoleGroup(group) {
1174
- const files = group.totalFiles === 1 ? "1 file" : `${group.totalFiles} files`;
1175
- if (group.singlePath) {
1176
- return `${group.label} \u2014 ${group.singlePath} (${files})`;
1177
- }
1178
- const dirs = group.dirCount === 1 ? "1 dir" : `${group.dirCount} dirs`;
1179
- return `${group.label} \u2014 ${dirs} (${files})`;
1180
- }
1181
-
1182
- // src/display-monorepo.ts
1183
- var import_types2 = require("@viberails/types");
1184
- var import_chalk6 = __toESM(require("chalk"), 1);
1185
-
1186
- // src/display.ts
1187
- function formatItem(item, nameMap) {
1188
- const name = nameMap?.[item.name] ?? item.name;
1189
- return item.version ? `${name} ${item.version}` : name;
1190
- }
1191
-
1192
- // src/commands/init-wizard.ts
1193
- var DEFAULT_WIZARD_RESULT = {
1194
- enforcement: "warn",
1195
- checks: {
1196
- fileSize: true,
1197
- naming: true,
1198
- tests: true,
1199
- boundaries: false
1200
- },
1201
- integration: ["pre-commit"]
1202
- };
1203
- async function runWizard(scanResult) {
1204
- const isMonorepo = scanResult.packages.length > 1;
1205
- displayScanSummary(scanResult);
1206
- const enforcement = await p.select({
1207
- message: "How strict should viberails be?",
1208
- initialValue: "warn",
1209
- options: [
1210
- { value: "warn", label: "Warn", hint: "show issues, never block commits" },
1211
- { value: "enforce", label: "Enforce", hint: "block commits with violations" }
1212
- ]
1213
- });
1214
- if (p.isCancel(enforcement)) {
1215
- p.cancel("Setup cancelled.");
1216
- return null;
1217
- }
1218
- const checkOptions = [
1219
- { value: "fileSize", label: "File size limit (300 lines)" },
1220
- { value: "naming", label: "File naming conventions" },
1221
- { value: "tests", label: "Missing test files" }
1222
- ];
1223
- if (isMonorepo) {
1224
- checkOptions.push({ value: "boundaries", label: "Import boundaries" });
1225
- }
1226
- const enabledChecks = await p.multiselect({
1227
- message: "Which checks should viberails run?",
1228
- options: checkOptions,
1229
- initialValues: ["fileSize", "naming", "tests"],
1230
- required: false
1231
- });
1232
- if (p.isCancel(enabledChecks)) {
1233
- p.cancel("Setup cancelled.");
1234
- return null;
1235
- }
1236
- const checks = {
1237
- fileSize: enabledChecks.includes("fileSize"),
1238
- naming: enabledChecks.includes("naming"),
1239
- tests: enabledChecks.includes("tests"),
1240
- boundaries: enabledChecks.includes("boundaries")
1241
- };
1242
- const integrationOptions = [
1243
- { value: "pre-commit", label: "Git pre-commit hook", hint: "runs on every commit" },
1244
- {
1245
- value: "claude-hook",
1246
- label: "Claude Code hook",
1247
- hint: "checks files as Claude edits them"
1248
- },
1249
- { value: "context-only", label: "Context files only", hint: "no hooks" }
1250
- ];
1251
- const integration = await p.multiselect({
1252
- message: "Where should checks run?",
1253
- options: integrationOptions,
1254
- initialValues: ["pre-commit"],
1255
- required: true
1256
- });
1257
- if (p.isCancel(integration)) {
1258
- p.cancel("Setup cancelled.");
1259
- return null;
1260
- }
1261
- const finalIntegration = integration.includes("context-only") ? ["context-only"] : integration;
1262
- return {
1263
- enforcement,
1264
- checks,
1265
- integration: finalIntegration
1266
- };
1267
- }
1268
- function displayScanSummary(scanResult) {
1269
- const { stack } = scanResult;
1270
- const parts = [];
1271
- if (stack.framework) parts.push(formatItem(stack.framework, import_types4.FRAMEWORK_NAMES));
1272
- parts.push(formatItem(stack.language));
1273
- if (stack.styling) parts.push(formatItem(stack.styling, import_types4.STYLING_NAMES));
1274
- if (stack.backend) parts.push(formatItem(stack.backend, import_types4.FRAMEWORK_NAMES));
1275
- p.log.info(`${import_chalk8.default.bold("Stack:")} ${parts.join(", ")}`);
1276
- if (stack.linter || stack.formatter || stack.testRunner || stack.packageManager) {
1277
- const tools = [];
1278
- if (stack.linter) tools.push(formatItem(stack.linter));
1279
- if (stack.formatter && stack.formatter !== stack.linter)
1280
- tools.push(formatItem(stack.formatter));
1281
- if (stack.testRunner) tools.push(formatItem(stack.testRunner));
1282
- if (stack.packageManager) tools.push(formatItem(stack.packageManager));
1283
- p.log.info(`${import_chalk8.default.bold("Tools:")} ${tools.join(", ")}`);
1284
- }
1285
- if (stack.libraries.length > 0) {
1286
- const libs = stack.libraries.map((lib) => formatItem(lib, import_types4.LIBRARY_NAMES)).join(", ");
1287
- p.log.info(`${import_chalk8.default.bold("Libraries:")} ${libs}`);
1288
- }
1289
- const groups = groupByRole(scanResult.structure.directories);
1290
- if (groups.length > 0) {
1291
- const structParts = groups.map((g) => formatRoleGroup(g));
1292
- p.log.info(`${import_chalk8.default.bold("Structure:")} ${structParts.join(", ")}`);
1293
- }
1294
- const conventionEntries = Object.entries(scanResult.conventions).filter(
1295
- ([, c]) => c.confidence !== "low"
1296
- );
1297
- if (conventionEntries.length > 0) {
1298
- const convParts = conventionEntries.map(([, c]) => {
1299
- const pct = Math.round(c.consistency);
1300
- return `${c.value} (${pct}%)`;
1301
- });
1302
- p.log.info(`${import_chalk8.default.bold("Conventions:")} ${convParts.join(", ")}`);
1303
- }
1304
- const pkgCount = scanResult.packages.length > 1 ? scanResult.packages.length : void 0;
1305
- p.log.info(`${import_chalk8.default.bold("Summary:")} ${formatSummary(scanResult.statistics, pkgCount)}`);
1306
- }
1307
-
1308
1405
  // src/commands/init.ts
1309
1406
  var CONFIG_FILE4 = "viberails.config.json";
1310
1407
  function filterHighConfidence(conventions) {
@@ -1319,13 +1416,6 @@ function filterHighConfidence(conventions) {
1319
1416
  }
1320
1417
  return filtered;
1321
1418
  }
1322
- function applyWizardResult(config, wizard) {
1323
- config.enforcement = wizard.enforcement;
1324
- if (!wizard.checks.fileSize) config.rules.maxFileLines = 0;
1325
- config.rules.enforceNaming = wizard.checks.naming;
1326
- config.rules.requireTests = wizard.checks.tests;
1327
- config.rules.enforceBoundaries = wizard.checks.boundaries;
1328
- }
1329
1419
  async function initCommand(options, cwd) {
1330
1420
  const startDir = cwd ?? process.cwd();
1331
1421
  const projectRoot = findProjectRoot(startDir);
@@ -1341,65 +1431,78 @@ async function initCommand(options, cwd) {
1341
1431
  );
1342
1432
  return;
1343
1433
  }
1344
- p2.intro("viberails");
1345
- const s = p2.spinner();
1346
- s.start("Scanning project...");
1434
+ console.log(import_chalk9.default.dim("Scanning project..."));
1347
1435
  const scanResult = await (0, import_scanner.scan)(projectRoot);
1348
- s.stop("Scan complete");
1349
- if (scanResult.statistics.totalFiles === 0) {
1350
- p2.log.warn(
1351
- `No source files detected. viberails will generate context with minimal content.
1352
- Run ${import_chalk9.default.cyan("viberails sync")} after adding source files.`
1353
- );
1354
- }
1355
- let wizard;
1356
- if (options.yes) {
1357
- wizard = { ...DEFAULT_WIZARD_RESULT };
1358
- } else {
1359
- const result = await runWizard(scanResult);
1360
- if (!result) return;
1361
- wizard = result;
1362
- }
1363
1436
  const config = (0, import_config4.generateConfig)(scanResult);
1364
1437
  if (options.yes) {
1365
1438
  config.conventions = filterHighConfidence(config.conventions);
1366
1439
  }
1367
- applyWizardResult(config, wizard);
1368
- if (wizard.checks.boundaries && config.workspace && config.workspace.packages.length > 0) {
1369
- s.start("Inferring boundary rules...");
1370
- const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1371
- const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1372
- const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
1373
- const inferred = inferBoundaries(graph);
1374
- const ruleCount = Object.values(inferred).reduce((sum, denied) => sum + denied.length, 0);
1375
- if (ruleCount > 0) {
1376
- config.boundaries = inferred;
1377
- s.stop(`Inferred ${ruleCount} boundary rules`);
1378
- } else {
1379
- s.stop("No boundary rules could be inferred");
1380
- config.rules.enforceBoundaries = false;
1440
+ displayScanResults(scanResult);
1441
+ if (scanResult.statistics.totalFiles === 0) {
1442
+ console.log(
1443
+ import_chalk9.default.yellow("!") + " No source files detected. viberails will generate context with minimal content.\n Run " + import_chalk9.default.cyan("viberails sync") + " after adding source files.\n"
1444
+ );
1445
+ }
1446
+ displayRulesPreview(config);
1447
+ if (!options.yes) {
1448
+ const accepted = await confirm("Proceed with these settings?");
1449
+ if (!accepted) {
1450
+ console.log("Aborted.");
1451
+ return;
1381
1452
  }
1382
1453
  }
1454
+ if (config.workspace && config.workspace.packages.length > 0) {
1455
+ let shouldInfer = options.yes;
1456
+ if (!options.yes) {
1457
+ console.log(import_chalk9.default.dim(" Scans imports between packages to suggest dependency rules"));
1458
+ shouldInfer = await confirm("Infer boundary rules from import patterns?");
1459
+ }
1460
+ if (shouldInfer) {
1461
+ console.log(import_chalk9.default.dim("Building import graph..."));
1462
+ const { buildImportGraph, inferBoundaries } = await import("@viberails/graph");
1463
+ const packages = resolveWorkspacePackages(projectRoot, config.workspace);
1464
+ const graph = await buildImportGraph(projectRoot, { packages, ignore: config.ignore });
1465
+ const inferred = inferBoundaries(graph);
1466
+ if (inferred.length > 0) {
1467
+ config.boundaries = inferred;
1468
+ config.rules.enforceBoundaries = true;
1469
+ console.log(` ${import_chalk9.default.green("\u2713")} Inferred ${inferred.length} boundary rules`);
1470
+ }
1471
+ }
1472
+ }
1473
+ const hookManager = detectHookManager(projectRoot);
1474
+ let integrations = { preCommitHook: true, claudeCodeHook: true };
1475
+ if (!options.yes) {
1476
+ console.log("");
1477
+ integrations = await selectIntegrations(hookManager);
1478
+ }
1383
1479
  fs12.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
1384
1480
  `);
1385
1481
  writeGeneratedFiles(projectRoot, config, scanResult);
1386
1482
  updateGitignore(projectRoot);
1387
- if (wizard.integration.includes("pre-commit")) {
1483
+ console.log(`
1484
+ ${import_chalk9.default.bold("Created:")}`);
1485
+ console.log(` ${import_chalk9.default.green("\u2713")} ${CONFIG_FILE4}`);
1486
+ console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md`);
1487
+ console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json`);
1488
+ if (integrations.preCommitHook) {
1388
1489
  setupPreCommitHook(projectRoot);
1389
1490
  }
1390
- if (wizard.integration.includes("claude-hook")) {
1491
+ if (integrations.claudeCodeHook) {
1391
1492
  setupClaudeCodeHook(projectRoot);
1392
1493
  }
1393
- p2.log.success(`${import_chalk9.default.bold("Created:")}`);
1394
- console.log(` ${import_chalk9.default.green("\u2713")} ${CONFIG_FILE4}`);
1395
- console.log(` ${import_chalk9.default.green("\u2713")} .viberails/context.md`);
1396
- console.log(` ${import_chalk9.default.green("\u2713")} .viberails/scan-result.json`);
1397
- p2.outro(
1398
- `${import_chalk9.default.bold("Next steps:")}
1399
- 1. Review ${import_chalk9.default.cyan("viberails.config.json")} and adjust rules
1400
- 2. Commit ${import_chalk9.default.cyan("viberails.config.json")} and ${import_chalk9.default.cyan(".viberails/context.md")}
1401
- 3. Run ${import_chalk9.default.cyan("viberails check")} to verify your project passes`
1402
- );
1494
+ const filesToCommit = [
1495
+ `${import_chalk9.default.cyan("viberails.config.json")}`,
1496
+ import_chalk9.default.cyan(".viberails/context.md")
1497
+ ];
1498
+ if (integrations.claudeCodeHook) {
1499
+ filesToCommit.push(import_chalk9.default.cyan(".claude/settings.json"));
1500
+ }
1501
+ console.log(`
1502
+ ${import_chalk9.default.bold("Next steps:")}`);
1503
+ console.log(` 1. Review ${import_chalk9.default.cyan("viberails.config.json")} and adjust rules`);
1504
+ console.log(` 2. Commit ${filesToCommit.join(", ")}`);
1505
+ console.log(` 3. Run ${import_chalk9.default.cyan("viberails check")} to verify your project passes`);
1403
1506
  }
1404
1507
  function updateGitignore(projectRoot) {
1405
1508
  const gitignorePath = path13.join(projectRoot, ".gitignore");
@@ -1409,8 +1512,9 @@ function updateGitignore(projectRoot) {
1409
1512
  }
1410
1513
  if (!content.includes(".viberails/scan-result.json")) {
1411
1514
  const block = "\n# viberails\n.viberails/scan-result.json\n";
1412
- fs12.writeFileSync(gitignorePath, `${content.trimEnd()}
1413
- ${block}`);
1515
+ const prefix = content.length === 0 ? "" : `${content.trimEnd()}
1516
+ `;
1517
+ fs12.writeFileSync(gitignorePath, `${prefix}${block}`);
1414
1518
  }
1415
1519
  }
1416
1520
 
@@ -1434,18 +1538,30 @@ async function syncCommand(cwd) {
1434
1538
  console.log(import_chalk10.default.dim("Scanning project..."));
1435
1539
  const scanResult = await (0, import_scanner2.scan)(projectRoot);
1436
1540
  const merged = (0, import_config5.mergeConfig)(existing, scanResult);
1437
- fs13.writeFileSync(configPath, `${JSON.stringify(merged, null, 2)}
1541
+ const existingJson = JSON.stringify(existing, null, 2);
1542
+ const mergedJson = JSON.stringify(merged, null, 2);
1543
+ const configChanged = existingJson !== mergedJson;
1544
+ if (configChanged) {
1545
+ console.log(
1546
+ ` ${import_chalk10.default.yellow("!")} Config updated \u2014 review ${import_chalk10.default.cyan(CONFIG_FILE5)} for changes`
1547
+ );
1548
+ }
1549
+ fs13.writeFileSync(configPath, `${mergedJson}
1438
1550
  `);
1439
1551
  writeGeneratedFiles(projectRoot, merged, scanResult);
1440
1552
  console.log(`
1441
1553
  ${import_chalk10.default.bold("Synced:")}`);
1442
- console.log(` ${import_chalk10.default.green("\u2713")} ${CONFIG_FILE5} \u2014 updated`);
1554
+ if (configChanged) {
1555
+ console.log(` ${import_chalk10.default.yellow("!")} ${CONFIG_FILE5} \u2014 updated (review changes)`);
1556
+ } else {
1557
+ console.log(` ${import_chalk10.default.green("\u2713")} ${CONFIG_FILE5} \u2014 unchanged`);
1558
+ }
1443
1559
  console.log(` ${import_chalk10.default.green("\u2713")} .viberails/context.md \u2014 regenerated`);
1444
1560
  console.log(` ${import_chalk10.default.green("\u2713")} .viberails/scan-result.json \u2014 updated`);
1445
1561
  }
1446
1562
 
1447
1563
  // src/index.ts
1448
- var VERSION = "0.3.0";
1564
+ var VERSION = "0.3.1";
1449
1565
  var program = new import_commander.Command();
1450
1566
  program.name("viberails").description("Guardrails for vibe coding").version(VERSION);
1451
1567
  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) => {
@@ -1466,12 +1582,13 @@ program.command("sync").description("Re-scan and update generated files").action
1466
1582
  process.exit(1);
1467
1583
  }
1468
1584
  });
1469
- 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(
1585
+ 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(
1470
1586
  async (options) => {
1471
1587
  try {
1472
1588
  const exitCode = await checkCommand({
1473
1589
  ...options,
1474
- noBoundaries: options.boundaries === false
1590
+ noBoundaries: options.boundaries === false,
1591
+ format: options.format === "json" ? "json" : "text"
1475
1592
  });
1476
1593
  process.exit(exitCode);
1477
1594
  } catch (err) {